Je pensais que puisque les objets de type définis par l'utilisateur C++ 11 devaient être construits avec le nouveau {...}
syntaxe au lieu de l'ancienne (...)
syntaxe (sauf pour le constructeur surchargé pour std::initializer_list
et des paramètres similaires (par exemple std::vector
: taille ctor vs 1 elem init_list ctor)).
Les avantages sont les suivants: pas de conversions implicites étroites, pas de problème avec l'analyse la plus contrariante, cohérence (?). Je n'ai vu aucun problème car je pensais que ce sont les mêmes (sauf l'exemple donné).
Mais ce n'est pas le cas.
Le {}
appelle le constructeur par défaut.
... Sauf quand:
Alors on dirait qu'il plutôt la valeur initialise l'objet? ... Même si l'objet a supprimé le constructeur par défaut, le {}
peut créer un objet. Cela ne va-t-il pas à l'encontre de la finalité d'un constructeur supprimé?
...Sauf quand:
Ensuite, il échoue avec call to deleted constructor
.
...Sauf quand:
Ensuite, il échoue avec des initialiseurs de champ manquants.
Mais alors vous pouvez utiliser {value}
pour construire l'objet.
Ok peut-être que c'est la même chose que la première exception (valeur init l'objet)
...Sauf quand:
Alors ni {}
ni {value}
peut créer un objet.
Je suis sûr que j'en ai manqué quelques-uns. L'ironie est qu'elle est appelée syntaxe d'initialisation uniforme . Je répète: [~ # ~] uniforme [~ # ~] syntaxe d'initialisation.
Quelle est cette folie?
struct foo {
foo() = delete;
};
// All bellow OK (no errors, no warnings)
foo f = foo{};
foo f = {};
foo f{}; // will use only this from now on.
struct foo {
foo() = delete;
foo(int) = delete;
};
foo f{}; // OK
struct foo {
foo() = delete;
foo(int) {};
};
foo f{}; // error call to deleted constructor
struct foo {
int a;
foo() = delete;
};
foo f{}; // error use of deleted function foo::foo()
foo f{3}; // OK
struct foo {
int a;
foo() = delete;
foo(int) = delete;
};
foo f{}; // ERROR: missing initializer
foo f{3}; // OK
struct foo {
int a = 3;
foo() = delete;
};
/* Fa */ foo f{}; // ERROR: use of deleted function `foo::foo()`
/* Fb */ foo f{3}; // ERROR: no matching function to call `foo::foo(init list)`
En regardant les choses de cette façon, il est facile de dire qu'il y a un chaos complet et total dans la façon dont un objet est initialisé.
La grande différence vient du type de foo
: s'il s'agit d'un type agrégé ou non.
C'est un agrégat s'il a:
- aucun constructeur fourni par l'utilisateur (une fonction supprimée ou par défaut ne compte pas comme fournie par l'utilisateur),
- aucun membre de données non statique privé ou protégé,
- pas d'initialisation d'accolade ou d'égalité pour les membres de données non statiques (depuis c ++ 11 jusqu'à (inversé) c ++ 14)
- pas de classes de base,
- aucune fonction membre virtuelle.
Donc:
foo
est un agrégatfoo
n'est pas un agrégat4.9
ne l'implémente pas.5.2.0
Est-ce que5.2.1 ubuntu
non (peut-être une régression)Les effets de l'initialisation de liste d'un objet de type T sont:
- ...
- Si T est un type agrégé, l'initialisation agrégée est effectuée. Cela prend en charge les scénarios A B D E (et F en C++ 14)
- Sinon, les constructeurs de T sont considérés en deux phases:
- Tous les constructeurs qui prennent std :: initializer_list ...
- sinon [...] tous les constructeurs de T participent à la résolution de surcharge [...] Cela prend en charge C (et F en C++ 11)
- ...
:
Initialisation agrégée d'un objet de type T (scénarios A B D E (F c ++ 14)):
- Chaque membre de classe non statique, dans l'ordre d'apparition dans la définition de classe, est initialisé en copie à partir de la clause correspondante de la liste d'initialisation. (référence de tableau omise)
TL; DR
Toutes ces règles peuvent encore sembler très compliquées et induire des maux de tête. Personnellement, je simplifie trop pour moi-même (si je me tire une balle dans le pied, alors qu'il en soit ainsi: je suppose que je vais passer 2 jours à l'hôpital plutôt que d'avoir une douzaine de jours de maux de tête):
Cela ne va-t-il pas à l'encontre de la finalité d'un constructeur supprimé?
Eh bien, je ne sais pas ça, mais la solution est de faire de foo
pas un agrégat. La forme la plus générale qui n'ajoute pas de surcharge et ne change pas la syntaxe utilisée de l'objet est de le faire hériter d'une structure vide:
struct dummy_t {};
struct foo : dummy_t {
foo() = delete;
};
foo f{}; // ERROR call to deleted constructor
Dans certaines situations (pas de membres non statiques du tout, je suppose), une alternative serait de supprimer le destructeur (cela rendra l'objet non instanciable dans n'importe quel contexte):
struct foo {
~foo() = delete;
};
foo f{}; // ERROR use of deleted function `foo::~foo()`
Cette réponse utilise les informations recueillies auprès de:
Que sont les agrégats et les POD et comment/pourquoi sont-ils spéciaux?
Un grand merci à @ M.M qui a aidé à corriger et à améliorer ce post.
Ce qui vous dérange, c'est l'initialisation agrégée .
Comme vous le dites, l'utilisation de l'initialisation de liste présente des avantages et des inconvénients. (Le terme "initialisation uniforme" n'est pas utilisé par la norme C++).
L'un des inconvénients est que l'initialisation de liste se comporte différemment pour les agrégats que pour les non-agrégats. De plus, la définition de agrégat change légèrement avec chaque norme.
Les agrégats ne sont pas créés via un constructeur. (Techniquement, ils pourraient l'être, mais c'est une bonne façon d'y penser). Au lieu de cela, lors de la création d'un agrégat, la mémoire est allouée, puis chaque membre est initialisé dans l'ordre en fonction de ce qui est dans l'initialiseur de liste.
Les non-agrégats sont créés via des constructeurs et, dans ce cas, les membres de l'initialiseur de liste sont des arguments de constructeur.
Il y a en fait un défaut de conception dans ce qui précède: si nous avons T t1; T t2{t1};
, Alors l'intention est d'effectuer une construction de copie. Cependant, (avant C++ 14) si T
est un agrégat, l'initialisation agrégée se produit à la place et le premier membre de t2
Est initialisé avec t1
.
Cette faille a été corrigée dans un rapport de défaut qui a modifié C++ 14, donc désormais, la construction de la copie est vérifiée avant de passer à l'initialisation agrégée.
La définition de agrégat de C++ 14 est:
Un agrégat est un tableau ou une classe (article 9) sans constructeurs fournis par l'utilisateur (12.1), sans membres de données non statiques privés ou protégés (article 11), sans classes de base (article 10) et sans fonctions virtuelles (10.3 ).
En C++ 11, une valeur par défaut pour un membre non statique signifiait qu'une classe n'était pas un agrégat; cependant cela a été changé pour C++ 14. Fourni par l'utilisateur signifie déclaré par l'utilisateur, mais pas = default
Ou = delete
.
Si vous voulez vous assurer que votre appel constructeur n'effectue jamais accidentellement l'initialisation agrégée, alors vous devez utiliser ( )
Plutôt que { }
Et évitez les MVP par d'autres moyens.
Ces cas autour de l'initialisation d'agrégats sont contre-intuitifs pour la plupart et ont fait l'objet de la proposition p1008: Interdire les agrégats avec les constructeurs déclarés par l'utilisateur qui dit:
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
et présente quelques exemples qui chevauchent bien les cas que vous présentez:
struct X { X() = delete; }; int main() { X x1; // ill-formed - default c’tor is deleted X x2{}; // compiles! }
De toute évidence, l'intention du constructeur supprimé est d'empêcher l'utilisateur d'initialiser la classe. Cependant, contrairement à l'intuition, cela ne fonctionne pas: l'utilisateur peut toujours initialiser X via l'initialisation agrégée car cela contourne complètement les constructeurs. L'auteur peut même supprimer explicitement tous les constructeurs par défaut, copier et déplacer, et ne parvient toujours pas à empêcher le code client d'instancier X via l'initialisation d'agrégation comme ci-dessus. La plupart des développeurs C++ sont surpris par le comportement actuel lorsque ce code s'affiche. L'auteur de la classe X pourrait également envisager de rendre le constructeur par défaut privé. Mais si ce constructeur reçoit une définition par défaut, cela n'empêche pas encore l'initialisation agrégée (et donc l'instanciation) de la classe:
struct X { private: X() = default; }; int main() { X x1; // ill-formed - default c’tor is private X x2{}; // compiles! }
En raison des règles actuelles, l'initialisation agrégée nous permet de "construire par défaut" une classe même si elle n'est pas, en fait, constructible par défaut:
static_assert(!std::is_default_constructible_v<X>);
passerait pour les deux définitions de X ci-dessus.
...
Les modifications proposées sont les suivantes:
Modifiez le paragraphe 1 de [dcl.init.aggr] comme suit:
Un agrégat est un tableau ou une classe (article 12) avec
pas fourni par l'utilisateur, explicite, u̲s̲e̲r̲-̲d̲e̲c̲l̲a̲r̲e̲d̲ ou constructeurs hérités (15.1),aucun membre de données non statique privé ou protégé (article 14),
aucune fonction virtuelle (13.3), et
aucune classe de base virtuelle, privée ou protégée (13.1).
Modifier le paragraphe 17 de [dcl.init.aggr] comme suit:
[Remarque: Un tableau agrégé ou une classe agrégée peut contenir des éléments d'un type classe >> avec un
fourni par l'utilisateurconstructeur u̲s̲e̲r̲-̲d̲e̲c̲l̲a̲r̲e̲d̲ (15.1). L'initialisation de >> ces objets agrégés est décrite au 15.6.1. —Fin note]Ajoutez ce qui suit à [diff.cpp17] dans l'annexe C, section C.5 C++ et ISO C++ 2017:
C.5.6 Article 11: déclarants [diff.cpp17.dcl.decl]
Paragraphe concerné : [dcl.init.aggr]
Changement : Une classe qui a des constructeurs déclarés par l'utilisateur n'est jamais un agrégat.
Justification : supprimer l'initialisation d'agrégat potentiellement sujette aux erreurs qui peut s'appliquer nonobstant les constructeurs déclarés d'une classe.
Effet sur la fonctionnalité d'origine : Le code C++ 2017 valide qui agrège-initialise un type avec un constructeur déclaré par l'utilisateur peut être mal formé ou ont une sémantique différente dans la présente Norme internationale.
Suivi d'exemples que j'omets.
La proposition était acceptée et fusionnée en C++ 2 nous pouvons trouver le dernier projet ici qui contient ces modifications et nous pouvons voir les modifications apportées à [dcl. init.aggr] p1.1 et [dcl.init.aggr] p17 et diff déclarations C++ 17 .
Cela devrait donc être corrigé dans C++ 20 vers l'avant.