Je peux comprendre pourquoi le type auto
de C++ 11 améliore la correction et la maintenabilité. J'ai lu que cela pouvait aussi améliorer les performances ( Presque toujours automatique de Herb Sutter), mais je manque une bonne explication.
auto
peut-il améliorer les performances?auto
peut améliorer les performances en en évitant les conversions implicites silencieuses. Un exemple que je trouve convaincant est le suivant.
std::map<Key, Val> m;
// ...
for (std::pair<Key, Val> const& item : m) {
// do stuff
}
Voir le bug? Nous pensons maintenant que nous prenons avec élégance chaque élément de la carte par référence constante et que nous utilisons la nouvelle plage-pour expression pour préciser notre intention, mais en réalité, nous copions l'élément every. Ceci est dû au fait std::map<Key, Val>::value_type
est std::pair<const Key, Val>
, ne pas std::pair<Key, Val>
. Ainsi, lorsque nous avons (implicitement):
std::pair<Key, Val> const& item = *iter;
Au lieu de prendre une référence à un objet existant et de le laisser comme cela, nous devons faire une conversion de type. Vous êtes autorisé à utiliser une référence const vers un objet (ou temporaire) d'un type différent tant qu'une conversion implicite est disponible, par exemple:
int const& i = 2.0; // perfectly OK
La conversion de type est une conversion implicite autorisée pour la même raison que vous pouvez convertir un fichier const Key
à un Key
, mais nous devons construire un temporaire du nouveau type afin de permettre cela. Ainsi, effectivement notre boucle fait:
std::pair<Key, Val> __tmp = *iter; // construct a temporary of the correct type
std::pair<Key, Val> const& item = __tmp; // then, take a reference to it
(Bien sûr, il n'y a pas réellement de __tmp
objet, il n’est là que pour illustration, en réalité le temporaire non nommé est simplement lié à item
pour sa durée de vie).
Je passe juste à:
for (auto const& item : m) {
// do stuff
}
vient de nous sauver une tonne de copies - maintenant le type référencé correspond au type d'initialiseur, donc aucune conversion temporaire ou n'est nécessaire, nous pouvons simplement faire une référence directe.
Étant donné que auto
déduit le type de l'expression d'initialisation, aucune conversion de type n'est impliquée. Combiné avec des algorithmes basés sur des modèles, cela signifie que vous pouvez obtenir un calcul plus direct que si vous deviez créer un type vous-même - en particulier lorsque vous utilisez des expressions dont vous ne pouvez pas nommer le type!
Un exemple typique vient de (ab) en utilisant std::function
:
std::function<bool(T, T)> cmp1 = std::bind(f, _2, 10, _1); // bad
auto cmp2 = std::bind(f, _2, 10, _1); // good
auto cmp3 = [](T a, T b){ return f(b, 10, a); }; // also good
std::stable_partition(begin(x), end(x), cmp?);
Avec cmp2
Et cmp3
, Tout l'algorithme peut intégrer l'appel de comparaison, alors que si vous construisez un objet std::function
, Non seulement l'appel ne peut pas être en ligne, mais vous devez également pour passer par la recherche polymorphe à l'intérieur effacé du type de l'emballage de la fonction.
Une autre variante de ce thème est que vous pouvez dire:
auto && f = MakeAThing();
C'est toujours une référence, liée à la valeur de l'expression d'appel de fonction, et ne construit jamais d'objets supplémentaires. Si vous ne connaissiez pas le type de la valeur renvoyée, vous pourriez être obligé de construire un nouvel objet (éventuellement temporaire) via quelque chose comme T && f = MakeAThing()
. (De plus, auto &&
Fonctionne même lorsque le type de retour n'est pas déplaçable et que la valeur de retour est une valeur.)
Il y a deux catégories.
auto
peut éviter un effacement de type. Il existe des types innommables (tels que lambdas) et des types quasi innommables (comme le résultat de std::bind
Ou un autre modèle de type expression, par exemple).
Sans auto
, vous finissez par taper effacer les données jusqu’à quelque chose comme std::function
. Le type effacement a des coûts.
std::function<void()> task1 = []{std::cout << "hello";};
auto task2 = []{std::cout << " world\n";};
task1
A une surcharge d'effacement de type - une allocation de tas possible, une difficulté à la ligne, et une surcharge d'appel de table de fonction virtuelle. task2
N'en a pas. Lambdas besoin auto ou d'autres formes de déduction de type à stocker sans effacement de type; d'autres types peuvent être si complexes qu'ils n'en ont besoin que dans la pratique.
Deuxièmement, vous pouvez vous tromper. Dans certains cas, le mauvais type fonctionnera apparemment parfaitement, mais provoquera une copie.
Foo const& f = expression();
se compilera si expression()
renvoie Bar const&
ou Bar
ou même Bar&
, où Foo
peut être construit à partir de Bar
. Un Foo
temporaire sera créé, puis lié à f
, et sa durée de vie sera prolongée jusqu'à ce que f
disparaisse.
Le programmeur a peut-être voulu dire Bar const& f
Et ne voulait pas en faire une copie, mais une copie est faite quand même.
L'exemple le plus courant est le type de *std::map<A,B>::const_iterator
, Qui est std::pair<A const, B> const&
Pas std::pair<A,B> const&
, Mais l'erreur est une catégorie d'erreurs qui pèsent en silence sur les performances. Vous pouvez construire un std::pair<A, B>
À partir d'un std::pair<const A, B>
. (La clé sur une carte est const, car son édition est une mauvaise idée)
@Barry et @KerrekSB ont d'abord illustré ces deux principes dans leurs réponses. Il s’agit simplement d’une tentative de mise en évidence des deux problèmes en une seule réponse, avec une formulation qui vise le problème plutôt que d’être centrée sur un exemple.
Les trois réponses existantes donnent des exemples dans lesquels utiliser auto
aide "le rend moins susceptible de pessimiser involontairement" , ce qui le rend effectivement "améliorer les performances".
Il y a un revers à la pièce. L'utilisation de auto
avec des objets dont les opérateurs ne renvoient pas l'objet de base peut générer un code incorrect (toujours compilable et exécutable). Par exemple, cette question demande comment l'utilisation de auto
a donné des résultats différents (incorrects) à l'aide de la bibliothèque Eigen, , c'est-à-dire les lignes suivantes
const auto resAuto = Ha + Vector3(0.,0.,j * 2.567);
const Vector3 resVector3 = Ha + Vector3(0.,0.,j * 2.567);
std::cout << "resAuto = " << resAuto <<std::endl;
std::cout << "resVector3 = " << resVector3 <<std::endl;
a abouti à une sortie différente. Certes, ceci est principalement dû à l'évaluation paresseuse d'Eigens, mais ce code est/devrait être transparent pour l'utilisateur (de la bibliothèque).
Bien que les performances ne soient pas très affectées ici, utiliser auto
pour éviter une pessimisation non intentionnelle pourrait être classé dans une optimisation prématurée, ou du moins, être une erreur;).