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 (en particulier celles des discussions de GoingNative 2012), ses exemples utilisent principalement cette syntaxe maintenant chaque fois qu'il construit des objets.
Est-il maintenant recommandé d'utiliser une initialisation uniforme dans les cas tous? Quelle devrait être l'approche générale de cette nouvelle fonctionnalité en ce qui concerne le style de codage et l'utilisation générale? Quelles sont les raisons pour lesquelles pas l'utiliser?
Notez que dans mon esprit, je pense principalement à la construction d'objets comme mon cas d'utilisation, mais s'il y a d'autres scénarios à considérer, faites-le moi savoir.
Le style de codage est en fin de compte subjectif et il est très peu probable que des avantages de performances substantiels en découlent. Mais voici ce que je dirais que vous gagnez de l'utilisation libérale de l'initialisation uniforme:
Considérer ce qui suit:
vec3 GetValue()
{
return vec3(x, y, z);
}
Pourquoi dois-je taper vec3
Deux fois? Y a-t-il un point à cela? Le compilateur sait bien ce que la fonction retourne. Pourquoi ne puis-je pas simplement dire "appeler le constructeur de ce que je retourne avec ces valeurs et le renvoyer?" Avec une initialisation uniforme, je peux:
vec3 GetValue()
{
return {x, y, z};
}
Tout fonctionne.
Encore mieux 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::string
Sait se construire implicitement à partir d'un const char*
. C'est génial. Mais que se passe-t-il si cette chaîne provient, par exemple RapidXML. Ou une chaîne Lua. Autrement dit, disons que je connais la longueur de la chaîne à l'avant. Le constructeur std::string
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 devrais faire ceci: DoSomething(std::string(strValue, strLen))
. Pourquoi avoir le nom de police supplémentaire là-dedans? 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 marche juste. Pas de noms de caractères, pas de chichis, rien. Le compilateur fait son travail, le code est plus court et tout le monde est content.
Certes, il y a des arguments à faire valoir que la première version (DoSomething(std::string(strValue, strLen))
) est plus lisible. Autrement dit, il est évident de savoir ce qui se passe et qui fait quoi. C'est vrai, dans une certaine mesure; comprendre le code uniforme basé sur l'initialisation nécessite de regarder le prototype de la fonction. C'est la même raison pour laquelle certains disent que vous ne devriez jamais passer de paramètres par référence non const: afin que vous puissiez voir sur le site d'appel si une valeur est en cours de modification.
Mais la même chose pourrait être dite pour auto
; savoir ce que vous obtenez de auto v = GetSomething();
nécessite de regarder la définition de GetSomething
. Mais cela n'a pas empêché auto
d'être utilisé avec un abandon imprudent une fois que vous y avez accès. Personnellement, je pense que ça ira bien une fois que vous vous y habituerez. Surtout avec un bon IDE.
Voici du code.
class Bar;
void Func()
{
int foo(Bar());
}
Quiz pop: qu'est-ce que foo
? Si vous avez répondu "une variable", vous vous trompez. C'est en fait le prototype d'une fonction qui prend comme paramètre une fonction qui retourne un Bar
, et la valeur de retour de la fonction foo
est un int.
C'est ce qu'on appelle le "Most Vexing Parse" de C++ car cela n'a absolument aucun sens pour un être humain. Mais les règles du C++ l'exigent malheureusement: s'il peut éventuellement être interprété comme un prototype de fonction, alors il le sera sera. Le problème est Bar()
; cela pourrait être l'une des deux choses. Il peut s'agir d'un type nommé Bar
, ce qui signifie qu'il crée un temporaire. Ou il peut s'agir d'une fonction qui ne prend aucun paramètre et renvoie 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 tout simplement pas en raison des règles d'analyse de C++. Avec Typename{}
, Il n'y a pas d'ambiguïté.
Le seul véritable pouvoir que vous abandonnez est le rétrécissement. Vous ne pouvez pas initialiser une valeur plus petite avec une plus grande avec une initialisation uniforme.
int val{5.2};
Cela ne se 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'initialisation.
Bien sûr, certains pourraient affirmer qu'un tel code mérite de ne pas compiler. Personnellement, je suis d'accord; le rétrécissement est très dangereux et peut conduire à un comportement désagréable. Il est probablement préférable de détecter ces problèmes très tôt au stade du compilateur. À tout le moins, le rétrécissement suggère que quelqu'un ne réfléchit pas trop au code.
Notez que les compilateurs vous avertissent généralement de ce genre de chose si votre niveau d'avertissement est élevé. Donc, tout cela ne fait que transformer l'avertissement en 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>
Avec 1 élément dont la valeur est 100
. Les deux sont théoriquement possibles.
En réalité, c'est ce dernier.
Pourquoi? Les listes d'initialisation utilisent la même syntaxe que l'initialisation uniforme. Il doit donc y avoir des règles pour expliquer ce qu'il faut 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 il le fera. Puisque vector<int>
A un constructeur de liste d'initialisation qui prend initializer_list<int>
, Et {100} pourrait être un initializer_list<int>
Valide, il doit donc doit être.
Pour obtenir le constructeur de dimensionnement, vous devez utiliser ()
Au lieu de {}
.
Notez que s'il s'agissait d'un vector
de quelque chose qui n'était pas convertible en entier, cela ne se produirait pas. Une initializer_list ne correspondrait pas au constructeur de liste d'initialisation de ce type vector
, et donc le compilateur serait libre de choisir parmi les autres constructeurs.
Je vais être en désaccord avec la section de réponse de Nicol Bolas Minimise les noms de caractères redondants . Parce que le code est écrit une fois et lu plusieurs fois, nous devrions essayer de minimiser le temps qu'il faut pour lire et comprendre le code, pas le temps qu'il faut pour écrire = code. Essayer de minimiser simplement la frappe revient à 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 immédiatement l'instruction return, 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 fonction IDE afin de voir le type de retour et de bien comprendre la déclaration de retour.
Et là encore, ce n'est pas facile pour quelqu'un 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 casser quand quelqu'un décide que DoSomething devrait également prendre en charge un autre type de chaîne et ajoute cette surcharge:
void DoSomething(const CoolStringType& str);
Si CoolStringType se trouve avoir un constructeur qui prend un char const * et un size_t (comme std :: string le fait), alors l'appel à DoSomething ({strValue, strLen}) entraînera une erreur d'ambiguïté.
Ma réponse à la question réelle:
Non, l'initialisation uniforme ne doit pas être considérée comme un remplacement de la syntaxe du constructeur de l'ancien style.
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 objets et versez-les dans cet objet que j'initialise.
2) Construisez cet objet en utilisant ces arguments que j'ai fournis comme guide.
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 # 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 des escaliers comme ceci:
Stairs s {2, 4, 6};
... parce que cela obscurcit le sens du constructeur. Une telle initialisation ressemble à la notion n ° 1, mais ce n'est pas le cas. Il ne s'agit pas de verser trois valeurs différentes de hauteurs de pas dans les objets, 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'ont utilisée, puis si l'implémentateur de bibliothèque ajoute plus tard un constructeur initializer_list à Stairs, alors tout le code qui a utilisé Stairs avec l'initialisation uniforme La syntaxe va casser.
Je pense que la communauté C++ devrait accepter une convention commune sur la façon dont l'initialisation uniforme est utilisée, c'est-à-dire uniformément sur 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.
APRÈS-RÉFLEXION:
Voici encore une autre raison pour laquelle vous ne devriez pas penser à l'initialisation uniforme en remplacement de l'ancienne syntaxe, et pourquoi 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);
Vous pensez maintenant que vous devez remplacer toutes les initialisations par la nouvelle syntaxe d'accolade afin que vous puissiez être (et le code sera) 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