集成初始化的C ++ 17扩展是否使得大括号初始化变得危险?

时间:2018-05-09 13:20:28

标签: c++ initialization c++17 list-initialization

关于其他形式的初始化brace initialization should be preferred似乎已达成普遍共识,但自从引入C ++ 17 extension to aggregate initialization以来,似乎存在意外转换的风险。请考虑以下代码:

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 Dstruct E代表两种完全不相关的类型。所以我很惊讶,从C ++ 17开始,你可以"转换"如果使用大括号(聚合)初始化,则从一种类型到另一种类型,没有任何警告。

您建议您避免这些类型的意外转换?或者我错过了什么?

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 ???
}

我可以理解您可以将circle视为point,因为circle已将point作为基类,但您可以静默转换为圆圈到一个矩形,对我来说是一个问题。

更新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 ???
}

2 个答案:

答案 0 :(得分:37)

  

struct D和struct E代表两种完全不相关的类型。

但他们并非完全不相关"类型。它们都具有相同的基类类型。这意味着每个D都可以隐式转换为B。因此,每个D 都是 B。因此,就调用的操作而言,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; };

所以这种性质总是存在的可能性。我不认为使用隐式基类转换会使它变得更糟糕"。

更深层次的问题是用户尝试使用E初始化D的原因。

  

你可以默默地从一个圆圈转换为一个矩形,这对我来说是一个问题。

如果你这样做,你会遇到同样的问题:

struct rectangle
{
  rectangle(point p);

  int sx; int sy;
  point p;
};

您不仅可以执行rectangle r{c};,还可以执行rectangle r(c)

您的问题是您未正确使用继承。您对circlerectanglepoint之间的关系说了些什么,这并不是您的意思。因此,编译器允许您执行您不想做的事情。

如果您使用了包容而不是继承,这不会是一个问题:

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.
}

circle 总是一个point,或者从不一个point。您有时会尝试将其设为point,而不是其他人。这在逻辑上是不连贯的。如果您创建逻辑上不连贯的类型,那么您可以编写逻辑上不连贯的代码。

  

你可以默默地从一个圆圈转换为一个矩形,这对我来说是一个问题。

这提出了一个重点。严格来说,转换看起来像这样:

circle cr = ...
rectangle rect = cr;

那是不正确的。执行rectangle rect = {cr};后,您进行转化。您正在显式调用列表初始化,这对于聚合通常会引发聚合初始化。

现在,列表初始化肯定可以执行转换。但仅考虑D d = {e};,我们不应指望这意味着您正在执行从eD的转换。您已使用D列出初始化e类型的对象。如果E可转换为D,则可以执行转换,但如果非转换列表初始化表单也可以正常工作,则此初始化仍然有效。

因此,说明此功能可使circle转换为rectangle是不正确的。

答案 1 :(得分:29)

这在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 ???
                   ^

更直接的方法是将构造函数添加到这些类型中,以便它们不再是聚合。毕竟,你 不能使用聚合初始化。