web-dev-qa-db-ja.com

集約の初期化に対するC ++ 17拡張により、ブレースの初期化が危険になりましたか?

ブレースの初期化が望ましい 他の初期化形式よりも一般的なコンセンサスがあるようですが、C++ 17 集約の初期化への拡張 があるため、意図しない変換のリスクになります。次のコードを検討してください。

struct B { int i; };
struct D : B { char j; };
struct E : B { float k; };

void f( const D& d )
{
  E e1 = d;   // error C2440: 'initializing': cannot convert from 'D' to 'E'
  E e2( d );  // error C2440: 'initializing': cannot convert from 'D' to 'E'
  E e3{ d };  // OK in C++17 ???
}

struct F
{
  F( D d ) : e{ d } {}  // OK in C++17 ???
  E e;
};

上記のコードでstruct Dおよびstruct Eは、まったく関係のない2つのタイプを表します。したがって、C++ 17の時点で、ブレース(集約)初期化を使用すると、警告なしで1つの型から別の型に「変換」できることは驚きです。

これらのタイプの偶発的な変換を避けるために何をお勧めしますか?それとも何か不足していますか?

PS:上記のコードはClang、GCC、最新のVC++でテストされました-それらはすべて同じです。

更新:ニコルからの回答に応えて。より実用的な例を考えてみましょう:

struct point { int x; int y; };
struct circle : point { int r; };
struct rectangle : point { int sx; int sy; };

void move( point& p );

void f( circle c )
{
  move( c ); // OK, makes sense
  rectangle r1( c );  // Error, as it should be
  rectangle r2{ c };  // OK ???
}

circlepointをベースクラスとして持っているため、circlepointとして表示できることは理解できますが、長方形に円、それは私にとって問題です。

更新2:クラス名の私の悪い選択が一部のために問題を曇らせているようだから。

struct shape { int x; int y; };
struct circle : shape { int r; };
struct rectangle : shape { int sx; int sy; };

void move( shape& p );

void f( circle c )
{
  move( c ); // OK, makes sense
  rectangle r1( c );  // Error, as it should be
  rectangle r2{ c };  // OK ???
}
48
Barnett

struct Dとstruct Eは、まったく関係のない2つのタイプを表します。

しかし、それらは「完全に無関係な」タイプではありません。どちらも同じ基本クラス型を持っています。これは、すべてのDが暗黙的にBに変換可能であることを意味します。したがって、すべてのDはaBです。したがって、_E e{d};_を実行することは、呼び出される操作の点で_E e{b};_と違いはありません。

基本クラスへの暗黙的な変換をオフにすることはできません。

これが本当に面倒な場合、唯一の解決策は、値をメンバーに転送する適切なコンストラクターを提供することにより、集計の初期化を防ぐことです。

これが集計の初期化をより危険にするかどうかについては、そうは思いません。これらの構造体を使用して、上記の状況を再現できます。

_struct B { int i; };
struct D { B b; char j; operator B() {return b;} };
struct E { B b; float k; };
_

そのため、この性質の何かは常に可能性でした。暗黙の基本クラス変換を使用すると、それほど「悪い」とは思わない。

より深い質問は、ユーザーが最初にEDを初期化しようとした理由です。

あなたが静かに円から長方形に変換できるという考えは、私にとっては問題です。

これを行うと、同じ問題が発生します。

_struct rectangle
{
  rectangle(point p);

  int sx; int sy;
  point p;
};
_

_rectangle r{c};_だけでなく、rectangle r(c)も実行できます。

問題は、継承を誤って使用していることです。 circlerectangle、およびpointの関係について言っているのですが、それは意味ではありません。したがって、コンパイラーは、意図していないことを実行できるようにします。

継承の代わりに包含を使用した場合、これは問題になりません。

_struct point { int x; int y; };
struct circle { point center; int r; };
struct rectangle { point top_left; int sx; int sy; };

void move( point& p );

void f( circle c )
{
  move( c ); // Error, as it should, since a circle is not a point.
  rectangle r1( c );  // Error, as it should be
  rectangle r2{ c };  // Error, as it should be.
}
_

circlealways a pointであるか、またはnever a pointです。あなたは時々pointにしようとしていますが、他のものではありません。それは論理的に一貫性がありません。論理的に一貫性のない型を作成すると、論理的に一貫性のないコードを作成できます。


あなたが静かに円から長方形に変換できるという考えは、私にとっては問題です。

これは重要なポイントをもたらします。厳密に言えば、変換は次のようになります。

_circle cr = ...
rectangle rect = cr;
_

それは不整形です。 _rectangle rect = {cr};_を実行すると、not変換を実行します。リストの初期化を明示的に呼び出しています。これは、通常、集約に対して集約の初期化を引き起こします。

現在、リストの初期化は確かに変換を実行できます。しかし、単に_D d = {e};_の場合、これはeからDへの変換を実行していることを期待するべきではありません。タイプDのオブジェクトをeでリスト初期化しています。 EDに変換可能な場合、変換を実行できますが、非変換リスト初期化フォームでも機能する場合、この初期化は有効です。

したがって、この機能によりcirclerectangleに変換可能になると言うのは誤りです。

37
Nicol Bolas

これはC++ 17の新機能ではありません。集約の初期化では、常にメンバーを空にすることができました(空の初期化リスト、 C++ 11 から初期化されます)。

struct X {
    int i, j;
};

X x{42}; // ok in C++11

含めることができるものが増えたため、やめることができるものが増えたのは今です。


gccとclangは、少なくとも何かが欠落していることを示す-Wmissing-field-initializers-Wextraの一部)によって警告を提供します。これが大きな懸念事項である場合は、その警告を有効にしてコンパイルします(場合によっては、エラーにアップグレードします)。

<source>: In function 'void f(const D&)':
<source>:9:11: warning: missing initializer for member 'E::k' [-Wmissing-field-initializers]
   E e3{ d };  // OK in C++17 ???
           ^
<source>: In constructor 'F::F(D)':
<source>:14:19: warning: missing initializer for member 'E::k' [-Wmissing-field-initializers]
   F( D d ) : e{ d } {}  // OK in C++17 ???
                   ^

より直接的な方法は、これらの型にコンストラクターを追加して、それらが集約されなくなるようにすることです。結局、集計の初期化を使用することはありませんhave

29
Barry