web-dev-qa-db-ja.com

デフォルトのコンストラクタを削除しました。オブジェクトはまだ作成できます...時々

素朴で楽観的、そして..C++ 11統一初期化構文の見方が間違っている

C++ 11のユーザー定義型オブジェクトは、古い{...}構文ではなく、新しい(...)構文で構築する必要があると考えました(std::initializer_listおよび同様のパラメーターでオーバーロードされたコンストラクターを除きます(例:std::vector:サイズctor vs 1 elem init_list ctor))。

利点は、狭い暗黙の変換がないこと、最も厄介な解析に問題がないこと、consistency(?)です。それらが同じであると思ったので、私は問題を見つけませんでした(与えられた例を除いて)。

しかし、そうではありません。

純粋な狂気の物語

{}はデフォルトのコンストラクターを呼び出します。

...次の場合を除く:

  • デフォルトのコンストラクタが削除され、
  • 定義されている他のコンストラクタはありません。

それでは、値がオブジェクトを初期化するように見えますか?...オブジェクトがデフォルトのコンストラクタを削除した場合でも、{}はオブジェクトを作成できます。これは、削除されたコンストラクタの目的全体に勝るものではありませんか?

...次の場合を除く:

  • オブジェクトにはデフォルトのコンストラクタが削除されており、
  • 定義された他のコンストラクター。

その後、call to deleted constructorで失敗します。

...次の場合を除く:

  • オブジェクトには削除されたコンストラクタがあり、
  • 定義されている他のコンストラクターおよび
  • 少なくとも非静的データメンバー。

その後、フィールド初期化子が欠落して失敗します。

ただし、{value}を使用してオブジェクトを作成できます。

OK、これは最初の例外と同じかもしれません(値はオブジェクトを初期化します)

...次の場合を除く:

  • クラスには削除されたコンストラクタがあります
  • クラス内の少なくとも1つのデータメンバーがデフォルトで初期化されます。

そうすると、{}{value}もオブジェクトを作成できません。

私はいくつかを逃したと確信しています。皮肉なことに、それはuniform初期化構文と呼ばれます。もう一度言います:[〜#〜] uniform [〜#〜]初期化構文。

この狂気とは何ですか?

シナリオA

削除されたデフォルトコンストラクター:

struct foo {
  foo() = delete;
};

// All bellow OK (no errors, no warnings)
foo f = foo{};
foo f = {};
foo f{}; // will use only this from now on.

シナリオB

デフォルトのコンストラクタを削除し、他のコンストラクタを削除しました

struct foo {
  foo() = delete;
  foo(int) = delete;
};

foo f{}; // OK

シナリオC

デフォルトのコンストラクタを削除し、他のコンストラクタを定義しました

struct foo {
  foo() = delete;
  foo(int) {};
};

foo f{}; // error call to deleted constructor

シナリオD

削除されたデフォルトコンストラクター、他のコンストラクターは定義されていない、データメンバー

struct foo {
  int a;
  foo() = delete;
};

foo f{}; // error use of deleted function foo::foo()
foo f{3}; // OK

シナリオE

デフォルトコンストラクターの削除、Tコンストラクターの削除、Tデータメンバー

struct foo {
  int a;
  foo() = delete;
  foo(int) = delete;
};

foo f{}; // ERROR: missing initializer
foo f{3}; // OK

シナリオF

削除されたデフォルトコンストラクター、クラス内データメンバー初期化子

struct foo {
  int a = 3;
  foo() = delete;
};

/* Fa */ foo f{}; // ERROR: use of deleted function `foo::foo()`
/* Fb */ foo f{3}; // ERROR: no matching function to call `foo::foo(init list)`
48
bolov

このように物事を見るとき、オブジェクトが初期化される方法に完全で完全なカオスがあると言うのは簡単です。

大きな違いは、foo:の型(集約型かどうか)です。

次の場合は集約です。

  • ユーザー提供のコンストラクターなし(削除またはデフォルト設定された関数はユーザー提供としてカウントされません)、
  • プライベートまたは保護された非静的データメンバーはありません。
  • 非静的データメンバ用のブレースまたはイコール初期化子はありません(c ++ 11から(元に戻す)c ++ 14まで)
  • 基本クラスなし、
  • 仮想メンバー関数はありません。

そう:

  • シナリオA B D E:fooは集約
  • シナリオC:fooは集約ではありません
  • シナリオF:
    • c ++ 11では、集約ではありません。
    • c ++ 14では集約です。
    • g ++はこれを実装しておらず、C++ 14でも非集計として扱います。
      • 4.9はこれを実装しません。
      • 5.2.0
      • 5.2.1 ubuntuはしません(おそらく回帰)

タイプTのオブジェクトのリスト初期化の効果は次のとおりです。

  • ...
  • Tが集約タイプの場合、集約の初期化が実行されます。これにより、シナリオA B D E(およびC++ 14のF)が処理されます。
  • それ以外の場合、Tのコンストラクターは2つのフェーズで考慮されます。
    • Std :: initializer_listを取るすべてのコンストラクター...
    • それ以外の場合[...] Tのすべてのコンストラクターがオーバーロード解決に参加します[...]これによりC(およびC++ 11のF)が処理されます
  • ...

タイプTのオブジェクトの集約初期化(シナリオA B D E(F c ++ 14)):

  • 各非静的クラスメンバは、クラス定義に順番に表示され、初期化子リストの対応する句からコピー初期化されます。 (配列参照は省略)

TL; DR

これらのルールはすべて非常に複雑で頭痛の種になるようです。私は個人的にこれを単純化しすぎています(それによって足で自分自身を撃った場合はそうです:数十日間の頭痛を抱えるのではなく、病院で2日間過ごすと思います):

  • 集計の場合、各データメンバーはリスト初期化子の要素から初期化されます
  • 他のコンストラクタを呼び出す

これは、削除されたコンストラクタの目的全体に勝るものではありませんか?

まあ、それについては知りませんが、解決策はfooを集約ではないようにすることです。オーバーヘッドを追加せず、オブジェクトの使用されている構文を変更しない最も一般的な形式は、空の構造体から継承することです。

struct dummy_t {};

struct foo : dummy_t {
  foo() = delete;
};

foo f{}; // ERROR call to deleted constructor

状況によっては(非静的メンバーがまったくないのではないか)、デストラクタを削除することもできます(これにより、オブジェクトはどのコンテキストでもインスタンス化できなくなります)。

struct foo {
  ~foo() = delete;
};

foo f{}; // ERROR use of deleted function `foo::~foo()`

この回答では、以下から収集した情報を使用します。

@ M.M に感謝します。この投稿の修正と改善にご協力いただきました。

33
bolov

あなたを混乱させているのは、集計の初期化です。

あなたが言うように、リストの初期化を使用することには利点と欠点があります。 (「統一初期化」という用語は、C++標準では使用されません)。

欠点の1つは、集計のリストの初期化の動作が非集計と異なることです。また、aggregateの定義は標準ごとに若干異なります。


集計はコンストラクターを介して作成されません。 (技術的には実際にはそうかもしれませんが、これはそれを考える良い方法です)。代わりに、集計を作成するときにメモリが割り当てられ、リスト初期化子の内容に従って順番に各メンバーが初期化されます。

非集約はコンストラクターを介して作成され、その場合、リスト初期化子のメンバーはコンストラクター引数です。

上記には実際に設計上の欠陥があります:T t1; T t2{t1};がある場合、その目的はコピー構築を実行することです。ただし、(C++ 14より前)Tが集約の場合、代わりに集約の初期化が行われ、t2の最初のメンバーはt1で初期化されます。

この欠陥は、C++ 14を修正した defect report で修正されたため、今後、集計の初期化に進む前にコピー構成がチェックされます。


C++ 14のaggregateの定義は次のとおりです。

集合体は、ユーザー提供のコンストラクター(12.1)、プライベートまたは保護された非静的データメンバー(11節)、基本クラス(10節)、および仮想関数(10.3)を持たない配列またはクラス(9節)です。 )。

C++ 11では、非静的メンバーのデフォルト値は、クラスが集約ではないことを意味していました。ただし、C++ 14では変更されました。 ユーザー指定はユーザー宣言を意味しますが、= defaultまたは= deleteではありません。


コンストラクター呼び出しneverが誤って集約の初期化を実行するようにしたい場合は、( )ではなく{ }を使用し、他の方法でMVPを避ける必要があります。

7
M.M

集約の初期化に関するこれらのケースは、ほとんどの場合直観に反するものであり、提案の主題でした p1008:ユーザー宣言コンストラクターによる集約の禁止

現在、C++では、ユーザーが宣言したコンストラクターを持つ一部の型を、それらのコンストラクターをバイパスして、集約の初期化を介して初期化できます。その結果、驚くほど複雑でバグの多いコードが作成されます。このペーパーでは、C++の初期化セマンティクスをより安全で、より統一し、簡単に教えることができる修正を提案します。また、この修正により導入される重大な変更についても説明します

また、いくつかの例を紹介します。これらの例は、提示したケースとうまく重なります。

struct X {
    X() = delete;
  };

 int main() {
    X x1;   // ill-formed - default c’tor is deleted
    X x2{}; // compiles!
}

明らかに、削除されたコンストラクターの目的は、ユーザーがクラスを初期化するのを防ぐことです。ただし、直感に反して、これは機能しません。ユーザーは、コンストラクターを完全にバイパスするため、集約の初期化を介してXを初期化できます。作成者は、すべてのデフォルト、コピー、および移動コンストラクタを明示的に削除することもできますが、それでもクライアントコードが上記の集約初期化を介してXをインスタンス化するのを防ぐことができません。ほとんどのC++開発者は、このコードを表示したときの現在の動作に驚いています。クラスXの作成者は、代わりにデフォルトコンストラクタをプライベートにすることを検討できます。しかし、このコンストラクターにデフォルトの定義が与えられている場合、これは再びクラスの集約の初期化(したがってインスタンス化)を妨げません。

struct X {
  private:
    X() = default;
  };

int main() {
    X x1;     // ill-formed - default c’tor is private
    X x2{};  // compiles!
  }

現在のルールのため、集約の初期化により、実際にはデフォルトで構築できないクラスであっても、クラスを「デフォルトで構築する」ことができます。

 static_assert(!std::is_default_constructible_v<X>);

上記のXの両方の定義に合格します。

...

提案された変更は次のとおりです。

[dcl.init.aggr]パラグラフ1を次のように変更します。

集合体は、配列またはクラス(12節)であり、

  • ユーザー提供の明示的ななしu̲s̲e̲r̲-̲d̲e̲c̲l̲a̲r̲e̲d̲または継承されたコンストラクター(15.1)、

  • プライベートまたは保護された非静的データメンバーはありません(第14項)。

  • 仮想機能なし(13.3)、および

  • 仮想、プライベート、または保護された基本クラスはありません(13.1)。

[dcl.init.aggr]パラグラフ17を次のように変更します。

[注:集合配列または集合クラスには、クラス>> typeの要素をで含めることができますユーザー提供u̲s̲e̲r̲-̲d̲e̲c̲l̲a̲r̲e̲d̲コンストラクター(15.1)。 >>これらの集約オブジェクトの初期化については、15.6.1で説明しています。 —注を終了]

次をAnnex Cの[diff.cpp17]のセクションC.5 C++およびISO C++ 2017に追加します。

C.5.6条項11:宣言子[diff.cpp17.dcl.decl]

影響を受ける副次句:[dcl.init.aggr]
Change:ユーザーが宣言したコンストラクターを持つクラスは、決して集約ではありません。
Rationale:クラスの宣言されたコンストラクターに関係なく適用される可能性のある、エラーが発生する可能性のある集計の初期化を削除します。
元の機能への影響:ユーザーが宣言したコンストラクターで型を集計初期化する有効なC++ 2017コードは、不正な形式であるか、この国際標準では異なるセマンティクスを持っています。

省略した例が続きます。

提案は 受け入れられ、C++ 20にマージされました これらの変更を含む 最新のドラフト を見つけることができ、 [dcl。 init.aggr] p1.1 および [dcl.init.aggr] p17 および C++ 17宣言diff

したがって、これはC++ 20以降で修正する必要があります。

4
Shafik Yaghmour