Расширяется ли расширение С++ 17 для агрегации инициализации, сделанной инициализацией скобки опасной?

Похоже, существует общий консенсус в отношении того, что инициализация скобок должна быть предпочтительной по сравнению с другими формами инициализации, однако с тех пор, как введение расширения С++ 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 представляют два совершенно несвязанных типа. Поэтому для меня неожиданно, что с С++ 17 вы можете "конвертировать" из одного типа в другой без предупреждения, если вы используете инициализацию скобки (агрегата).

Что бы вы порекомендовали избежать этих случайных преобразований? Или я чего-то не хватает?

PS: приведенный выше код был протестирован в Clang, GCC и последнем VC++ - они все одинаковы.

Обновление: в ответ на ответ от Nicol. Рассмотрим более практичный пример:

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

Ответы

Ответ 1

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

Ваша проблема в том, что вы неправильно используете наследование. Вы говорите об отношениях между circle, rectangle и 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.
}

Любой circle всегда является point, или это никогда не является point. Вы пытаетесь сделать его point иногда и не другие. Это логично некогерентно. Если вы создаете логически некогерентные типы, вы можете написать логически некогерентный код.


идея о том, что вы можете безмолвно преобразовать из круга в прямоугольник, это для меня проблема.

Это поднимает важный момент. Преобразование, строго говоря, выглядит так:

circle cr = ...
rectangle rect = cr;

Это плохо сформировано. Когда вы делаете rectangle rect = {cr}; , вы не делаете конверсии. Вы явно вызываете инициализацию списка, которая для агрегата обычно вызывает инициализацию агрегата.

Теперь инициализация списка, безусловно, может выполнять преобразование. Но при условии просто D d = {e}; , не следует ожидать, что это означает, что вы выполняете преобразование из e в D Вы инициализируете список объектов типа D с помощью e. Это может выполнять преобразование, если E конвертируется в D, но эта инициализация все еще может быть действительной, если также могут работать формы инициализации списка без преобразования.

Поэтому неверно говорить, что эта функция делает circle конвертируемой в rectangle.

Ответ 2

Это не ново в С++ 17. Агрегатная инициализация всегда позволяла вам оставлять члены (которые были бы инициализированы из пустого списка инициализаторов, С++ 11):

struct X {
    int i, j;
};

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

Это просто теперь, когда есть больше видов вещей, которые можно было бы оставить, так как есть больше видов вещей, которые могут быть включены.


gcc и clang, по крайней мере, предоставят предупреждение посредством -Wmissing-field-initializers (это часть -Wextra), которые -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 ???
                   ^

Более прямым было бы просто добавить конструктор к этим типам, чтобы они перестали быть агрегатами. В конце концов, вам не нужно использовать агрегатную инициализацию.