首页 > 解决方案 > 聚合初始化的 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 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 ???
}

我可以理解您可以将 acircle视为 a point,因为circlehaspoint作为基类,但是您可以默默地将圆形转换为矩形的想法对我来说是个问题。

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

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

解决方案


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

但它们不是“完全不相关”的类型。它们都具有相同的基类类型。这意味着 everyD可以隐式转换为 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; };

所以这种性质的东西总是有可能的。我不认为使用隐式基类转换会使它变得“更糟”。

一个更深层次的问题是为什么用户试图E用 a来初始化一个D

您可以默默地将圆形转换为矩形的想法,这对我来说是个问题。

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

struct rectangle
{
  rectangle(point p);

  int sx; int sy;
  point p;
};

你不仅可以表演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.
}

要么circle总是a ,要么永远不是。你试图让它成为有时而不是其他人。这在逻辑上是不连贯的。如果您创建逻辑不连贯的类型,那么您可以编写逻辑不连贯的代码。pointpointpoint


您可以默默地将圆形转换为矩形的想法,这对我来说是个问题。

这提出了一个重要的观点。严格来说,转换如下所示:

circle cr = ...
rectangle rect = cr;

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

现在,列表初始化当然可以执行转换。但是仅仅给出D d = {e};,人们不应该期望这意味着您正在执行从 ae到 a 的转换D。您正在D使用e. E如果可转换为,则可以执行转换D,但如果非转换列表初始化形式也可以工作,则此初始化仍然有效。

所以说这个功能可以circle转换为rectangle.


推荐阅读