関数のreturn
ステートメントで構造全体を返すのではなく、構造へのポインターを返す利点は何ですか?
fopen
のような関数やその他の低レベル関数について話していますが、おそらく構造体へのポインタを返す高レベル関数もあるでしょう。
これは単なるプログラミングの問題というよりはむしろ設計上の選択であると私は信じており、2つの方法の長所と短所についてもっと知りたいと思っています。
構造体へのポインターを返す利点であると私が考えた理由の1つは、NULL
ポインターを返すことによって関数が失敗した場合に、より簡単に通知できるようにすることです。
NULL
であるfull構造体を返すことは、私が推測するのが難しくなるか、効率が低下します。これは正当な理由ですか?
fopen
のような関数がstruct
型のインスタンスの代わりにポインタを返す理由はいくつかあります:
struct
タイプの表現をユーザーから隠したいとします。FILE *
のようなタイプの場合は、タイプの表現の詳細をユーザーに公開したくないためです。FILE *
オブジェクトは不透明なハンドルとして機能し、そのハンドルをさまざまなI/Oルーチン(およびFILE
はしばしばstruct
タイプとして実装されますが、haveにはなりません)。
したがって、ヘッダーのincompletestruct
タイプをどこかに公開できます:
typedef struct __some_internal_stream_implementation FILE;
不完全な型のインスタンスを宣言することはできませんが、それへのポインタを宣言することはできます。したがって、FILE *
を作成し、fopen
、freopen
などを介してそれに割り当てることができますが、それが指すオブジェクトを直接操作することはできません。
また、fopen
関数がFILE
などを使用してmalloc
オブジェクトを動的に割り当てている可能性もあります。その場合、ポインターを返すことは理にかなっています。
最後に、ある種の状態をstruct
オブジェクトに格納している可能性があり、その状態をいくつかの異なる場所で利用できるようにする必要があります。 struct
タイプのインスタンスを返した場合、それらのインスタンスは互いにメモリ内の個別のオブジェクトになり、最終的には同期しなくなります。単一のオブジェクトへのポインタを返すことにより、誰もが同じオブジェクトを参照しています。
「構造体を返す」には2つの方法があります。データのコピーを返すことも、データへの参照(ポインタ)を返すこともできます。いくつかの理由から、ポインタを返す(および一般に渡す)ことが一般的に推奨されます。
まず、構造体をコピーすると、ポインタをコピーするよりも多くのCPU時間がかかります。これがコードで頻繁に行われる処理である場合、パフォーマンスに顕著な違いが生じる可能性があります。
次に、ポインタを何度コピーしても、メモリ内の同じ構造を指し続けます。これに対するすべての変更は、同じ構造に反映されます。ただし、構造自体をコピーしてから変更を加えると、変更はそのコピー上のみを表示します。別のコピーを保持するコードには変更が反映されません。まれに、これで十分な場合もありますが、ほとんどの場合そうではありません。間違った場合にバグが発生する可能性があります。
他の回答に加えて、値によってsmallstruct
を返すことも価値があります。たとえば、1つのデータとそれに関連するエラー(または成功)コードのペアを返すことができます。
例として、fopen
は1つのデータ(開かれたFILE*
)のみを返し、エラーの場合は errno
疑似グローバル変数を通じてエラーコードを示します。ただし、FILE*
ハンドルとエラーコード(ファイルハンドルがstruct
の場合に設定される)の2つのメンバーのNULL
を返す方が良いでしょう。歴史的な理由でそうではありません(そして、エラーはerrno
グローバルを通じて報告されますが、これは現在マクロです)。
Go言語 には、2つ(またはいくつか)の値を返すためのニース表記があることに注意してください。
Linux/x86-64では [〜#〜] abi [〜#〜] と呼び出し規約( x86-psABI ページを参照)がstruct
を指定していることにも注意してください。 2つのスカラーメンバー(たとえば、ポインターと整数、または2つのポインター、または2つの整数)が2つのレジスターを介して返されます(これは非常に効率的で、メモリーを介して行われません)。
そのため、新しいCコードでは、小さなC struct
を返す方が読みやすく、スレッドフレンドリーで効率的です。
FILE*
のようなものは、クライアントコードに関する限り、実際には構造体へのポインタではなく、ファイルのようないくつかのotherエンティティに関連付けられた不透明な識別子の形式です。プログラムがfopen
を呼び出す場合、通常、返される構造体の内容は関係ありません。fread
のような他の関数が必要なことをすべて実行することだけが関係します。それでやりなさい。
標準ライブラリがFILE*
内に保持されている場合、たとえば、そのファイル内の現在の読み取り位置。fread
への呼び出しは、その情報を更新できる必要があります。 fread
にFILE
へのポインタを受け取ると、それが簡単になります。代わりにfread
がFILE
を受け取った場合、呼び出し元が保持しているFILE
オブジェクトを更新する方法はありません。
あなたは正しい軌道に乗っています
あなたが言及した両方の理由は有効です:
構造体へのポインターを返す利点であると私が考えた理由の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;
}
情報の非表示
関数の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
をヒープに割り当てることもできます。