集計初期化メンバーの脱落を防ぐことはできますか?


43

このような同じタイプの多くのメンバーを持つ構造体があります

struct VariablePointers {
   VariablePtr active;
   VariablePtr wasactive;
   VariablePtr filename;
};

問題は、次のように構造体メンバーの1つ(例:)を初期化し忘れた場合ですwasactive

VariablePointers{activePtr, filename}

コンパイラはそれについて文句を言うことはありませんが、部分的に初期化されたオブジェクトが1つあります。この種のエラーを防ぐにはどうすればよいですか?コンストラクタを追加することはできますが、変数のリストを2回複製するため、これをすべて3回入力する必要があります。

C ++ 11の解決策がある場合は、C ++ 11の回答も追加してください(現在、そのバージョンに制限されています)。ただし、最近の言語標準も歓迎します。


6
コンストラクタを入力してもそれほどひどく聞こえません。メンバーが多すぎる場合を除いて、その場合、おそらくリファクタリングが適切です。
Gonen I

1
@Someprogrammerdude彼は誤って初期化値を省略できるというエラーだと思います
Gonen I

2
@theWiseBro配列/ベクトルがどのように役立つかを知っている場合は、回答を投稿する必要があります。それはそれほど明白ではありません、私はそれを見ません
idclev 463035818

2
@Someprogrammerdudeしかし、それは警告でもありますか?VS2019では表示されません。
acraig5075

8
-Wmissing-field-initializersコンパイルフラグがあります。
ロン

回答:


42

必要な初期化子がない場合にリンカーエラーをトリガーするトリックを次に示します。

struct init_required_t {
    template <class T>
    operator T() const; // Left undefined
} static const init_required;

使用法:

struct Foo {
    int bar = init_required;
};

int main() {
    Foo f;
}

結果:

/tmp/ccxwN7Pn.o: In function `Foo::Foo()':
prog.cc:(.text._ZN3FooC2Ev[_ZN3FooC5Ev]+0x12): undefined reference to `init_required_t::operator int<int>() const'
collect2: error: ld returned 1 exit status

警告:

  • C ++ 14より前は、これFooは集合体になることをまったく妨げていました。
  • これは技術的には未定義の動作(ODR違反)に依存していますが、どの正常なプラットフォームでも機能するはずです。

変換演算子を削除すると、コンパイラエラーになります。
jrok

@jrokはい、ただしFoo、実際に演算子を呼び出さなくても、宣言されたらすぐに1になります。
クエンティン

2
@jrokただし、初期化が提供されていてもコンパイルされません。godbolt.org/z/yHZNq_ 補遺: MSVCの場合、説明どおりに機能します:godbolt.org/z/uQSvDaこれはバグですか?
n314159

もちろん、ばかげた私。
jrok

6
残念ながら、このトリックはC ++ 11では機能しません。非集計になるためです:(C ++ 11タグを削除したので、あなたの答えも実行可能です(削除しないでください)。可能であれば、C ++ 11ソリューションが引き続き推奨されます
Johannes Schaub-litb

22

clangとgccの場合-Werror=missing-field-initializers、フィールド初期化子が見つからない場合の警告をエラーに変換するようにコンパイルできます。ゴッドボルト

編集: MSVCの場合、levelでも警告が発行され/Wallないようです。このコンパイラーで不足している初期化子について警告することはできないと思います。ゴッドボルト


7

エレガントで便利なソリューションではないと思いますが、C ++ 11でも機能し、コンパイル時(リンク時ではなく)エラーが発生するはずです。

考え方は、デフォルトの初期化なしの型の最後の位置に追加のメンバーを構造体に追加することです(そして、型の値VariablePtr(または先行する値の型は何でも)で初期化することはできません)。

例として

struct bar
 {
   bar () = delete;

   template <typename T> 
   bar (T const &) = delete;

   bar (int) 
    { }
 };

struct foo
 {
   char a;
   char b;
   char c;

   bar sentinel;
 };

この方法では、最後の値(sentinel例ではの整数)を明示的に初期化する値を含めて、集約初期化リストにすべての要素を追加するか、「削除された 'bar'のコンストラクターの呼び出し」エラーが発生します。

そう

foo f1 {'a', 'b', 'c', 1};

コンパイルして

foo f2 {'a', 'b'};  // ERROR

しません。

残念ながらまた

foo f3 {'a', 'b', 'c'};  // ERROR

コンパイルしません。

-編集-

MSalters(ありがとう)で指摘されているように、私の元の例には欠陥(別の欠陥)barがあります。char値は(に変換可能なint)値で初期化できるため、次の初期化で機能します

foo f4 {'a', 'b', 'c', 'd'};

これは非常に混乱する可能性があります。

この問題を回避するために、次の削除されたテンプレートコンストラクターを追加しました

 template <typename T> 
 bar (T const &) = delete;

したがって、削除されたテンプレートコンストラクターによって値がインターセプトされるため、前述のf4宣言ではコンパイルエラーが発生します。d


よろしくお願いします。あなたが言ったようにそれは完璧ではfoo f;なく、コンパイルも失敗させますが、おそらくそれはこのトリックの欠点よりも機能のほうが多いのです。これ以上の提案がなければ受け入れます。
ヨハネスシャウブ-litb

1
読みやすくするために、barコンストラクターにinit_list_endのような名前のconstネストクラスメンバーを受け入れさせる
I

@GonenI-読みやすくするために、を受け入れ、その値に(単に)enum名前を付けることができます。しかし、可読性は多くのタイプライティングを追加するので、追加の値がこの回答の弱点であることを考えると、それが良いアイデアかどうかはわかりません。init_list_endlist_endenum
max66

たぶんconstexpr static int eol = 0;のヘッダに何かを追加してくださいbartest{a, b, c, eol}私にはかなり読みやすいようです。
n314159

@ n314159-まあ...になるbar::eol; enum値を渡すのとほとんど同じです。しかし、私はそれが重要だとは思いません。答えの核心は、「構造体に、最後の位置に、デフォルトの初期化のないタイプの追加メンバーを追加すること」です。このbar部分は、ソリューションが機能することを示す簡単な例にすぎません。正確な「デフォルトの初期化なしのタイプ」は、状況によって異なります(IMHO)。
max66

4

CppCoreCheckもちろん、プログラム全体の通常-すべてのメンバーが初期化されている、それはエラーに警告から回すことができるかどうか、ということを正確にチェックするためのルールがあります。

更新:

チェックするルールはタイプセーフの一部ですType.6

Type.6:常にメンバー変数を初期化します。デフォルトのコンストラクターまたはデフォルトのメンバー初期化子を使用して、常に初期化します。


2

最も簡単な方法は、メンバーの型に引数のないコンストラクターを与えないことです。

struct B
{
    B(int x) {}
};
struct A
{
    B a;
    B b;
    B c;
};

int main() {

        // A a1{ 1, 2 }; // will not compile 
        A a1{ 1, 2, 3 }; // will compile 

別のオプション:メンバーがconst&の場合、すべてのメンバーを初期化する必要があります。

struct A {    const int& x;    const int& y;    const int& z; };

int main() {

//A a1{ 1,2 };  // will not compile 
A a2{ 1,2, 3 }; // compiles OK

ダミーのconst&メンバーを1つ持つことができる場合は、@ max66のセンチネルのアイデアと組み合わせることができます。

struct end_of_init_list {};

struct A {
    int x;
    int y;
    int z;
    const end_of_init_list& dummy;
};

    int main() {

    //A a1{ 1,2 };  // will not compile
    //A a2{ 1,2, 3 }; // will not compile
    A a3{ 1,2, 3,end_of_init_list() }; // will compile

cppreferenceからhttps://en.cppreference.com/w/cpp/language/aggregate_initialization

初期化子句の数がメンバーの数より少ないか、初期化子リストが完全に空の場合、残りのメンバーは値で初期化されます。参照型のメンバーがこれらの残りのメンバーの1つである場合、プログラムの形式は正しくありません。

別のオプションは、max66のセンチネルアイデアを取り、読みやすくするために構文糖を追加することです

struct init_list_guard
{
    struct ender {

    } static const end;
    init_list_guard() = delete;

    init_list_guard(ender e){ }
};

struct A
{
    char a;
    char b;
    char c;

    init_list_guard guard;
};

int main() {
   // A a1{ 1, 2 }; // will not compile 
   // A a2{ 1, init_list_guard::end }; // will not compile 
   A a3{ 1,2,3,init_list_guard::end }; // compiles OK

残念ながら、これによりA移動Aできなくなり、コピーのセマンティクスが変更されます(いわば、値の集約ではなくなりました):(
Johannes Schaub-litb

@ JohannesSchaub-litb OK。私の編集した回答でこのアイデアはどうですか?
I

@ JohannesSchaub-litb:同様に重要なことですが、最初のバージョンでは、メンバーにポインターを作成することで、間接参照のレベルを追加しています。さらに重要なことに、オブジェクトは何かの参照ある必要があり、1,2,3オブジェクトは事実上、自動ストレージ内のローカルであり、関数が終了するとスコープ外になります。また、64ビットポインター(x86-64など)を備えたシステムでは、sizeof(A)が3ではなく24になります。
Peter Cordes

ダミー参照により、サイズが3バイトから16バイトに増加します(ポインター(参照)メンバー+ポインター自体の配置のパディング)。参照を使用しない限り、なくなったオブジェクトを指す場合はおそらく問題ありません。範囲。私は間違いなくそれが最適化されないことを心配します、そしてそれをコピーすることは確かにそうしません。(空のクラスはそのサイズ以外に最適化される可能性が高いため、ここでの3番目のオプションは最も悪いものですが、少なくとも一部のABIではすべてのオブジェクトのスペースが依然として必要です。パディングが損傷することについても心配します場合によっては最適化されます。)
Peter Cordes
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.