L'initialisation uniforme de C ++ 11 remplace-t-elle l'ancienne syntaxe?

172

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.

void.pointer
la source
Cela pourrait être un sujet mieux discuté sur Programmers.se. Il semble pencher vers le côté Bon subjectif.
Nicol Bolas
6
@ NicolBolas: D'autre part, votre excellente réponse pourrait être un très bon candidat pour la balise c ++-faq. Je ne pense pas que nous ayons eu d'explication à ce sujet avant.
Matthieu M.

Réponses:

233

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:

vec3 GetValue()
{
  return vec3(x, y, z);
}

Pourquoi dois-je taper vec3deux 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:

vec3 GetValue()
{
  return {x, y, z};
}

Tout fonctionne.

Encore mieux est pour les arguments de fonction. Considère ceci:

void DoSomething(const std::string &str);

DoSomething("A string.");

Cela fonctionne sans avoir à taper un nom de type, car std::stringsait se construire de manière const 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. Le std::stringconstructeur qui prend un const char*devra prendre la longueur de la chaîne si je passe juste un const 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 avec auto, nous pouvons éviter d'avoir des noms de caractères supplémentaires:

DoSomething({strValue, strLen});

Ç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 obtenez auto v = GetSomething();nécessite de regarder la définition de GetSomething. Mais cela n’a pas cessé autod’ê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.

class Bar;

void Func()
{
  int foo(Bar());
}

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 un Bar, et la foovaleur 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 un Bar.

L’initialisation uniforme ne peut pas être interprétée comme un prototype de fonction:

class Bar;

void Func()
{
  int foo{Bar{}};
}

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 ++. Avec Typename{}, 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.

int val{5.2};

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:

std::vector<int> v{100};

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 un vector<int>élément avec 1 élément dont la valeur est 100. 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 prend initializer_list<int>, et que {100} pourrait être valide initializer_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 vectorde 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 ce vectortype, et le compilateur serait donc libre de choisir parmi les autres constructeurs.

Nicol Bolas
la source
11
+1 cloué dessus. Je supprime ma réponse car la vôtre aborde tous les mêmes points de manière beaucoup plus détaillée.
R. Martinho Fernandes
21
Le dernier point est pourquoi je voudrais vraiment std::vector<int> v{100, std::reserve_tag};. De même avec std::resize_tag. Il faut actuellement deux étapes pour réserver un espace vectoriel.
Xeo
6
@ NicolBolas - Deux points: je pensais que le problème de l'analyse contrariante était foo (), pas Bar (). En d'autres termes, si vous le faisiez 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?
void.pointer
7
@ RobertDailey: "Si vous le faisiez 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'il Bar()pourrait s'agir d'un nom de type ou d'une valeur temporaire. C'est ce qui crée l'ambiguïté du compilateur.
Nicol Bolas
8
unpleasant behavior- il y a un nouveau terme standard à retenir:>
see
64

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:

vec3 GetValue()
{
  <lots and lots of code here>
  ...
  return {x, y, z};
}

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:

void DoSomething(const std::string &str);
...
const char* strValue = ...;
size_t strLen = ...;

DoSomething({strValue, strLen});

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:

void DoSomething(const CoolStringType& str);

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:

struct Collection
{
    int first;
    char second;
    double third;
};

Collection c {1, '2', 3.0};
std::array<int, 3> a {{ 1, 2, 3 }};
std::map<int, char> m { {1, '1'}, {2, '2'}, {3, '3'} };

Exemple d'utilisation de la notion n ° 2:

class Stairs
{
    std::vector<float> stepHeights;

public:
    Stairs(float initHeight, int numSteps, float stepHeight)
    {
        float height = initHeight;

        for (int i = 0; i < numSteps; ++i)
        {
            stepHeights.push_back(height);
            height += stepHeight;
        }
    }
};

Stairs s (2.5, 10, 0.5);

Je pense que c'est une mauvaise chose que la nouvelle norme permette aux gens d'initialiser Stairs comme ceci:

Stairs s {2, 4, 6};

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

T var1;
T var2 (var1);

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:

T var2 {var1}; // fails if T is std::array for example
TommiT
la source
48
Si vous avez "<beaucoup de code ici>", votre code sera difficile à comprendre quelle que soit la syntaxe.
Kevin Cline
8
Outre IMO, il est du devoir de votre IDE de vous informer du type de retour (par ex. Via le survol). Bien sûr, si vous n'utilisez pas d'IDE, vous vous en
chargez
4
@TommiT Je suis d'accord avec certaines parties de ce que vous dites. Cependant, dans le même esprit que la autocontre 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).
Voir
5
" Mais la syntaxe utilisant des accolades ne fonctionne pas si le type T est un agrégat: " Notez qu'il s'agit d'un défaut signalé dans le comportement standard, plutôt que intentionnel, attendu.
Nicol Bolas
5
"Maintenant, il devra revenir à la signature de fonction" si vous devez faire défiler, votre fonction est trop grande.
Miles Rout
-3

Si vos constructeurs merely copy their parametersdans les variables de classe respectives in exactly the same orderdans 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
2
Pourquoi dites-vous que cela peut être plus rapide?
Jbcoe
Ceci est une erreur. Il n'y a pas d' obligation de déclarer un constructeur: 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.
Tim