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

Bar*_*ett 48 c++ initialization list-initialization 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;
};
Run Code Online (Sandbox Code Playgroud)

在上面的代码中struct D,struct 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 ???
}
Run Code Online (Sandbox Code Playgroud)

我可以理解你可以查看a circle作为一个point,因为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 ???
}
Run Code Online (Sandbox Code Playgroud)

Nic*_*las 37

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

但它们并非"完全不相关"的类型.它们都具有相同的基类类型.这意味着每个都D可以隐式转换为a 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; };
Run Code Online (Sandbox Code Playgroud)

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

更深层次的问题是用户为什么尝试E使用a D来初始化a .

你可以默默地从一个圆圈转换成一个矩形的想法,这对我来说是一个问题.

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

struct rectangle
{
  rectangle(point p);

  int sx; int sy;
  point p;
};
Run Code Online (Sandbox Code Playgroud)

你不仅可以执行rectangle r{c};,但是rectangle r(c).

您的问题是您正在使用继承错误.你说约之间的关系的事情circle,rectanglepoint你没有意思.因此,编译器允许您执行您不想做的事情.

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

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.
}
Run Code Online (Sandbox Code Playgroud)

无论circle总是一个point,或者是从来没有一个point.你试图让它成为point有时而不是其他人.这在逻辑上是不连贯的.如果您创建逻辑上不连贯的类型,那么您可以编写逻辑上不连贯的代码.


你可以默默地从一个圆圈转换成一个矩形的想法,这对我来说是一个问题.

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

circle cr = ...
rectangle rect = cr;
Run Code Online (Sandbox Code Playgroud)

那是不合理的.当你这样做时rectangle rect = {cr};,你没有进行转换.您正在显式调用列表初始化,这对于聚合通常会引发聚合初始化.

现在,列表初始化当然可以执行转换.但是仅仅给出D d = {e};,人们不应该指望这意味着你正在进行从a e到a的转换D.你是D用一个列表初始化一个类型的对象e.如果E可以转换为可以执行转换D,但是如果非转换列表初始化表单也可以工作,则此初始化仍然有效.

所以说这个功能可以circle转换成是不正确的rectangle.

  • @Barnett:你的新榜样并不比旧的好.圈*有*分; 矩形*有*点,但它们不是任何实际的点.那么为什么你声明一个"是一个"关系(即:继承),其中这种关系没有反映在类型中?问题不在于语言; 这是你的用途. (20认同)
  • @Barnett:你问为什么语言的运作方式如此.我解释了继承背后的理论思想.你不必同意它,但这就是语言看到它的方式,这就是语言的行为方式.你想要做的不是语言想要支持的东西; 这就是为什么你在使它工作时遇到问题的原因. (8认同)
  • @Barnett如果使用C++对象模型,则会受到其语义的限制.`struct A:B`并不意味着"重复使用`B`的实现",它意味着"a`是一个`B`,可以在任何可以使用`B`的地方使用".如果你尝试使用它*没有*语义意味着你可以被烧掉.你已经说过,圆是一种点,矩形是一种点.确实,C++ 17现在已经添加了一个新的聚合构造方法,但它导致了一个问题,这取决于你的语义错误. (5认同)
  • @Barnett:如果"`B`可用于初始化`C`"并且"`A`可以在任何地方使用`B`可以使用",那么通过传递属性,"`A`可用于初始化` C`".问题是你试图将单个对象的初始化视为与多个对象的初始化不同的东西. (4认同)
  • @Barnett要解决这个问题,你需要一个*构造函数*.如果`C`>`B`写一个构造函数,它不仅仅是一个`B`.在这次改变之前,你究竟是多么想让人们构建一个"圈子"?创建一个未初始化的,然后一次设置一个值? (3认同)
  • `struct rectangle {point top_left; 点top_right; };` (2认同)

Bar*_*rry 29

这在C++ 17中并不新鲜.聚合初始化总是允许你放弃成员(这将从空的初始化列表C++ 11初始化):

struct X {
    int i, j;
};

X x{42}; // ok in C++11
Run Code Online (Sandbox Code Playgroud)

刚才有更多种类的东西可以省去,因为可以包含更多种类的东西.


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 ???
                   ^
Run Code Online (Sandbox Code Playgroud)

更直接的只是向这些类型添加构造函数,以便它们不再是聚合.你不具备使用集合初始化,毕竟.

  • @Barnett保护已经消失......除非你添加一个构造函数.或编译警告.或者不使用大括号初始化.或者有任何非默认初始化的成员.要么 ... (8认同)