web-dev-qa-db-fra.com

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

Avec 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 devrais-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 chaque fois que possible. Chaque fonction pouvant être écrite de cette façon devrait l'être. Le problème 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. À 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 pense pas que cela pose vraiment un problème, même si le type de retour dépend 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}; à la place d'un vecteur, mais cela ne marche pas toujours et nous devons spécifier le type à la fois dans l'en-tête et le corps de la fonction. Ceci est purement redondant et la déduction de type de retour automatique 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 si elle n’est pas fournie ici, nous devons passer à 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 réellement ê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 fonction pour qu’elle ne me morde pas plus tard? Y at-il quelque chose de nouveau que cette fonctionnalité apporte à la table qui ne serait pas possible sans elle?

Notez que les multiples questions sont censées aider à trouver des perspectives pour répondre à cette question.

122
chris

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

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

Désolé, mais vous n'allez pas résoudre ce problème en énonçant des règles générales. Vous devez examiner un code particulier et décider vous-même s'il est plus facile de spécifier des types partout: est-il préférable que votre code indique «le type de cette chose est X» ou est-il préférable pour votre code pour dire, "le type de cette chose n'est pas pertinent pour comprendre cette partie du code: le compilateur doit savoir et nous pourrions probablement le résoudre mais nous n'avons pas besoin de le dire ici"?

Étant donné que la "lisibilité" n'est pas définie objectivement [*] et qu'elle varie également d'un lecteur à l'autre, vous avez la 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 à dire que tout ce qui leur est inconnu est "moins lisible". Ainsi, la lisibilité d'une règle de style proposée 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) seront utiles au style de codage de quelqu'un. Personnellement, j'estime que le second cas d'utilisation est le plus convaincant, mais je prévois 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 documenter en tant que decltype(t+u) crée une interface publiée sur laquelle vous pouvez (espérons-le) compter.

[*] Parfois, quelqu'un essaie de prendre des mesures objectives. Dans la faible mesure où quiconque aboutit à des résultats statistiquement significatifs et généralement applicables, ils sont totalement ignorés par les programmeurs actifs, en faveur de l'instinct de ce qui est "lisible" de l'auteur.

54
Steve Jessop

D'une 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 agréable d'abandonner 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 . La fonction apply a pour objet de transférer les éléments d'un std::Tuple à une fonction et de renvoyer le résultat. Le int_seq et le make_int_seq ne sont qu’une partie de la mise en œuvre et ne feront probablement que dérouter tout utilisateur essayant de comprendre ce qu’il fait.

Comme vous pouvez le constater, le type de retour n'est rien d'autre qu'une decltype de l'expression renvoyé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 identique à celui de apply. 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é supprimé et remplacé par decltype(auto) dans la proposition d'ajouter apply à la norme, N3915 (notez également que ma réponse d'origine 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 savoir. Une bonne documentation avec des exemples sera beaucoup plus utile.


Autre chose qui n’a pas encore été mentionnée: while declype(t+u) permet d’utiliser l’expression SFINAE , decltype(auto) ne le permet pas (même si il existe une proposition pour changer ce comportement). Prenons par exemple une fonction foobar qui appellera la fonction membre foo d'un type si elle existe ou la fonction membre bar du type si elle existe et supposons qu'une classe a toujours exacty foo ou bar mais pas 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 de foobar sur une instance de X affichera foo tandis que l'appel de foobar sur une instance de Y affichera bar. Si vous utilisez plutôt la déduction de type de retour automatique (avec ou sans decltype(auto)), vous n'obtiendrez pas l'expression SFINAE et l'appel de foobar sur une instance de X ou Y ne provoquera pas d'erreur de compilation.

22
Morwenn

Ce n'est jamais nécessaire. Quant à savoir quand vous devriez, vous obtiendrez 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 ça, ça va être un argument religieux. Personnellement, je dirais que ne jamais utiliser le type de retour rend le code plus clair, est beaucoup plus facile à gérer (je peux regarder la signature d’une fonction et savoir ce qu’elle retourne par rapport à la lecture du code) vous pensez que cela devrait renvoyer un type et le compilateur en pense un autre, ce qui cause des problèmes (comme ce fut le cas avec tous les langages de script que j'ai utilisés). Je pense que l'automobile était une énorme erreur et que cela causerait des ordres de grandeur plus douloureux que de l'aide. D'autres diront que vous devriez l'utiliser tout le temps, car cela correspond à leur philosophie de programmation. En tout cas, ceci est hors de portée pour ce site.

7
Gabe Sechan

Cela n'a rien à voir avec la simplicité de la fonction (comme un dupliqué maintenant supprimé de cette question supposée).

Le type de retour est soit fixe (n'utilisez pas auto), soit dépend de manière complexe d'un paramètre de modèle (utilisez auto dans la plupart des cas, associé à decltype lorsqu'il existe plusieurs points de retour).

7
Ben Voigt

Considérons 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 doit changer pour une raison quelconque.

Si le type de retour est auto partout et que les appelants de foo() et des fonctions associées utilisent auto lors de l'obtention de la valeur renvoyée, les modifications à apporter sont minimes. Sinon, cela pourrait signifier des heures de travail extrêmement fastidieux et sujet aux erreurs.

En tant qu'exemple concret, on m'a demandé de remplacer un pointeur brut par un autre point par un pointeur intelligent. La réparation des tests unitaires était plus pénible que le code réel.

Bien que cela puisse être géré d’autres manières, l’utilisation des types de retour auto semble un bon choix.

3
Jim Wood

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

Imaginez que vous souhaitiez créer un court alias pour un appel de fonction long et ultérieur. Avec auto, vous n'avez pas besoin de vous occuper du type de retour d'origine (il changera peut-être ultérieurement) 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 cela question.

2
kaiser

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 .

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'y a pas de mot-clé auto, mais le principe est le même pour éviter la redondance et donner aux programmeurs qui n'ont pas accès à la source un meilleur moment.

0
Serve Laurijssen