web-dev-qa-db-ja.com

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

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

//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);

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

34
Trevor Hickey

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

しかし... 1番目の方法はそれほど悪くはなく、多くの領域でよく使用されます(1番目の方法のようなプロパティ注入、または2番目の方法のようなコンストラクター注入のいずれかである依存関係注入の最良の方法についての議論もあります)。 。どちらも間違っていません。

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

25
gbjbaanb

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

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

  1. 2番目のオブジェクトは実際には結果のオブジェクトを構築せず、スタック上の別のオブジェクトを初期化し、それが最後のオブジェクトにコピーされます。これが、2番目のアプローチをやや劣ると見なす理由です。受け取ったプッシュバックは、おそらくこの無関係なコピーが原因です。

    DerivedからクラスFooを派生させると、これはさらに悪化します(構造体は主にCのオブジェクト指向に使用されます)。2番目のアプローチでは、関数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 };
}

私は、構築引数の提供方法の不一致ではなく、出力パラメーターによる初期化とリターンによる初期化に重点を置いていると思います。

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

1
jamesdlin

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

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

Cは、そのような制限されたポインタを渡すことは、渡すことの直接的なパフォーマンス上の利点を得るので、Cは、関数が渡されたポインタ(C++参照に類似したセマンティクス)のコピーを保持することが禁止されていると言う手段を提供しません既存のオブジェクトへのポインタですが、同時に、変数のアドレスを「公開」と見なすようコンパイラに要求するという意味上のコストを回避します。

1
supercat

「出力パラメータ」スタイルを支持する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-parameterスタイルを使用することは価値があります。

1
Viktor Dahl