最近、私のCマッスルに取り組んで、私が取り組んできた多くのライブラリーを調べてみると、何が良いプラクティスであるかについての良いアイデアが得られました。私が見たことがないことの1つは、構造体を返す関数です。
something_t make_something() { ... }
私がこれを吸収したのは、これを行う「正しい」方法です。
something_t *make_something() { ... }
void destroy_something(something_t *object) { ... }
コードスニペット2のアーキテクチャは、スニペット1よりもはるかに人気があります。それでは、スニペット1のように、なぜ構造体を直接返すのでしょうか。 2つのオプションを選択する場合、どのような違いを考慮する必要がありますか?
さらに、このオプションはどのように比較されますか?
void make_something(something_t *object)
something_t
が小さい場合(読み取り:コピーはポインターをコピーするのと同じくらい安い)、デフォルトでスタックに割り当てたい場合:
something_t make_something(void);
something_t stack_thing = make_something();
something_t *heap_thing = malloc(sizeof *heap_thing);
*heap_thing = make_something();
something_t
が大きい場合、またはヒープに割り当てたい場合:
something_t *make_something(void);
something_t *heap_thing = make_something();
something_t
のサイズに関係なく、それがどこに割り当てられているか気にしない場合:
void make_something(something_t *);
something_t stack_thing;
make_something(&stack_thing);
something_t *heap_thing = malloc(sizeof *heap_thing);
make_something(heap_thing);
これはほとんど常にABIの安定性に関するものです。ライブラリのバージョン間のバイナリ安定性。そうでない場合は、動的なサイズの構造体を持つことがあります。まれに、非常に大きなstruct
sまたはパフォーマンスに関するものです。
ヒープにstruct
を割り当てて返すことは、値ごとに返すのと同じくらい速いことは非常にまれです。 struct
は巨大でなければなりません。
実際、速度2は、値による戻りではなく、手法2によるポインターによる戻りの背後にある理由ではありません。
テクニック2はABIの安定性のために存在します。 struct
があり、ライブラリの次のバージョンがさらに20フィールドを追加する場合、ライブラリの以前のバージョンのコンシューマはバイナリ互換です事前に構築されたポインターが渡される場合。彼らが知っているstruct
の終わりを越える余分なデータは、彼らが知る必要がないものです。
スタックでそれを返す場合、呼び出し元はそれのためにメモリを割り当てています、そして、彼らはそれがどれくらい大きいかについてあなたに同意しなければなりません。ライブラリが最後に再構築されてから更新された場合、スタックを破棄します。
手法2では、返すポインターの前後に余分なデータを非表示にすることもできます(構造体の末尾にデータを追加するバージョンは、そのバリアントです)。可変サイズの配列で構造体を終了するか、いくつかの追加データをポインターに追加するか、その両方を行うことができます。
安定したABIでスタックに割り当てられたstruct
sが必要な場合、struct
と通信するほとんどすべての関数にバージョン情報を渡す必要があります。
そう
something_t make_something(unsigned library_version) { ... }
ここで、library_version
は、something_t
のどのバージョンを返すかを決定するためにライブラリによって使用され、操作するスタックの量を変更します。これは標準Cを使用しては不可能ですが、
void make_something(something_t* here) { ... }
です。この場合、something_t
の最初の要素(またはサイズフィールド)としてversion
フィールドがあり、make_something
を呼び出す前にそれを設定する必要があります。
something_t
を使用する他のライブラリコードは、次にversion
フィールドを照会して、使用しているsomething_t
のバージョンを判別します。
経験則として、値によってstruct
オブジェクトを渡さないでください。実際には、CPUが単一の命令で処理できる最大サイズ以下である限り、そうすることは問題ありません。しかし、スタイル的には、通常はそれを避けます。値で構造体を渡さない場合は、後で構造体にメンバーを追加できますが、パフォーマンスには影響しません。
void make_something(something_t *object)
はCで構造を使用する最も一般的な方法だと思います。割り当ては呼び出し元に任せます。効率的ですが、きれいではありません。
ただし、オブジェクト指向のCプログラムはsomething_t *make_something()
を使用します。これは、不透明型の概念で構築されているため、ポインターを使用する必要があるためです。返されるポインタが動的メモリを指すか、他の何かを指すかは、実装によって異なります。 OO不透明型を使用することは、多くの場合、より複雑なCプログラムを設計するための最もエレガントで最良の方法の1つですが、残念ながら、それを知っている/気にしないCプログラマはほとんどいません。
最初のアプローチの長所:
free
を忘れてもメモリリークはありません。短所:
びっくりしました。
違いは、例1はスタック上に構造を作成し、例2はヒープ上に構造を作成することです。 C、または事実上CであるC++コードでは、ヒープ上にほとんどのオブジェクトを作成するのが慣用的で便利です。 C++ではそうではなく、ほとんどがスタックに置かれます。スタック上にオブジェクトを作成する場合、デストラクタが自動的に呼び出されるため、ヒープ上にオブジェクトを作成する場合は、明示的に呼び出す必要があるため、メモリリークがないことを確認し、例外を処理する方がはるかに簡単ですすべてがスタックに置かれます。 Cでは、デストラクタはとにかく明示的に呼び出される必要があり、特別なデストラクタ関数の概念はありません(デストラクタはもちろんありますが、destroy_myobject()のような名前を持つ通常の関数です)。
現在、C++の例外は低レベルのコンテナオブジェクトです。ベクトル、ツリー、ハッシュマップなど。これらはヒープメンバーを保持し、デストラクタがあります。現在、ほとんどのメモリを大量に使用するオブジェクトは、サイズ、ID、タグなどを提供するいくつかの即時データメンバーで構成され、STL構造の残りの情報、ピクセルデータのベクトルまたは英語の単語/値のペアのマップで構成されます。そのため、実際にはほとんどのデータはC++であってもヒープ上にあります。
そして、最新のC++は、このパターン
class big
{
std::vector<double> observations; // thousands of observations
int station_x; // a bit of data associated with them
int station_y;
std::string station_name;
}
big retrieveobservations(int a, int b, int c)
{
big answer;
// lots of code to fill in the structure here
return answer;
}
void high_level()
{
big myobservations = retriveobservations(1, 2, 3);
}
かなり効率的なコードにコンパイルされます。大規模な監視メンバーは、不要なメイクワークコピーを生成しません。
他の言語(Pythonなど)とは異なり、Cには Tuple の概念がありません。たとえば、次はPythonで有効です。
def foo():
return 1,2
x,y = foo()
print x, y
関数foo
は、x
とy
に割り当てられる2つの値をタプルとして返します。
CにはTupleの概念がないため、関数から複数の値を返すのは不便です。これを回避する1つの方法は、値を保持する構造を定義してから、次のように構造を返すことです。
typedef struct { int x, y; } stPoint;
stPoint foo( void )
{
stPoint point = { 1, 2 };
return point;
}
int main( void )
{
stPoint point = foo();
printf( "%d %d\n", point.x, point.y );
}
これは、関数が構造体を返すのを確認できる一例です。