Сделало ли расширение C ++ 17 для агрегатной инициализации опасной инициализацию фигурной скобки? - PullRequest
0 голосов
/ 09 мая 2018

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

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 равен всегда a point, либо никогда a point. Вы пытаетесь сделать это point иногда, а не другими. Это логически бессвязно. Если вы создаете логически несвязные типы, то вы можете написать логически несвязный код.


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

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

circle cr = ...
rectangle rect = cr;

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

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

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

0 голосов
/ 09 мая 2018

Это не ново в 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 ???
                   ^

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

...