Quand dois-je utiliser la déduction de type de retour automatique C ++ 14?

144

Avec la sortie de GCC 4.8.0, nous avons un compilateur qui prend en charge la déduction automatique du type de retour, qui fait partie de C ++ 14. Avec -std=c++1y, je peux faire ceci:

auto foo() { //deduced to be int
    return 5;
}

Ma question est la suivante: quand dois-je utiliser cette fonctionnalité? Quand est-ce nécessaire et quand rend-il le code plus propre?

Scénario 1

Le premier scénario auquel je peux penser est dans la mesure du possible. Chaque fonction qui peut être écrite de cette façon devrait l'être. Le problème avec cela est que cela ne rend pas toujours le code plus lisible.

Scénario 2

Le scénario suivant consiste à éviter des types de retour plus complexes. A titre d'exemple très léger:

template<typename T, typename U>
auto add(T t, U u) { //almost deduced as decltype(t + u): decltype(auto) would
    return t + u;
}

Je ne crois pas que ce serait jamais vraiment un problème, bien que je suppose que le type de retour dépende explicitement des paramètres pourrait être plus clair dans certains cas.

Scénario 3

Ensuite, pour éviter la redondance:

auto foo() {
    std::vector<std::map<std::pair<int, double>, int>> ret;
    //fill ret in with stuff
    return ret;
}

En C ++ 11, nous pouvons parfois simplement return {5, 6, 7};remplacer un vecteur, mais cela ne fonctionne pas toujours et nous devons spécifier le type à la fois dans l'en-tête de la fonction et dans le corps de la fonction. C'est purement redondant, et la déduction automatique du type de retour nous évite cette redondance.

Scénario 4

Enfin, il peut être utilisé à la place de fonctions très simples:

auto position() {
    return pos_;
}

auto area() {
    return length_ * width_;
}

Parfois, cependant, nous pouvons regarder la fonction, voulant connaître le type exact, et s'il n'y est pas fourni, nous devons aller à un autre point du code, comme où pos_est déclaré.

Conclusion

Avec ces scénarios présentés, lequel d'entre eux s'avère être une situation dans laquelle cette fonctionnalité est utile pour rendre le code plus propre? Qu'en est-il des scénarios que j'ai négligé de mentionner ici? Quelles précautions dois-je prendre avant d'utiliser cette fonctionnalité pour qu'elle ne me morde pas plus tard? Y a-t-il quelque chose de nouveau que cette fonctionnalité apporte à la table qui ne serait pas possible sans elle?

Notez que les multiples questions visent à aider à trouver des perspectives à partir desquelles y répondre.

Chris
la source
18
Merveilleuse question! Pendant que vous demandez quels scénarios améliorent le code, je me demande également quels scénarios aggraveront les choses .
Drew Dormann le
2
@DrewDormann, c'est ce que je me demande aussi. J'aime utiliser les nouvelles fonctionnalités, mais savoir quand les utiliser et quand ne pas le faire est très important. Il y a une période de temps où de nouvelles fonctionnalités apparaissent que nous prenons pour comprendre cela, alors faisons-le maintenant afin que nous soyons prêts pour le moment venu officiellement :)
chris
2
@NicolBolas, Peut-être, mais le fait que ce soit dans une version réelle d'un compilateur maintenant serait suffisant pour que les gens commencent à l'utiliser dans le code personnel (il doit certainement être tenu à l'écart des projets à ce stade). Je fais partie de ces personnes qui aiment utiliser les dernières fonctionnalités possibles dans mon propre code, et bien que je ne sache pas dans quelle mesure la proposition va avec le comité, je pense que c'est la première incluse dans cette nouvelle option dit quelque chose. Il vaudrait peut-être mieux le laisser pour plus tard, ou (je ne sais pas comment cela fonctionnerait) relancé quand nous savons avec certitude que cela va arriver.
chris
1
@NicolBolas, si ça aide, ça a été adopté maintenant: p
chris
1
Les réponses actuelles ne semblent pas mentionner que le remplacement ->decltype(t+u)par la déduction automatique tue SFINAE.
Marc Glisse

Réponses:

62

C ++ 11 soulève des questions similaires: quand utiliser la déduction de type de retour dans lambdas et quand utiliser des autovariables.

La réponse traditionnelle à la question en C et C ++ 03 a été "au-delà des limites des instructions, nous rendons les types explicites, dans les expressions ils sont généralement implicites mais nous pouvons les rendre explicites avec des transtypages". C ++ 11 et C ++ 1y introduisent des outils de déduction de type afin que vous puissiez omettre le type à de nouveaux endroits.

Désolé, mais vous n'allez pas résoudre ce problème dès le départ en établissant des règles générales. Vous devez examiner un code particulier et décider par vous-même si cela facilite ou non la lisibilité de spécifier des types partout: vaut-il mieux que votre code dise «le type de cette chose est X», ou est-ce mieux pour votre code pour dire, "le type de cette chose n'est pas pertinent pour comprendre cette partie du code: le compilateur a besoin de savoir et nous pourrions probablement le résoudre mais nous n'avons pas besoin de le dire ici"?

Puisque la «lisibilité» n'est pas définie objectivement [*], et de plus elle varie selon le lecteur, vous avez une responsabilité en tant qu'auteur / éditeur d'un morceau de code qui ne peut pas être entièrement satisfait par un guide de style. Même dans la mesure où un guide de style spécifie des normes, différentes personnes préféreront des normes différentes et auront tendance à trouver que tout ce qui n'est pas familier est «moins lisible». Ainsi, la lisibilité d'une règle de style proposée particulière ne peut souvent être jugée que dans le contexte des autres règles de style en place.

Tous vos scénarios (même le premier) trouveront une utilité pour le style de codage de quelqu'un. Personnellement, je trouve que le second est le cas d'utilisation le plus convaincant, mais je pense néanmoins que cela dépendra de vos outils de documentation. Il n'est pas très utile de voir documenté que le type de retour d'un modèle de fonction est auto, alors que le voir documenté comme decltype(t+u)crée une interface publiée sur laquelle vous pouvez (espérons-le) compter.

[*] Parfois, quelqu'un essaie de faire des mesures objectives. Dans la petite mesure où quiconque parvient à des résultats statistiquement significatifs et généralement applicables, ils sont complètement ignorés par les programmeurs en activité, en faveur de l'instinct de l'auteur de ce qui est «lisible».

Steve Jessop
la source
1
Bon point avec les connexions aux lambdas (bien que cela permette des corps de fonctions plus complexes). L'essentiel est qu'il s'agit d'une fonctionnalité plus récente, et j'essaie toujours d'équilibrer les avantages et les inconvénients de chaque cas d'utilisation. Pour ce faire, il est utile de voir les raisons pour lesquelles il serait utilisé pour quoi afin que je puisse moi-même découvrir pourquoi je préfère ce que je fais. Je pense peut-être trop, mais c'est comme ça que je suis.
chris
1
@chris: comme je le dis, je pense que tout revient au même. Vaut-il mieux que votre code dise, "le type de cette chose est X", ou est-il préférable que votre code dise: "le type de cette chose n'est pas pertinent, le compilateur a besoin de savoir et nous pourrions probablement le résoudre mais nous n'avons pas besoin de le dire ". Lorsque nous écrivons, 1.0 + 27Unous affirmons ce dernier, lorsque nous écrivons, (double)1.0 + (double)27Unous affirmons le premier. La simplicité de la fonction, le degré de duplication, l'évitement decltypepourraient tous y contribuer mais aucun ne sera décisif de manière fiable.
Steve Jessop
1
Vaut-il mieux que votre code dise, "le type de cette chose est X", ou est-il préférable que votre code dise: "le type de cette chose n'est pas pertinent, le compilateur a besoin de savoir et nous pourrions probablement le résoudre mais nous n'avons pas besoin de le dire ". - Cette phrase correspond exactement à ce que je recherche. Je vais prendre cela en considération lorsque je rencontre des options d'utilisation de cette fonctionnalité, et autoen général.
chris
1
Je voudrais ajouter que les IDE pourraient atténuer le "problème de lisibilité". Prenez Visual Studio, par exemple: si vous survolez le automot - clé, il affichera le type de retour réel.
andreee
1
@andreee: c'est vrai dans certaines limites. Si un type a de nombreux alias, connaître le type réel n'est pas toujours aussi utile que vous le souhaiteriez. Par exemple, un type d'itérateur pourrait être int*, mais ce qui est réellement important, le cas échéant, c'est que la raison pour laquelle c'est int*parce que c'est ce qui std::vector<int>::iterator_typeest avec vos options de construction actuelles!
Steve Jessop
30

De manière générale, le type de retour de fonction est d'une grande aide pour documenter une fonction. L'utilisateur saura ce qui est attendu. Cependant, il y a un cas où je pense qu'il pourrait être intéressant de supprimer ce type de retour pour éviter la redondance. Voici un exemple:

template<typename F, typename Tuple, int... I>
  auto
  apply_(F&& f, Tuple&& args, int_seq<I...>) ->
  decltype(std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...))
  {
    return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...);
  }

template<typename F, typename Tuple,
         typename Indices = make_int_seq<std::tuple_size<Tuple>::value>>
  auto
  apply(F&& f, Tuple&& args) ->
  decltype(apply_(std::forward<F>(f), std::forward<Tuple>(args), Indices()))
  {
    return apply_(std::forward<F>(f), std::forward<Tuple>(args), Indices());
  }

Cet exemple est tiré du document officiel du comité N3493 . Le but de la fonction applyest de transmettre les éléments de a std::tupleà une fonction et de renvoyer le résultat. Les int_seqet make_int_seqne sont qu'une partie de l'implémentation, et ne dérouteront probablement que tout utilisateur essayant de comprendre ce qu'il fait.

Comme vous pouvez le voir, le type de retour n'est rien de plus qu'un decltypede l'expression retournée. De plus, apply_n'étant pas destiné à être vu par les utilisateurs, je ne suis pas sûr de l'utilité de documenter son type de retour lorsqu'il est plus ou moins le même que celui applyde celui-ci. Je pense que, dans ce cas particulier, la suppression du type de retour rend la fonction plus lisible. Notez que ce type de retour a en fait été abandonné et remplacé par decltype(auto)dans la proposition d'ajout applyà la norme, N3915 (notez également que ma réponse originale est antérieure à cet article):

template <typename F, typename Tuple, size_t... I>
decltype(auto) apply_impl(F&& f, Tuple&& t, index_sequence<I...>) {
    return forward<F>(f)(get<I>(forward<Tuple>(t))...);
}

template <typename F, typename Tuple>
decltype(auto) apply(F&& f, Tuple&& t) {
    using Indices = make_index_sequence<tuple_size<decay_t<Tuple>>::value>;
    return apply_impl(forward<F>(f), forward<Tuple>(t), Indices{});
}

Cependant, la plupart du temps, il est préférable de conserver ce type de retour. Dans le cas particulier que j'ai décrit ci-dessus, le type de retour est plutôt illisible et un utilisateur potentiel ne gagnera rien à le connaître. Une bonne documentation avec des exemples sera bien plus utile.


Autre chose qui n'a pas encore été mentionnée: while declype(t+u)permet d'utiliser l' expression SFINAE , decltype(auto)non (même s'il y a une proposition pour changer ce comportement). Prenons par exemple une foobarfonction qui appellera la foofonction membre d' un type si elle existe ou appellera la barfonction membre du type si elle existe, et supposera qu'une classe a toujours exacty fooou barmais ni les deux à la fois:

struct X
{
    void foo() const { std::cout << "foo\n"; }
};

struct Y
{
    void bar() const { std::cout << "bar\n"; }
};

template<typename C> 
auto foobar(const C& c) -> decltype(c.foo())
{
    return c.foo();
}

template<typename C> 
auto foobar(const C& c) -> decltype(c.bar())
{
    return c.bar();
}

L'appel foobarsur une instance de Xs'affiche foolors de l'appel foobarsur une instance de Ys'affiche bar. Si vous utilisez la déduction de type de retour automatique à la place (avec ou sans decltype(auto)), vous n'obtiendrez pas l'expression SFINAE et l'appel foobarsur une instance de l'un Xou l' autre Ydéclenchera une erreur de compilation.

Morwenn
la source
8

Ce n'est jamais nécessaire. Quant à savoir quand vous devriez - vous allez avoir beaucoup de réponses différentes à ce sujet. Je dirais pas du tout jusqu'à ce que ce soit en fait une partie acceptée de la norme et bien soutenue par la majorité des principaux compilateurs de la même manière.

Au-delà de cela, ce sera un argument religieux. Je dirais personnellement que ne jamais - mettre le type de retour réel rend le code plus clair, est beaucoup plus facile pour la maintenance (je peux regarder la signature d'une fonction et savoir ce qu'elle renvoie par rapport au fait d'avoir à lire le code), et cela supprime la possibilité que vous pensez qu'il devrait renvoyer un type et le compilateur pense qu'un autre cause des problèmes (comme cela s'est produit avec chaque langage de script que j'ai jamais utilisé). Je pense que l'automobile était une énorme erreur et qu'elle causera des ordres de grandeur plus de douleur que d'aide. D'autres diront que vous devriez l'utiliser tout le temps, car cela correspond à leur philosophie de programmation. En tout cas, cela est hors de portée de ce site.

Gabe Sechan
la source
1
Je suis d'accord pour dire que des personnes d'horizons différents auront des opinions différentes à ce sujet, mais j'espère que cela aboutira à une conclusion C ++. Dans (presque) tous les cas, il est inexcusable d'abuser des fonctionnalités du langage pour essayer d'en faire un autre, comme utiliser #defines pour transformer C ++ en VB. Les fonctionnalités ont généralement un bon consensus dans l'état d'esprit du langage sur ce qui est une bonne utilisation et ce qui ne l'est pas, en fonction de ce à quoi les programmeurs de ce langage sont habitués. La même fonctionnalité peut être présente dans plusieurs langues, mais chacune a ses propres lignes directrices sur son utilisation.
chris
2
Certaines fonctionnalités ont un bon consensus. Beaucoup ne le font pas. Je connais beaucoup de programmeurs qui pensent que la majorité de Boost est une poubelle qui ne devrait pas être utilisée. Je connais aussi certains qui pensent que c'est la plus grande chose qui soit arrivée au C ++. Dans les deux cas, je pense qu'il y a des discussions intéressantes à avoir à ce sujet, mais c'est vraiment un exemple exact de l'option close comme non constructive sur ce site.
Gabe Sechan
2
Non, je ne suis pas du tout d'accord avec vous et je pense que autoc'est une pure bénédiction. Cela supprime beaucoup de redondance. Il est tout simplement pénible de répéter le type de retour parfois. Si vous souhaitez renvoyer un lambda, cela peut même être impossible sans stocker le résultat dans a std::function, ce qui peut entraîner une surcharge.
Yongwei Wu
1
Il y a au moins une situation où c'est presque entièrement nécessaire. Supposons que vous ayez besoin d'appeler des fonctions, puis de consigner les résultats avant de les renvoyer, et les fonctions n'ont pas nécessairement toutes le même type de retour. Si vous le faites via une fonction d'enregistrement qui prend les fonctions et leurs arguments en tant que paramètres, vous devrez déclarer le type de retour de la fonction de journalisation autoafin qu'il corresponde toujours le type de retour de la fonction passée.
Justin Time - Réintègre Monica le
1
[Il existe techniquement une autre façon de faire cela, mais il utilise la magie des modèles pour faire exactement la même chose, et a toujours besoin que la fonction de journalisation soit autopour le type de retour de fin.]
Justin Time - Réintégrer Monica le
7

Cela n'a rien à voir avec la simplicité de la fonction (comme le supposait une copie maintenant supprimée de cette question).

Soit le type de retour est fixe (ne pas utiliser auto), soit il dépend de manière complexe d'un paramètre de modèle (à utiliser autodans la plupart des cas, associé à decltypelorsqu'il y a plusieurs points de retour).

Ben Voigt
la source
3

Prenons un environnement de production réel: de nombreuses fonctions et tests unitaires sont tous interdépendants du type de retour de foo(). Supposons maintenant que le type de retour doive changer pour une raison quelconque.

Si le type de retour est autopartout et que les appelants foo()et les fonctions associées l'utilisent autolors de l'obtention de la valeur retournée, les modifications qui doivent être apportées sont minimes. Sinon, cela pourrait signifier des heures de travail extrêmement fastidieux et sujet aux erreurs.

À titre d'exemple dans le monde réel, on m'a demandé de changer un module de l'utilisation de pointeurs bruts partout en pointeurs intelligents. La correction des tests unitaires était plus pénible que le code réel.

Bien qu'il existe d'autres façons de gérer cela, l'utilisation de autotypes de retour semble être une bonne solution.

Jim Wood
la source
1
Sur ces cas, ne serait-il pas préférable d'écrire decltype(foo())?
Oren S
Personnellement, je pense que typedefles déclarations s et alias (c'est-à-dire la usingdéclaration de type) sont meilleures dans ces cas, en particulier lorsqu'elles ont une portée de classe.
MAChitgarha
3

Je veux donner un exemple où le type de retour automatique est parfait:

Imaginez que vous vouliez créer un alias court pour un long appel de fonction ultérieur. Avec auto, vous n'avez pas besoin de vous occuper du type de retour d'origine (peut-être que cela changera à l'avenir) et l'utilisateur peut cliquer sur la fonction d'origine pour obtenir le type de retour réel:

inline auto CreateEntity() { return GetContext()->GetEntityManager()->CreateEntity(); }

PS: Cela dépend de cette question.

kaiser
la source
2

Pour le scénario 3, je retournerais le type de retour de signature de fonction avec la variable locale à renvoyer. Cela rendrait plus clair pour les programmeurs clients que la fonction retourne. Comme ça:

Scénario 3 Pour éviter la redondance:

std::vector<std::map<std::pair<int, double>, int>> foo() {
    decltype(foo()) ret;
    return ret;
}

Oui, il n'a pas de mot-clé auto mais le principe est le même pour éviter la redondance et donner plus de facilité aux programmeurs qui n'ont pas accès à la source.

Servir Laurijssen
la source
2
OMI, la meilleure solution pour cela est de fournir un nom spécifique au domaine pour le concept de tout ce qui est destiné à être représenté comme un vector<map<pair<int,double>,int>, puis de l'utiliser.
davidbak le