パラメーターを介して、または戻り値によってC構造体を初期化する必要がありますか?[閉まっている]


33

私が働いている会社は、次のような初期化関数を使用して、すべてのデータ構造を初期化しています。

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
InitializeFoo(Foo* const foo){
   foo->a = x; //derived here based on other data
   foo->b = y; //derived here based on other data
   foo->c = z; //derived here based on other data
}

//initializing the structure  
Foo foo;
InitializeFoo(&foo);

私はこのような構造体を初期化しようとするいくつかのプッシュバックを得ました:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
Foo ConstructFoo(int a, int b, int c){
   Foo foo;
   foo.a = a; //part of parameter input (inputs derived outside of function)
   foo.b = b; //part of parameter input (inputs derived outside of function)
   foo.c = c; //part of parameter input (inputs derived outside of function)
   return foo;
}

//initialize (or construct) the structure
Foo foo = ConstructFoo(x,y,z);

一方に他方よりも利点はありますか?
どちらを行うべきか、そしてそれをより良い方法としてどのように正当化するのか?


4
@gnatこれは、構造体の初期化に関する明示的な質問です。このスレッドは、この特定の設計決定に適用されるのと同じ理論的根拠のいくつかを具体化したものです。
トレバーヒッキー

2
@Jefffrey私たちはCにいるので、実際にメソッドを持つことはできません。常に直接的な値のセットでもありません。構造体の初期化は、値を取得することであり(何らかの方法で)、構造体を初期化するためのロジックを実行します。
トレバーヒッキー

1
@JacquesB私は「あなたが構築するすべてのコンポーネントは他とは異なるでしょう。構造体のために他の場所で使用されるInitialize()関数があります。技術的に言えば、コンストラクタと呼ぶことは誤解を招きます。」
トレバー・ヒッキー

1
@TrevorHickey InitializeFoo()はコンストラクターです。C ++コンストラクターとの唯一の違いは、thisポインターが暗黙的にではなく明示的に渡されることです。のコンパイル済みコードInitializeFoo()と対応するC ++ Foo::Foo()はまったく同じです。
cmaster

2
より良いオプション:C ++ over Cの使用を停止します。オートウィン。
トーマスエディング

回答:


25

2番目のアプローチでは、半分初期化されたFooはありません。すべての構造を1つの場所に置くことは、より賢明で明白な場所のようです。

しかし...第1の方法はそれほど悪くはなく、多くの分野でよく使用されます(あなたの第1の方法のようなプロパティインジェクション、または第2のようなコンストラクタインジェクションのいずれかで、依存性インジェクションの最良の方法についての議論さえあります) 。どちらも間違っていません。

したがって、どちらも間違っておらず、会社の残りの部分がアプローチ#1を使用している場合は、既存のコードベースに適合する必要があり、新しいパターンを導入することでコードベースを台無しにしないでください。これは本当にここでプレイする上で最も重要な要素であり、新しい友達とうまくプレイし、違ったやり方をする特別なスノーフレークになろうとしないでください。


わかりました。どのような入力が初期化されているのかを見ることができずにオブジェクトを初期化すると、混乱につながるという印象を受けました。私は予測可能なテスト可能なコードを生成するために、データイン/データアウトの概念に従っていました。他の方法で行うと、構造体のソースファイルが初期化を実行するために余分な依存関係を必要としたため、カップリングが増加するように思われました。ただし、ある方法が別の方法よりも優先されない限り、ボートを揺り動かしたくないという点で、あなたは正しい。
トレバーヒッキー

4
@TrevorHickey:実際、私はあなたが与える例の間に2つの重要な違いがあると言います-(1)1つは関数に初期化する構造体へのポインタが渡され、もう1つは初期化された構造体を返します。(2)1つは初期化パラメーターが関数に渡され、もう1つは暗黙的です。(2)についてもっと質問しているようですが、ここでの答えは(1)に集中しています。あなたはことを明確することを望むかもしれない-私はほとんどの人が、明示的パラメータとポインタを使用して、2つのハイブリッドをお勧めします疑う:void SetupFoo(Foo *out, int a, int b, int c)
psmears

1
最初のアプローチは、どのようにして「半分初期化された」Foo構造になりますか?最初のアプローチでは、すべての初期化を1か所で実行します。(または、初期化されていないFoo構造体を「半分初期化された」と考えてますか?)
jamesdlin

1
@oomesdlinは、Fooが作成され、InitialiseFooが誤って失われた場合。長い説明を入力せずに2フェーズの初期化を説明するのは、単なる音声の図でした。私は経験豊富な開発者タイプの人々が理解するだろうと考えました。
gbjbaanb

22

どちらのアプローチも、初期化コードを単一の関数呼び出しにバンドルします。ここまでは順調ですね。

ただし、2番目のアプローチには2つの問題があります。

  1. 2番目は、結果のオブジェクトを実際に構築するのではなく、スタック上の別のオブジェクトを初期化し、その後、最終的なオブジェクトにコピーします。このため、2番目のアプローチはやや劣っていると思います。受け取ったプッシュバックは、おそらくこの余分なコピーによるものです。

    あなたがクラスを派生とき、これはさらに悪くなるDerivedからFoo(構造体は、主にC言語でオブジェクト指向のために使用されている):第二のアプローチでは、関数はConstructDerived()呼び出しますConstructFoo()、その結果、一時的なコピーFooのスーパースロットに超えるオブジェクトDerivedのオブジェクトを、Derivedオブジェクトの初期化を終了します。結果として返されるオブジェクトを再度コピーするだけです。3番目のレイヤーを追加すると、全体が完全にばかげたものになります。

  2. 2番目のアプローチでは、ConstructClass()関数は構築中のオブジェクトのアドレスにアクセスできません。これにより、オブジェクトがコールバックのために別のオブジェクトに自分自身を登録する必要がある場合に必要になるため、構築中にオブジェクトをリンクできなくなります。


最後に、すべてstructsが完全なクラスではありません。いくつかstructsの変数は、これらの変数の値に対する内部の制限なしに、単に変数の束を束ねるだけです。typedef struct Point { int x, y; } Point;これの良い例でしょう。これらのイニシャライザ関数の使用は過剰に思えます。これらの場合、複合リテラル構文が便利な場合があります(C99です)。

Point = { .x = 7, .y = 9 };

または

Point foo(...) {
    //other stuff

    return (Point){ .x = n, .y = n*n };
}


5
コンパイラがコピーを削除する可能性があるからといって、コピーを書き留めているという事実を緩和するわけではありません。Cでは、余分な操作を記述し、コンパイラに依存してそれらを修正することは、悪いスタイルと見なされます。これは、コンパイラーがネストされたテンプレートによって残されたすべての残骸を理論的に削除できることを証明できるときに誇りを持っているC ++とは異なります。Cでは、マシンが実行するコードを正確に記述しようとします。とにかく、アクセスできないアドレスに関するポイントは残っていますが、コピーの省略はあなたを助けることはできません。
cmaster

3
コンパイラを使用する人は誰でも、彼らが書くコードがコンパイラによって変換されることを期待すべきです。ハードウェアCインタプリタを実行していない限り、彼らが書くコードは、他の方法で信じることが容易であっても、実行するコードではありません。彼らがコンパイラーを理解していれば、彼らは省略を理解するでしょう、そしてそれはバイナリにint x = 3;文字列xを保存しないことと違いはありません。アドレスと継承ポイントは良好です。脱落の想定される失敗は愚かです。
-Yakk

@Yakk:歴史的に、Cはシステムプログラミング用の高レベルアセンブリ言語の形式として機能するために発明されました。それ以来、そのアイデンティティはますます曖昧になっています。一部の人々はそれを最適化されたアプリケーション言語にしたいが、高レベルのアセンブリ言語のより良い形が出現していないので、Cはその後者の役割を果たすためにまだ必要です。最小限の最適化でコンパイルされた場合でも、適切に記述されたプログラムコードは少なくともきちんと動作するという考えには何の問題もありません。
supercat

@Yakk:たとえば、「次の変数はコードの次のストレッチ中にレジスタに安全に保持できる」コンパイラーに指示するディレクティブを持つほかunsigned char、厳密なエイリアシング規則では不十分であり、同時にプログラマーの期待を明確にします。
supercat

1

構造の内容と使用されている特定のコンパイラに応じて、どちらのアプローチも高速になる可能性があります。典型的なパターンは、特定の条件を満たす構造体がレジスターで返されることです。他の構造タイプを返す関数の場合、呼び出し元は一時構造にどこか(通常はスタック上)にスペースを割り当て、そのアドレスを「非表示」パラメーターとして渡す必要があります。関数の戻り値が、外部コードによってアドレスが保持されていないローカル変数に直接格納されている場合、一部のコンパイラはその変数のアドレスを直接渡すことができる場合があります。

構造体のタイプが特定の実装の要件を満たし、レジスタに返される(たとえば、1マシンワード以下、または正確に2マシンワードを満たす)関数は、構造体のアドレスを渡すよりも高速です。変数のアドレスをそのコピーを保持する可能性のある外部コードに公開すると、いくつかの有用な最適化が妨げられる可能性があるためです。型がそのような要件を満たさない場合、構造体を返す関数の生成コードは、宛先ポインターを受け入れる関数のコードと同様になります。呼び出しコードは、ポインターを受け取るフォームの方が高速になる可能性がありますが、そのフォームは最適化の機会をいくつか失います。

Cは、関数が渡されたポインター(C ++参照に似たセマンティクス)のコピーを保持することを禁止されていると言う手段を提供しません。既存のオブジェクトへのポインタですが、同時にコンパイラが変数のアドレスを「公開」することを考慮する必要があるというセマンティックコストを回避します。


3
最後に:C ++には、関数が参照として渡されたポインターのコピーを保持しないようにするものは何もありません。関数はオブジェクトのアドレスを取得するだけです。参照を使用して、その参照を含む別のオブジェクトを作成することもできます(ネイキッドポインターは作成されません)。それにもかかわらず、ポインターコピーまたはオブジェクト内の参照のいずれかが、それらが指すオブジェクトより長く存続し、ぶら下がりポインター/参照を作成する場合があります。そのため、参照の安全性に関するポイントはかなり静かです。
cmaster

@cmaster:一時ストレージにポインターを渡すことで構造を返すプラットフォームでは、Cコンパイラは、呼び出された関数にそのストレージのアドレスにアクセスする方法を提供しません。C ++では、参照によって渡される変数のアドレスを取得できますが、呼び出し元が渡されたアイテムの有効期間を保証しない限り(この場合、通常はポインターを渡したはずです)、未定義の動作が発生する可能性があります。
supercat

1

「出力パラメータ」スタイルを支持する1つの引数は、関数がエラーコードを返すことができるということです。

struct MyStruct {
    int x;
    char *y;
    // ...
};

int MyStruct_init(struct MyStruct *out) {
    // ...
    char *c = malloc(n);
    if (!c) {
        return -1;
    }
    out->y = c;
    return 0;  // Success!
}

関連する構造体のセットを検討すると、初期化がそれらのいずれかで失敗する可能性がある場合は、一貫性を保つためにすべてのパラメーターでoutパラメータースタイルを使用する価値があります。


1
ただ設定することもできますがerrno
デデュプリケーター

0

あなたの焦点は、出力引数による初期化と戻り値による初期化にあり、構築引数の供給方法の不一致ではないと推測しています。

最初のアプローチはFoo不透明にすることができることに注意してください(現在の使用方法ではありませんが)、それは長期的な保守性のために通常望ましいことです。たとえば、不透明なFoo構造体を初期化せずに割り当てる関数を検討できます。または、Foo以前に異なる値で初期化された構造体を再初期化する必要があるかもしれません。


Downvoter、説明する気?私が言った事実は間違っていますか?
ジェームズドリン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.