web-dev-qa-db-fra.com

L’extension C ++ 17 à l’initialisation des agrégats a-t-elle rendu l’initialisation par accolade dangereuse?

Il semble y avoir un consensus général sur le fait que l’initialisation par accolade devrait être préférée par rapport aux autres formes d’initialisation, mais depuis l’introduction du C++ 17 extension à l’initialisation de l’agrégat , il semble que être un risque de conversions involontaires. Considérons le code suivant:

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

Dans le code ci-dessus struct D et struct E représente deux types complètement indépendants. Je suis donc surpris de constater qu’à partir de C++ 17, vous pouvez "convertir" d’un type à un autre sans avertissement préalable si vous utilisez l’initialisation par accolade (agrégat).

Que recommanderiez-vous pour éviter ces types de conversions accidentelles? Ou est-ce que je manque quelque chose?

PS: Le code ci-dessus a été testé dans Clang, GCC et le dernier VC++ - ils sont tous identiques.

Mise à jour: en réponse à la réponse de Nicol. Prenons un exemple plus pratique:

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

Je peux comprendre que vous puissiez voir un circle sous la forme d'un point, parce que circle a point comme classe de base, mais vous pouvez convertir en mode silencieux un cercle à un rectangle, cela pour moi est un problème.

Mise à jour 2: Parce que mon mauvais choix de nom de classe semble obscurcir le problème pour certains.

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

struct D et struct E représentent deux types complètement non liés.

Mais ce ne sont pas des types "complètement indépendants". Ils ont tous deux le même type de classe de base. Cela signifie que chaque D est implicitement convertible en B. Et donc chaque Dest aB. Donc, faire E e{d}; N'est pas différent de E e{b}; En termes d'opération invoquée.

Vous ne pouvez pas désactiver la conversion implicite en classes de base.

Si cela vous dérange vraiment, la seule solution consiste à empêcher l'initialisation de l'agrégat en fournissant un ou des constructeurs appropriés qui transmettent les valeurs aux membres.

Pour ce qui est de savoir si cela rend l'initialisation globale plus dangereuse, je ne le pense pas. Vous pouvez reproduire les circonstances ci-dessus avec ces structures:

struct B { int i; };
struct D { B b; char j; operator B() {return b;} };
struct E { B b; float k; };

Donc, quelque chose de cette nature était toujours une possibilité. Je ne pense pas que l’utilisation de la conversion implicite de la classe de base aggrave la situation.

Une question plus profonde est de savoir pourquoi un utilisateur a essayé d’initialiser un E avec un D pour commencer.

l'idée que vous pouvez convertir en silence d'un cercle à un rectangle est un problème pour moi.

Vous auriez le même problème si vous faisiez ceci:

struct rectangle
{
  rectangle(point p);

  int sx; int sy;
  point p;
};

Vous pouvez non seulement exécuter rectangle r{c}; Mais aussi rectangle r(c).

Votre problème est que vous n'utilisez pas correctement l'héritage. Vous dites des choses sur la relation entre circle, rectangle et point, ce que vous ne voulez pas dire. Et par conséquent, le compilateur vous permet de faire des choses que vous ne vouliez pas faire.

Si vous aviez utilisé le confinement au lieu de l'héritage, cela ne poserait pas de problème:

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

Soit circle soit toujours a point, soit jamais a point. Vous essayez d'en faire un point parfois et pas d'autres. C'est logiquement incohérent. Si vous créez des types logiquement incohérents, vous pouvez alors écrire du code logiquement incohérent.


l'idée que vous pouvez convertir en silence d'un cercle à un rectangle est un problème pour moi.

Cela soulève un point important. La conversion, à proprement parler, ressemble à ceci:

circle cr = ...
rectangle rect = cr;

C'est mal formé. Quand vous faites rectangle rect = {cr};, Vous faites pas une conversion. Vous appelez explicitement l'initialisation de liste, ce qui pour un agrégat provoque généralement l'initialisation de l'agrégat.

Maintenant, l'initialisation de liste peut certainement effectuer une conversion. Mais étant donné simplement D d = {e};, Il ne faut pas s’attendre à ce que cela signifie que vous effectuez la conversion d’un e en un D. Vous initialisez par la liste un objet de type D avec un e. Cela peut effectuer une conversion si E est convertible en D, mais cette initialisation peut toujours être valide si des formulaires d'initialisation de liste sans conversion peuvent également fonctionner.

Il est donc incorrect de dire que cette fonctionnalité rend circle convertible en rectangle.

37
Nicol Bolas

Ce n'est pas nouveau en C++ 17. L’initialisation des agrégats vous permettait toujours de laisser des membres (qui seraient initialisés à partir d’une liste d’initialisateurs vide, C++ 11 ):

struct X {
    int i, j;
};

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

C'est juste maintenant qu'il y a plus de sortes de choses qui pourraient être laissées, puisqu'il y a plus de sortes de choses qui peuvent être incluses.


gcc et clang au moins donneront un avertissement par -Wmissing-field-initializers (fait partie de -Wextra) qui indiquera qu'il manque quelque chose. Si c'est un gros problème, compilez simplement avec cet avertissement activé (et, éventuellement, mis à niveau en erreur):

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

Plus direct serait simplement d'ajouter un constructeur à ces types pour qu'ils cessent d'être des agrégats. Vous n'avez pas pour utiliser l'initialisation globale, après tout.

29
Barry