web-dev-qa-db-ja.com

Cでは、構造体を返すか、構造体へのポインタを返すかをどのように選択しますか?

最近、私の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)
53

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);
57
Jon Purdy

これはほとんど常にABIの安定性に関するものです。ライブラリのバージョン間のバイナリ安定性。そうでない場合は、動的なサイズの構造体を持つことがあります。まれに、非常に大きなstructsまたはパフォーマンスに関するものです。


ヒープにstructを割り当てて返すことは、値ごとに返すのと同じくらい速いことは非常にまれです。 structは巨大でなければなりません。

実際、速度2は、値による戻りではなく、手法2によるポインターによる戻りの背後にある理由ではありません。

テクニック2はABIの安定性のために存在します。 structがあり、ライブラリの次のバージョンがさらに20フィールドを追加する場合、ライブラリの以前のバージョンのコンシューマはバイナリ互換です事前に構築されたポインターが渡される場合。彼らが知っているstructの終わりを越える余分なデータは、彼らが知る必要がないものです。

スタックでそれを返す場合、呼び出し元はそれのためにメモリを割り当てています、そして、彼らはそれがどれくらい大きいかについてあなたに同意しなければなりません。ライブラリが最後に再構築されてから更新された場合、スタックを破棄します。

手法2では、返すポインターの前後に余分なデータを非表示にすることもできます(構造体の末尾にデータを追加するバージョンは、そのバリアントです)。可変サイズの配列で構造体を終了するか、いくつかの追加データをポインターに追加するか、その両方を行うことができます。

安定したABIでスタックに割り当てられたstructsが必要な場合、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プログラマはほとんどいません。

13
Lundin

最初のアプローチの長所:

  • 記述するコードが少なくなります。
  • 複数の値を返すユースケースの場合、より慣用的です。
  • 動的割り当てのないシステムで動作します。
  • 小さいオブジェクトや小さいオブジェクトの場合はおそらく高速です。
  • freeを忘れてもメモリリークはありません。

短所:

  • オブジェクトが大きい(たとえば、メガバイト)場合、スタックオーバーフローが発生する可能性があります。また、コンパイラが最適化しない場合は遅くなる可能性があります。
  • 1970年代にCを習得し、これが不可能で最新の状態に保たれていない人々を驚かせるかもしれません。
  • 自分自身の一部へのポインタを含むオブジェクトでは機能しません。
9
M.M

びっくりしました。

違いは、例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);
}

かなり効率的なコードにコンパイルされます。大規模な監視メンバーは、不要なメイクワークコピーを生成しません。

4
Malcolm McLean

他の言語(Pythonなど)とは異なり、Cには Tuple の概念がありません。たとえば、次はPythonで有効です。

def foo():
    return 1,2

x,y = foo()
print x, y

関数fooは、xyに割り当てられる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 );
}

これは、関数が構造体を返すのを確認できる一例です。

3
user3386109