web-dev-qa-db-fra.com

Pourquoi l'initialisation agrégée ne fonctionne plus depuis C ++ 20 si un constructeur est explicitement par défaut ou supprimé?

Je migre un projet Visual Studio C++ de VS2017 vers VS2019.

Je reçois maintenant une erreur, qui ne s'est pas produite auparavant, qui peut être reproduite avec ces quelques lignes de code:

struct Foo
{
    Foo() = default;
    int bar;
};
auto test = Foo { 0 };

L'erreur est

(6): erreur C2440: 'initialisation': impossible de convertir de 'liste d'initialisation' en 'Foo'

(6): remarque: aucun constructeur n'a pu prendre le type source, ou la résolution de surcharge du constructeur était ambiguë

Le projet est compilé avec /std:c++latest drapeau. Je l'ai reproduit sur godbolt . Si je le mets en /std:c++17, il compile bien comme avant.

J'ai essayé de compiler le même code avec clang avec -std=c++2a et a obtenu une erreur similaire. En outre, la valeur par défaut ou la suppression d'autres constructeurs génère cette erreur.

Apparemment, de nouvelles fonctionnalités C++ 20 ont été ajoutées dans VS2019 et je suppose que l'origine de ce problème est décrite dans https://en.cppreference.com/w/cpp/language/aggregate_initialization . Il y est dit qu'un agrégat peut être une structure qui (entre autres critères) a

  • aucun constructeur fourni par l'utilisateur, hérité ou explicite (les constructeurs explicitement par défaut ou supprimés sont autorisés) (depuis C++ 17) (jusqu'à C++ 20)
  • aucun constructeur déclaré ou hérité par l'utilisateur (depuis C++ 20)

Notez que la partie entre parenthèses "les constructeurs explicitement par défaut ou supprimés sont autorisés" a été supprimée et que "fourni par l'utilisateur" est devenu "déclaré par l'utilisateur".

Donc ma première question est, ai-je raison de supposer que ce changement dans la norme est la raison pour laquelle mon code a été compilé auparavant mais ne le fait plus?

Bien sûr, il est facile de résoudre ce problème: supprimez simplement les constructeurs explicitement par défaut.

Cependant, j'ai explicitement par défaut et supprimé de nombreux constructeurs dans tous mes projets parce que j'ai trouvé que c'était une bonne habitude de rendre le code beaucoup plus expressif de cette façon car cela entraîne simplement moins de surprises qu'avec les constructeurs implicitement par défaut ou supprimés. Avec ce changement cependant, cela ne semble plus être une si bonne habitude ...

Donc ma vraie question est: Quel est le raisonnement derrière ce changement de C++ 17 à C++ 20? Cette rupture de compatibilité descendante a-t-elle été faite exprès? Y a-t-il eu un compromis comme "Ok, nous cassons la compatibilité descendante ici, mais c'est pour le plus grand bien."? Quel est ce plus grand bien?

22
sebrockm

Le résumé de P1008 , la proposition qui a conduit au changement:

C++ permet actuellement à certains types avec des constructeurs déclarés par l'utilisateur d'être initialisés via l'initialisation agrégée, en contournant ces constructeurs. Le résultat est un code surprenant, déroutant et bogué. Cet article propose un correctif qui rend la sémantique d'initialisation en C++ plus sûre, plus uniforme et plus facile à enseigner. Nous discutons également des changements de rupture introduits par ce correctif.

Un des exemples qu'ils donnent est le suivant.

struct X {
  int i{4};
  X() = default;
};

int main() {
  X x1(3); // ill-formed - no matching c’tor
  X x2{3}; // compiles!
}

Pour moi, il est tout à fait clair que les modifications proposées valent l'incompatibilité en amont qu'elles portent. Et en effet, il ne semble plus être une bonne pratique de = default agréger les constructeurs par défaut.

22
lubgr

Le raisonnement de P1008 (PDF) peut être mieux compris dans deux directions:

  1. Si vous posiez un programmeur C++ relativement nouveau devant une définition de classe et demandiez "est-ce un agrégat", seraient-ils corrects?

La conception commune d'un agrégat est "une classe sans constructeurs". Si Typename() = default; est dans une définition de classe, la plupart des gens verront cela comme ayant un constructeur. Il se comportera comme le constructeur standard par défaut, mais le type en a toujours un. Telle est la conception large de l'idée de nombreux utilisateurs.

Un agrégat est censé être une classe de données pures, capable de faire en sorte qu'un membre assume n'importe quelle valeur qui lui est donnée. De ce point de vue, vous n'avez aucune raison de lui donner des constructeurs de toute nature, même si vous les avez par défaut. Ce qui nous amène au prochain raisonnement:

  1. Si ma classe remplit les exigences d'un agrégat, mais que je ne veux pas que ce soit un agrégat, comment dois-je faire?

La réponse la plus évidente serait de = default le constructeur par défaut, car je suis probablement quelqu'un du groupe # 1. De toute évidence, cela ne fonctionne pas.

Avant C++ 20, vos options sont de donner à la classe un autre constructeur ou d'implémenter l'une des fonctions membres spéciales. Aucune de ces options n'est acceptable, car (par définition) ce n'est pas quelque chose que vous avez réellement besoin de mettre en œuvre; vous le faites juste pour provoquer un effet secondaire.

Après C++ 20, la réponse évidente fonctionne.

En modifiant les règles de cette manière, cela rend visible la différence entre un agrégat et un non agrégé . Les agrégats n'ont pas de constructeurs; donc si vous voulez qu'un type soit un agrégat, vous ne lui donnez pas de constructeurs.

Oh, et voici un fait amusant: pré-C++ 20, ceci est un agrégat:

class Agg
{
  Agg() = default;
};

Notez que le constructeur par défaut est privé , donc seules les personnes ayant un accès privé à Agg peuvent l'appeler ... sauf si elles utilisent Agg{}, contourne le constructeur et est parfaitement légal.

L'intention claire de cette classe est de créer une classe qui peut être copiée, mais ne peut obtenir sa construction initiale que de ceux qui ont un accès privé. Cela permet le transfert des contrôles d'accès, car seul le code qui a reçu un Agg peut appeler des fonctions qui prennent Agg comme paramètre. Et seul le code ayant accès à Agg peut en créer un.

Ou du moins, c'est comme ça que ça devrait être.

Maintenant, vous pouvez résoudre ce problème de manière plus ciblée en disant qu'il s'agit d'un agrégat si les constructeurs par défaut/supprimés ne sont pas déclarés publiquement. Mais cela semble encore plus incohérent; parfois, une classe avec un constructeur visiblement déclaré est un agrégat et parfois non, selon l'endroit où se trouve ce constructeur visiblement déclaré.

14
Nicol Bolas

En fait, MSDN a répondu à votre préoccupation dans le document ci-dessous:

Spécification modifiée du type d'agrégat

Dans Visual Studio 2019, sous/std: c ++ latest, une classe avec n'importe quel constructeur déclaré par l'utilisateur (par exemple, y compris un constructeur déclaré = default ou = delete) n'est pas un agrégat. Auparavant, seuls les constructeurs fournis par l'utilisateur disqualifiaient une classe d'être un agrégat. Cette modification impose des restrictions supplémentaires sur la façon dont ces types peuvent être initialisés.

3
Santosh Dhanawade