wake-up-neo.com

Hat die C ++ 17-Erweiterung für die Aggregatinitialisierung die Klammerinitialisierung gefährlich gemacht?

Es scheint ein allgemeiner Konsens zu bestehen, dass Klammerinitialisierung sollte bevorzugt werden gegenüber anderen Formen der Initialisierung, jedoch seit Einführung von C++ 17 Erweiterung auf aggregierte Initialisierung zu bestehen scheint ein Risiko für unbeabsichtigte Konvertierungen. Betrachten Sie den folgenden Code:

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

Im obigen Code struct D und struct E stehen für zwei völlig unabhängige Typen. Daher ist es für mich eine Überraschung, dass Sie ab C++ 17 ohne Warnung von einem Typ zu einem anderen Typ "konvertieren" können, wenn Sie die geschweifte (aggregierte) Initialisierung verwenden.

Was würden Sie empfehlen, um solche versehentlichen Konvertierungen zu vermeiden? Oder vermisse ich etwas?

PS: Der obige Code wurde in Clang, GCC und dem neuesten VC++ getestet - sie sind alle gleich.

Update: Als Antwort auf die Antwort von Nicol. Betrachten Sie ein praktischeres Beispiel:

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

Ich kann verstehen, dass Sie ein circle als point anzeigen können, da circlepoint als Basisklasse hat, aber die Idee, dass Sie stillschweigend konvertieren können Ein Kreis zu einem Rechteck, das ist für mich ein Problem.

Update 2: Weil meine schlechte Wahl des Klassennamens das Problem für einige zu trüben scheint.

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 und struct E repräsentieren zwei völlig unabhängige Typen.

Aber sie sind keine "völlig unabhängigen" Typen. Sie haben beide den gleichen Basisklassentyp. Dies bedeutet, dass jedes D implizit in ein B konvertierbar ist. Und deshalb ist jeder DaB. Das Ausführen von E e{d}; Unterscheidet sich in Bezug auf die aufgerufene Operation nicht von E e{b};.

Sie können die implizite Konvertierung in Basisklassen nicht deaktivieren.

Wenn Sie dies wirklich stört, besteht die einzige Lösung darin, die Aggregatinitialisierung zu verhindern, indem Sie einen oder mehrere geeignete Konstruktoren bereitstellen, die die Werte an die Mitglieder weiterleiten.

Ob dies die Initialisierung von Aggregaten gefährlicher macht, glaube ich nicht. Sie könnten die obigen Umstände mit diesen Strukturen reproduzieren:

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

So etwas war immer eine Möglichkeit. Ich glaube nicht, dass die Verwendung der impliziten Basisklassenkonvertierung dies so viel "schlimmer" macht.

Eine tiefere Frage ist, warum ein Benutzer versucht hat, ein E mit einem D zu initialisieren.

die Idee, dass man leise von einem Kreis in ein Rechteck konvertieren kann, ist für mich ein Problem.

Sie würden das gleiche Problem haben, wenn Sie dies tun:

struct rectangle
{
  rectangle(point p);

  int sx; int sy;
  point p;
};

Sie können nicht nur rectangle r{c}; Ausführen, sondern auch rectangle r(c).

Ihr Problem ist, dass Sie die Vererbung falsch verwenden. Sie sagen Dinge über die Beziehung zwischen circle, rectangle und point, die Sie nicht meinen. Und deshalb können Sie mit dem Compiler Dinge tun, die Sie nicht tun wollten.

Wenn Sie Containment anstelle von Vererbung verwendet hätten, wäre dies kein Problem:

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

Entweder ist circleimmer ein point, oder es ist nie ein point. Sie versuchen es manchmal zu einem point zu machen und nicht zu einem anderen. Das ist logisch inkohärent. Wenn Sie logisch inkohärente Typen erstellen, können Sie logisch inkohärenten Code schreiben.


die Idee, dass man leise von einem Kreis in ein Rechteck konvertieren kann, ist für mich ein Problem.

Dies bringt einen wichtigen Punkt auf den Punkt. Die Konvertierung sieht genau genommen so aus:

circle cr = ...
rectangle rect = cr;

Das ist schlecht geformt. Wenn Sie rectangle rect = {cr}; Ausführen, führen Sie nicht eine Konvertierung durch. Sie rufen explizit die Listeninitialisierung auf, die für ein Aggregat normalerweise eine Aggregatinitialisierung auslöst.

Nun kann die Listeninitialisierung sicherlich eine Konvertierung durchführen. Wenn Sie jedoch nur D d = {e}; Angeben, sollten Sie nicht damit rechnen, dass Sie eine Konvertierung von einem e in einen D durchführen. Sie initialisieren ein Objekt vom Typ D mit einem e. Das kann eine Konvertierung durchführen, wenn E in D konvertierbar ist, aber diese Initialisierung kann immer noch gültig sein, wenn nicht konvertierte Listeninitialisierungsformulare ebenfalls funktionieren.

Es ist also falsch zu sagen, dass diese Funktion circle in rectangle konvertierbar macht.

37
Nicol Bolas

Dies ist in C++ 17 nicht neu. Bei der Gesamtinitialisierung konnten Sie immer Mitglieder auslassen (die aus einer leeren Initialisierungsliste initialisiert würden, C++ 11 ):

struct X {
    int i, j;
};

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

Es ist gerade so, dass es mehr Arten von Dingen gibt, die weggelassen werden könnten, da es mehr Arten von Dingen gibt, die eingeschlossen werden können.


zumindest gcc und clang warnen durch -Wmissing-field-initializers (es ist ein Teil von -Wextra) das zeigt an, dass etwas fehlt. Wenn dies ein großes Problem darstellt, kompilieren Sie einfach mit dieser aktivierten Warnung (und führen Sie möglicherweise ein Upgrade auf einen Fehler durch):

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

Direkter wäre es, diesen Typen einen Konstruktor hinzuzufügen, damit sie keine Aggregate mehr sind. Schließlich müssen Sie nicht die Aggregatinitialisierung verwenden.

29
Barry