Cで構造体を返す多くの関数が、実際に構造体へのポインターを返すのはなぜですか?


49

return関数のステートメントで構造全体を返すのではなく、構造へのポインタを返すことの利点は何ですか?

私はfopen他の低レベル関数のような関数について話しているが、おそらく構造体へのポインタを返す高レベル関数もあるだろう。

これは単なるプログラミングの質問ではなく、設計上の選択であり、2つの方法の長所と短所についてもっと知りたいと思っています。

構造体へのポインターを返すのが利点だと思った理由の1つは、ポインターを返すことで関数が失敗したかどうかをより簡単に判断できることNULLです。

完全な構造を返すのは、NULL私にとっては難しいか、効率が悪いでしょう。これは正当な理由ですか?


9
@ JohnR.Strohm私はそれを試しましたが、実際に動作します。関数は構造体を返すことができます。...では、実行されない理由は何ですか?
yoyo_fun

27
事前標準化Cでは、構造体をコピーしたり、値で渡すことができませんでした。C標準ライブラリには、その時代から今日ではそのように書かれていない多くのホールドアウトがあります。たとえば、C11まで完全に誤って設計されたgets()関数が削除されるまでかかりました。一部のプログラマーは、構造体をコピーすることにまだ嫌悪感を抱いており、古い習慣は激しく死にます。
アモン

26
FILE*事実上不透明なハンドルです。ユーザーコードは、その内部構造を気にしないでください。
CodesInChaos

3
参照によるリターンは、ガベージコレクションがある場合にのみ妥当なデフォルトです。
イダンアリー

6
@ JohnR.Strohmあなたのプロファイルの「非常に上級」は1989年以前に戻っているようです;-)-ANSI CがK&R Cが許可していないことを許可したとき:割り当ての構造、パラメーターの受け渡し、および戻り値。K&Rのオリジナルの本は確かに明確に述べています(言い換えます):「構造を使用して正確に2つのことを行い、そのアドレスを& 使用してメンバーにアクセスできます.。」
ピーター-モニカの復活

回答:


61

関数のような関数fopenstruct型のインスタンスの代わりにポインターを返すのには、いくつかの実用的な理由があります。

  1. structユーザーに対してタイプの表現を非表示にします。
  2. オブジェクトを動的に割り当てています。
  3. 複数の参照を介してオブジェクトの単一のインスタンスを参照しています。

以下のようなタイプの場合はFILE *ユーザーにタイプの表現の詳細を公開したくないので、それはだ- FILE *オブジェクトが不透明なハンドルとして機能し、あなただけのさまざまなI / Oルーチンにそのハンドルを渡す(しばらくはFILEしばしばありますstruct型として実装されている必要はありません)。

そのため、ヘッダーの不完全な struct型をどこかに公開できます。

typedef struct __some_internal_stream_implementation FILE;

不完全な型のインスタンスを宣言することはできませんが、そのポインターを宣言することはできます。作成することができます私はFILE *とてそれに割り当てfopenfreopenなどが、私は直接それが指すオブジェクトを操作することはできません。

また、fopen関数は、FILEオブジェクトを使用して、mallocまたは同様に動的に割り当てている可能性があります。その場合、ポインターを返すことは理にかなっています。

最後に、ある種の状態をstructオブジェクトに保存している可能性があり、その状態をいくつかの異なる場所で利用可能にする必要があります。struct型のインスタンスを返した場合、それらのインスタンスはメモリ内のオブジェクトとは別のオブジェクトになり、最終的には同期しなくなります。単一のオブジェクトへのポインタを返すことにより、全員が同じオブジェクトを参照します。


31
ポインターを不透明(OPAQUE)型として使用することの特別な利点は、構造自体がライブラリーのバージョン間で変更でき、呼び出し元を再コンパイルする必要がないことです。
バーマー

6
@Barmarは:確かに、ABIの安定性があるCの巨大なセールスポイント、それは不透明なポインタのない安定したようではありません。
マチューM.17年

37

「構造を戻す」には2つの方法があります。データのコピーを返すことも、データへの参照(ポインター)を返すこともできます。一般的に、いくつかの理由で、ポインターを返す(および一般に渡す)ことをお勧めします。

まず、構造のコピーは、ポインターのコピーよりも多くのCPU時間を必要とします。これがコードで頻繁に行われることである場合、顕著なパフォーマンスの違いを引き起こす可能性があります。

第二に、ポインタを何度コピーしても、メモリ内の同じ構造を指し示しています。それに対するすべての変更は、同じ構造に反映されます。ただし、構造自体をコピーしてから変更を加えた場合、そのコピーにのみ変更が反映されます。別のコピーを保持するコードには、変更は表示されません。時々、非常にまれに、これがあなたが望むものですが、ほとんどの場合そうではありません、そしてあなたがそれを間違えたらバグを引き起こす可能性があります。


54
ポインターで戻ることの欠点:今、そのオブジェクトの所有権を追跡し、可能な限り解放する必要があります。また、ポインターの間接化は、クイックコピーよりもコストがかかる場合があります。ここには多くの変数があるため、ポインターを使用することは普遍的に優れているわけではありません。
アモン

17
また、最近のポインターは、ほとんどのデスクトップおよびサーバープラットフォームで64ビットです。私のキャリアでは、64ビットに適合するいくつかの構造体を見てきました。そのため、ポインタをコピーする方が構造体をコピーするよりも安いとは必ずしも言えません。
ソロモンスロー

37
これはほとんど良い答えですが、この部分については時々同意しますが、非常にまれですが、これはあなたが望むものですが、ほとんどの場合そうではありません -まったく逆です。ポインターを返すと、いくつかの種類の望ましくない副作用が発生し、ポインターの所有権を誤って取得するいくつかの種類の厄介な方法が許可されます。CPU時間はそれほど重要ではないケースでは、私はそれはオプションがある場合、それは、コピーバリアントを好むずっと少ないエラーが発生しやすいです。
Doc Brown

6
これは実際には外部APIにのみ適用されることに注意してください。内部関数については、過去数十年のわずかに有能なコンパイラーすべてが、追加の引数としてポインターを取り、そこで直接オブジェクトを構築するために大きな構造体を返す関数を書き直します。不変vs可変の引数は十分に頻繁に行われていますが、不変データ構造はほとんど決してあなたが望むものではないという主張は真実ではないということは、私たち全員が同意できると思います。
Voo

6
また、ポインターの長所として、編集用のファイアウォールを挙げることもできます。広く共有されたヘッダーを持つ大規模なプログラムでは、関数を持つ不完全な型により、実装の詳細が変更されるたびに再コンパイルする必要がなくなります。コンパイル動作の改善は、実際には、インターフェースと実装が分離されたときに達成されるカプセル化の副作用です。値で返す(および渡す、割り当てる)には実装情報が必要です。
ピーター-モニカの復活

12

他の回答に加えて、小さな struct by値を返すことも価値があります。たとえば、1つのデータのペアとそれに関連するエラー(または成功)コードを返すことができます。

例として、fopen1つのデータ(opened FILE*)のみを返し、エラーが発生した場合、errno疑似グローバル変数を介してエラーコードを返します。ただしstructFILE*ハンドルとエラーコード(ファイルハンドルがの場合に設定されますNULL)の2つのメンバーのいずれかを返す方がよいでしょう。歴史的な理由から、そうではありません(そしてエラーはerrnoグローバルに報告されますが、これは今日マクロです)。

Go言語には、2つ(または少数)の値を返すための優れた表記法があることに注意してください。

また、Linux / x86-64では、ABIおよび呼び出し規約(x86-psABIページを参照)はstruct、2つのスカラーメンバー(たとえば、ポインターと整数、または2つのポインター、または2つの整数)が2つのレジスタを通じて返されることを指定します(そして、これは非常に効率的であり、メモリを通過しません)。

そのため、新しいCコードでは、小さなCを返すstruct方が読みやすく、スレッドフレンドリーで、効率的です。


実際には小さな構造体がに詰め込まれていrdx:raxます。したがってstruct foo { int a,b; };rax(たとえばshift / orで)パックされて返され、shift / movでアンパックする必要があります。Godboltの例を次に示します。しかし、x86は、高ビットを気にせずに32ビット操作に64ビットレジスタの低32ビットを使用できるので、常に悪いですが、2メンバー構造体にほとんどの場合2レジスタを使用するよりも明らかに悪いです。
ピーターコーデス

関連:bugs.llvm.org/show_bug.cgi?id=34840 std::optional<int>はの上半分にブール値を返すraxため、でテストするには64ビットのマスク定数が必要testです。または、を使用できますbt。しかしdl、コンパイラが「プライベート」関数に対して行うべきであるを使用するのに比べて、呼び出し元と呼び出し先にとっては面倒です。また、関連する:libstdc ++ std::optional<T>はTであっても簡単にコピーできないため、常に隠しポインターstackoverflow.com/questions/46544019/…を介して戻ります。(libc ++は簡単にコピー可能です)
ピーターコーデス

@PeterCordes:あなたの関連するものはCではなくC ++です
Basile Starynkevitch

おっと、そう。まあ、同じことが当てはまる正確struct { int a; _Bool b; };呼び出し側は自明-コピー可能C ++の構造体は、Cと同じABI使用しているため、ブール値をテストしたい場合は、Cで
ピーター・コルド

1
古典的な例div_t div()
chux-モニカを

6

あなたは正しい軌道に乗っています

あなたが言及した理由は両方とも有効です:

構造体へのポインターを返すことの利点だと思った理由の1つは、NULLポインターを返すことで関数が失敗したかどうかをより簡単に判断できることです。

NULLであるFULL構造体を返すことは、私にとっては難しいか、効率が低いと思われます。これは正当な理由ですか?

メモリ内のどこかにテクスチャ(たとえば)があり、プログラム内の複数の場所でそのテクスチャを参照する場合。参照するたびにコピーを作成するのは賢明ではありません。代わりに、テクスチャを参照するためのポインタを単に渡すと、プログラムははるかに高速に実行されます。

ただし、最大の理由は動的なメモリ割り当てです。多くの場合、プログラムのコンパイル時には、特定のデータ構造に必要なメモリ量が正確にわからないことがあります。この場合、使用する必要のあるメモリの量は実行時に決定されます。「malloc」を使用してメモリを要求し、「free」の使用が終了したら解放することができます。

この良い例は、ユーザーが指定したファイルから読み取ることです。この場合、プログラムをコンパイルするときにファイルがどれほど大きいかはわかりません。プログラムが実際に実行されているときに必要なメモリ量のみを把握できます。

mallocとfreeの両方が、メモリ内の場所へのポインターを返します。したがって、動的メモリ割り当てを利用する関数は、メモリ内で構造を作成した場所へのポインタを返します。

また、コメントから、関数から構造体を返すことができるかどうかについて疑問があることがわかります。あなたは確かにこれを行うことができます。以下が機能するはずです。

struct s1 {
   int integer;
};

struct s1 f(struct s1 input){
   struct s1 returnValue = xinput
   return returnValue;
}

int main(void){
   struct s1 a = { 42 };
   struct s1 b= f(a);

   return 0;
}

構造体型が既に定義されている場合、特定の変数に必要なメモリ量を知ることができないのはどうしてですか?
yoyo_fun

9
@JenniferAnderson Cには不完全な型の概念があります。型名は宣言できますが、まだ定義されていないため、サイズは使用できません。そのタイプの変数を宣言することはできませんが、そのタイプへのポインターを宣言することはできますstruct incomplete* foo(void)。そうすれば、ヘッダーで関数を宣言できますが、Cファイル内で構造体のみを定義できるため、カプセル化が可能になります。
アモン

@amonだから、これは、関数ヘッダー(プロトタイプ/署名)を宣言する前に、それらがどのように動作するかを実際にCで行う方法ですか?また、Cの構造体と共用体にも同じことを行うことができます
yoyo_fun

@JenniferAndersonは、ヘッダーファイルで関数プロトタイプ(本体のない関数)を宣言し、関数の本体を知らなくても他のコードでそれらの関数を呼び出すことができます。コンパイラーは引数の配置方法と、戻り値。プログラムをリンクするまでに、実際には関数定義(つまり、本体)を知っている必要がありますが、一度だけ処理する必要があります。非単純型を使用する場合、その型の構造も知る必要がありますが、ポインターは多くの場合同じサイズであり、プロトタイプの使用には関係ありません。
-simpleuser

6

aのようなものは、FILE*クライアントコードに関する限り、実際には構造体へのポインタではなく、ファイルのような他のエンティティに関連付けられた不透明な識別子の形式です。プログラムがを呼び出すとfopen、通常、返される構造体の内容は一切気にしません。気にするのは、他の関数freadが必要なことを何でもするということだけです。

標準ライブラリがFILE*そのファイル内の現在の読み取り位置などに関する情報を保持している場合、への呼び出しfreadはその情報を更新できる必要があります。freadへのポインタを受け取ると、FILE簡単になります。fread代わりにを受け取った場合、呼び出し元が保持してFILEいるFILEオブジェクトを更新する方法はありません。


3

情報隠蔽

関数のreturnステートメントで構造全体を返すのではなく、構造へのポインターを返すことの利点は何ですか?

最も一般的なのは情報隠蔽です。Cには、たとえば、structプライベートのフィールドを作成する機能はなく、それらにアクセスするメソッドは提供していません。

したがって、開発者がのように指示先の内容を強制的に表示および改ざんできないようにしたい場合FILE、唯一の方法は、指示先のサイズが定義は外の世界には知られていない。の定義はFILE、のようにfopen、その定義を必要とする操作を実装している人にのみ表示されますが、パブリックヘッダーには構造体宣言のみが表示されます。

バイナリ互換性

構造定義を非表示にすると、dylib APIのバイナリ互換性を維持するための余裕を提供できます。ライブラリの実装者は、ライブラリの使用者とのバイナリ互換性を損なうことなく、不透明な構造のフィールドを変更できます。コードの性質は、構造の大きさやフィールドではなく、構造で何ができるかを知るだけでよいためです。持っています。

例として、今日のWindows 95時代に構築されたいくつかの古代のプログラムを実際に実行できます(常に完全ではありませんが、驚くほど多くがまだ動作しています)。古代のバイナリのコードの一部では、Windows 95時代からサイズと内容が変更された構造体への不透明なポインターが使用されていた可能性があります。それでも、プログラムはこれらの構造のコンテンツにさらされていないため、新しいバージョンのウィンドウで動作し続けます。バイナリ互換性が重要なライブラリで作業する場合、クライアントが公開されていないものは一般に、後方互換性を損なうことなく変更できます。

効率

NULLである完全な構造体を返すのは、私にとっては難しいか、効率が悪いでしょう。これは正当な理由ですか?

malloc既に割り当てられている可変サイズのアロケーターではなく、固定サイズのアロケーターのように、一般的にバックグラウンドで使用される一般化されたメモリーアロケーターよりもはるかに少ない一般的なメモリアロケーターがない限り、スタックが実際に適合し、スタックに割り当てられると仮定すると、通常は効率が低下します。この場合、ライブラリ開発者がに関連する不変式(概念的な保証)を維持できるようにすることは、この場合の安全性のトレードオフFILEです。

少なくともパフォーマンスの観点からfopen、ポインターを返すことNULLは、ファイルを開くことができない場合にのみ返されるため、それほど有効な理由ではありません。それは、すべての一般的なケースの実行パスを遅くすることと引き換えに、例外的なシナリオを最適化することです。場合によっては、設計をより簡単にして、ポインターをNULL返して、何らかの事後条件で返せるようにするための有効な生産性の理由があるかもしれません。

ファイル操作の場合、オーバーヘッドはファイル操作自体に比べて比較的些細なものであり、手動fcloseでの回避は避けられません。私たちはクライアントに定義を露出させることにより(終値)のリソースを解放するの面倒保存することができますようではありませんそれはそうFILEとして値によってそれを返すにfopen自身がヒープ割り当てを避けるために、またはファイル操作の相対的なコスト与えられたパフォーマンス向上の多くを期待します。

ホットスポットと修正

しかし、他のケースでは、malloc不透明なポインターでこのプラクティスを頻繁に使用し、ヒープ上に必要以上に多くのものを割り当てた結果として、ホットスポットがあり、不要な強制キャッシュミスがあるレガシーコードベースで多くの無駄なCコードをプロファイルしました大きなループ。

私が代わりに使用する代替方法は、クライアントがそれらを改ざんすることを意図していない場合でも、構造定義を公開することです。

struct Foo
{
   /* priv_* indicates that you shouldn't tamper with these fields! */
   int priv_internal_field;
   int priv_other_one;
};

struct Foo foo_create(void);
void foo_destroy(struct Foo* foo);
void foo_something(struct Foo* foo);

将来的にバイナリ互換性の懸念がある場合、次のように将来の目的のために余分なスペースを余分に予約するだけで十分であることがわかりました:

struct Foo
{
   /* priv_* indicates that you shouldn't tamper with these fields! */
   int priv_internal_field;
   int priv_other_one;

   /* reserved for possible future uses (emergency backup plan).
     currently just set to null. */
   void* priv_reserved;
};

その予約済みスペースは少し無駄ですが、将来Foo、ライブラリを使用するバイナリを壊さずにデータを追加する必要がある場合に、命を救うことができます。

私の意見では、情報の隠蔽とバイナリ互換性は、通常、可変長構造体以外の構造体のヒープ割り当てのみを許可する唯一の正当な理由です(これは常にそれを必要とするか、クライアントが割り当てなければならない場合は少なくとも少し厄介です) VLSを割り当てるためのVLA形式のスタック上のメモリ)。大きな構造体であっても、ソフトウェアがスタック上のホットメモリでより多くの作業を行うことを意味する場合、値で返す方が安価であることがよくあります。そして、彼らが作成の価値によって返すのが安くなかったとしても、簡単にこれを行うことができます:

int foo_create(struct Foo* foo);
...
/* In the client code: */
struct Foo foo;
if (foo_create(&foo))
{
    foo_something(&foo);
    foo_destroy(&foo);
}

... Foo余分なコピーの可能性なしにスタックから初期化する。または、クライアントFooが何らかの理由で必要に応じてヒープに割り当てる自由さえあります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.