Je comprends que l'initialisation uniforme de C ++ 11 résout une certaine ambiguïté syntaxique dans le langage, mais dans de nombreuses présentations de Bjarne Stroustrup (notamment lors des entretiens sur GoingNative 2012), ses exemples utilisent principalement cette syntaxe maintenant lorsqu'il construit des objets.
Est-il recommandé maintenant d'utiliser l'initialisation uniforme dans tous les cas? Quelle devrait être l’approche générale pour cette nouvelle fonctionnalité en ce qui concerne le style de codage et son utilisation générale? Quelles sont les raisons pour ne pas l' utiliser?
Notez que, dans mon esprit, je pense principalement à la construction d’objet comme cas d’utilisation, mais s’il existe d’autres scénarios à envisager, merci de le faire savoir.
Réponses:
Le style de codage est finalement subjectif, et il est hautement improbable que des avantages substantiels en termes de performances en découlent. Mais voici ce que je dirais que l'utilisation libérale de l'initialisation uniforme vous apporte:
Minimise les noms de types redondants
Considérer ce qui suit:
Pourquoi dois-je taper
vec3
deux fois? Y at-il un point à cela? Le compilateur sait bien ce que la fonction retourne. Pourquoi ne puis-je pas simplement dire: "appelle le constructeur de ce que je retourne avec ces valeurs et le renvoie?" Avec une initialisation uniforme, je peux:Tout fonctionne.
Encore mieux est pour les arguments de fonction. Considère ceci:
Cela fonctionne sans avoir à taper un nom de type, car
std::string
sait se construire de manièreconst char*
implicite. C'est génial. Mais que se passe-t-il si cette chaîne provient de RapidXML? Ou une chaîne de Lua. C'est-à-dire, disons que je connais réellement la longueur de la chaîne à l'avant. Lestd::string
constructeur qui prend unconst char*
devra prendre la longueur de la chaîne si je passe juste unconst char*
.Il existe cependant une surcharge qui prend explicitement une longueur. Mais pour l' utiliser, je dois le faire:
DoSomething(std::string(strValue, strLen))
. Pourquoi le nom de type supplémentaire est-il là? Le compilateur sait quel est le type. Tout comme avecauto
, nous pouvons éviter d'avoir des noms de caractères supplémentaires:Ça fonctionne. Pas de noms de famille, pas de chichi, rien. Le compilateur fait son travail, le code est plus court et tout le monde est content.
Certes, il y a des arguments à faire pour que la première version (
DoSomething(std::string(strValue, strLen))
) soit plus lisible. En clair, ce qui se passe et qui fait quoi est évident. C'est vrai dans une certaine mesure. comprendre le code uniforme basé sur l'initialisation nécessite d'examiner le prototype de fonction. C'est la même raison pour laquelle certains disent que vous ne devriez jamais transmettre de paramètres par référence non const: vous pouvez ainsi voir sur le site de l'appel si une valeur est en cours de modification.Mais la même chose pourrait être dite pour
auto
; savoir ce que vous obtenezauto v = GetSomething();
nécessite de regarder la définition deGetSomething
. Mais cela n’a pas cesséauto
d’être utilisé avec un abandon presque téméraire une fois que vous y avez accès. Personnellement, je pense que ça ira une fois que vous vous y serez habitués. Surtout avec un bon IDE.Ne jamais obtenir le plus vexant parse
Voici du code.
Quiz Pop: c'est quoi
foo
? Si vous avez répondu "une variable", vous avez tort. C'est en fait le prototype d'une fonction qui prend comme paramètre une fonction qui retourne unBar
, et lafoo
valeur de retour de la fonction est un int.Cela s'appelle "La plupart des analyses vexantes" de C ++, car cela n'a aucun sens pour un être humain. Mais les règles de C ++ exigent malheureusement cela: si cela peut éventuellement être interprété comme un prototype de fonction, alors ce sera le cas. Le problème est
Bar()
; cela pourrait être l'une des deux choses. Il pourrait s'agir d'un type nomméBar
, ce qui signifie qu'il crée un temporaire. Ou cela pourrait être une fonction qui ne prend aucun paramètre et retourne unBar
.L’initialisation uniforme ne peut pas être interprétée comme un prototype de fonction:
Bar{}
crée toujours un temporaire.int foo{...}
crée toujours une variable.Il existe de nombreux cas où vous souhaitez utiliser
Typename()
mais que vous ne pouvez tout simplement pas utiliser à cause des règles d'analyse de C ++. AvecTypename{}
, il n'y a pas d'ambiguïté.Raisons pour ne pas
Le seul pouvoir réel que vous abandonnez est le rétrécissement. Vous ne pouvez pas initialiser une valeur plus petite avec une valeur plus grande avec une initialisation uniforme.
Cela ne compilera pas. Vous pouvez le faire avec une initialisation à l’ancienne, mais pas une initialisation uniforme.
Cela a été fait en partie pour que les listes d’initialisation fonctionnent réellement. Sinon, il y aurait beaucoup de cas ambigus en ce qui concerne les types de listes d'initialiseurs.
Bien sûr, certains pourraient soutenir qu'un tel code mérite de ne pas être compilé. Je suis personnellement d'accord rétrécir est très dangereux et peut conduire à un comportement désagréable. Il est probablement préférable de détecter ces problèmes dès le début du compilateur. À tout le moins, le rétrécissement suggère que quelqu'un ne pense pas trop au code.
Notez que les compilateurs vous préviendront généralement de ce genre de chose si votre niveau d’alerte est élevé. En réalité, tout cela fait de l’avertissement une erreur forcée. Certains pourraient dire que vous devriez le faire de toute façon;)
Il y a une autre raison de ne pas:
Qu'est-ce que cela fait? Il pourrait créer un
vector<int>
avec cent éléments construits par défaut. Ou il pourrait créer unvector<int>
élément avec 1 élément dont la valeur est100
. Les deux sont théoriquement possibles.En réalité, il fait le dernier.
Pourquoi? Les listes d'initialiseur utilisent la même syntaxe que l'initialisation uniforme. Il doit donc y avoir des règles pour expliquer quoi faire en cas d’ambiguïté. La règle est assez simple: si le compilateur peut utiliser un constructeur de liste d'initialisation avec une liste initialisée par accolade, alors ce sera le cas . Puisqu'il
vector<int>
a un constructeur de liste d'initialisation qui prendinitializer_list<int>
, et que {100} pourrait être valideinitializer_list<int>
, il doit donc l' être .Pour obtenir le constructeur de dimensionnement, vous devez utiliser à la
()
place de{}
.Notez que s'il s'agissait
vector
de quelque chose qui n'était pas convertible en entier, cela ne se produirait pas. Une liste d'initialisation ne correspondrait pas au constructeur de liste d'initialisateurs de cevector
type, et le compilateur serait donc libre de choisir parmi les autres constructeurs.la source
std::vector<int> v{100, std::reserve_tag};
. De même avecstd::resize_tag
. Il faut actuellement deux étapes pour réserver un espace vectoriel.int foo(10)
, ne rencontriez-vous pas le même problème? Deuxièmement, une autre raison de ne pas l'utiliser semble être davantage un problème d'ingénierie excessive, mais que se passe-t-il si nous construisons tous nos objets en utilisant{}
, mais un jour plus tard, j'ajoute un constructeur pour les listes d'initialisation? Désormais, toutes mes instructions de construction se transforment en instructions de liste d'initialisation. Cela semble très fragile en termes de refactoring. Des commentaires à ce sujet?int foo(10)
, ne rencontriez-vous pas le même problème?" N ° 10 est un littéral entier, et un littéral entier ne peut jamais être un nom de type. L'analyse vexante vient du fait qu'ilBar()
pourrait s'agir d'un nom de type ou d'une valeur temporaire. C'est ce qui crée l'ambiguïté du compilateur.unpleasant behavior
- il y a un nouveau terme standard à retenir:>Je ne suis pas d'accord avec la réponse de Nicol Bolas qui minimise les noms de caractères redondants . Étant donné que le code est écrit une fois et lu plusieurs fois, nous devrions essayer de minimiser le temps nécessaire à la lecture et à la compréhension du code, et non le temps nécessaire à l' écriture du code. Essayer simplement de minimiser la frappe, c'est essayer d’optimiser la mauvaise chose.
Voir le code suivant:
Quelqu'un qui lit le code ci-dessus pour la première fois ne comprendra probablement pas l'instruction de retour immédiatement, car au moment où il atteindra cette ligne, il aura oublié le type de retour. Maintenant, il devra revenir à la signature de la fonction ou utiliser une fonctionnalité de l'EDI pour voir le type de retour et bien comprendre la déclaration de retour.
Et là encore, il est difficile pour une personne qui lit le code pour la première fois de comprendre ce qui est réellement construit:
Le code ci-dessus va se rompre lorsque quelqu'un décide que DoSomething doit également prendre en charge un autre type de chaîne et ajoute cette surcharge:
Si CoolStringType a un constructeur qui prend un caractère constant * et un size_t (comme le fait std :: string), alors l'appel à DoSomething ({strValue, strLen}) provoquera une erreur d'ambiguïté.
Ma réponse à la question:
non, l'initialisation uniforme ne doit pas être considérée comme un remplacement de la syntaxe de constructeur de style ancien.
Et mon raisonnement est le suivant:
si deux déclarations n’ont pas le même type d’intention, elles ne devraient pas se ressembler. Il existe deux types de notions d'initialisation d'objet:
1) Prenez tous ces éléments et versez-les dans cet objet que j'initialise.
2) Construisez cet objet en utilisant les arguments que j'ai fournis à titre indicatif.
Exemples d'utilisation de la notion n ° 1:
Exemple d'utilisation de la notion n ° 2:
Je pense que c'est une mauvaise chose que la nouvelle norme permette aux gens d'initialiser Stairs comme ceci:
... parce que cela brouille le sens du constructeur. Une initialisation comme celle-ci ressemble à la notion n ° 1, mais ce n’est pas le cas. Il ne s'agit pas de verser trois valeurs différentes de hauteur de pas dans l'objet, même si cela semble être le cas. Et aussi, plus important encore, si une implémentation de bibliothèque de Stairs comme ci-dessus a été publiée et que les programmeurs l’utilisent, puis si le développeur de la bibliothèque ajoute ultérieurement un constructeur initializer_list à Stairs, alors tout le code qui utilise Stairs avec initialisation uniforme La syntaxe va se rompre.
Je pense que la communauté C ++ devrait s'accorder sur une convention commune sur la manière dont l'initialisation uniforme est utilisée, c'est-à-dire uniformément pour toutes les initialisations ou, comme je le suggère fortement, séparer ces deux notions d'initialisation et clarifier ainsi l'intention du programmeur au lecteur de le code.
AFTERTHOUGHT:
Voici encore une autre raison pour laquelle vous ne devriez pas penser à l'initialisation uniforme pour remplacer l'ancienne syntaxe et pour laquelle vous ne pouvez pas utiliser la notation d'accolade pour toutes les initialisations:
Dites, votre syntaxe préférée pour faire une copie est:
Maintenant, vous pensez que vous devriez remplacer toutes les initialisations par la nouvelle syntaxe d'accolade afin que vous puissiez être (et le code semblera) plus cohérent. Mais la syntaxe utilisant des accolades ne fonctionne pas si le type T est un agrégat:
la source
auto
contre déclaration de type explicite débat, je dirais un équilibre: initializers uniforme roche assez grand temps modèle méta-programmation des situations où le type est généralement assez évident de toute façon. Cela évitera de répéter votre sacrement compliqué-> decltype(....)
pour une incantation, par exemple pour des modèles simples de fonctions en ligne (me fait pleurer).Si vos constructeurs
merely copy their parameters
dans les variables de classe respectivesin exactly the same order
dans lesquelles ils sont déclarés à l'intérieur de la classe, l'utilisation d'une initialisation uniforme peut éventuellement être plus rapide (mais peut aussi être absolument identique) par rapport à l'appel du constructeur.Évidemment, cela ne change pas le fait que vous devez toujours déclarer le constructeur.
la source
struct X { int i; }; int main() { X x{42}; }
. Il est également incorrect que l’initialisation uniforme puisse être plus rapide que l’initialisation de valeur.