45 votes

Pourquoi devrais-je préférer l'idiome "initialisateur explicitement typé" plutôt que de donner explicitement le type

J'ai récemment acheté le nouveau livre Effective modern C++ de Scott Meyers et je le lis actuellement. Mais je rencontre une chose qui me dérange totalement.

Au point 5, Scott dit qu'en utilisant auto est une excellente chose. Il permet de ne pas avoir à taper, vous donne dans la plupart des cas le type correct et il peut être immunisé contre les incompatibilités de type. Je comprends tout à fait cela et je pense à auto comme une bonne chose aussi.

Mais ensuite, au point 6, Scott dit que chaque pièce a deux côtés. Et de même, il pourrait y avoir des cas, où auto déduit un type totalement erroné, par exemple pour les objets proxy.

Vous connaissez peut-être déjà cet exemple :

class Widget;
std::vector<bool> features(Widget w);

Widget w;

bool priority = features(w)[5]; // this is fine

auto priority = features(w)[5]; // this result in priority being a proxy
                                // to a temporary object, which will result
                                // in undefined behavior on usage after that
                                // line

Jusqu'à présent, tout va bien.

Mais la solution de Scott à ce problème est ce qu'on appelle "l'idiome de l'initialisateur explicitement typé". L'idée est d'utiliser static_cast sur l'initialisateur comme ceci :

auto priority = static_cast<bool>(features(w)[5]);

Mais cela ne conduit pas seulement à plus de typage, mais vous indiquez aussi explicitement le type, qui devrait être déduit. En fait, vous avez perdu les deux avantages de auto sur un type explicite donné.

Quelqu'un peut-il me dire pourquoi il est avantageux d'utiliser cet idiome ?


Tout d'abord, pour clarifier les choses, mes questions visent à savoir pourquoi je dois écrire :

auto priority = static_cast<bool>(features(w)[5]);

au lieu de :

bool priority = features(w)[5];

@Sergey a indiqué un lien vers un bel article sur GotW sur ce sujet, ce qui répond en partie à ma question.

Ligne directrice : Pensez à déclarer des variables locales auto x = type{ expr } ; lorsque vous souhaitez vous engager explicitement dans un type. Cela permet de montrer de manière auto-documentée que le code demande explicitement une conversion, cela garantit que la variable sera initialisée, et cela ne permettra pas une conversion implicite accidentelle de restriction. Ce n'est que lorsque vous souhaitez un rétrécissement explicite que vous devez utiliser ( ) au lieu de { }.

Ce qui m'amène à une question connexe. Lesquelles de ces quatre Quelles alternatives dois-je choisir ?

bool priority = features(w)[5];

auto priority = static_cast<bool>(features(w)[5]);

auto priority = bool(features(w)[5]);

auto priority = bool{features(w)[5]};

Le numéro un est toujours mon préféré. Il est moins typographique et aussi explicite que les trois autres.

L'argument de l'initialisation garantie ne tient pas vraiment, puisque je déclare de toute façon des variables avant de pouvoir les initialiser d'une manière ou d'une autre. Et l'autre argument concernant le rétrécissement n'a pas bien fonctionné lors d'un test rapide (cf. http://ideone.com/GXvIIr ).

4 votes

J'ai lu ses livres (pas celui-ci), et je doute qu'il n'ait pas donné d'explications.

6 votes

@Niall : D'après ce que je comprends, la question n'est pas de savoir pourquoi c'est nécessaire, mais pourquoi mettre le type en static_cast plutôt qu'à la place de auto

0 votes

@PiotrS. C'est exactement ce que je voulais dire.

26voto

Piotr S. Points 9759

Suivre la norme C++ :

§ 8.5 Initialisateurs [dcl.init]

  1. L'initialisation qui se produit dans la forme

    T x = a;

    ainsi que dans le passage des arguments, le retour de la fonction, le lancement d'une exception (15.1), la gestion d'une exception (15.3) et l'initialisation des membres de l'agrégat (8.5.1). copier-initialisation .

Je peux penser à l'exemple donné dans le livre :

auto x = features(w)[5];

comme celle qui représente toute forme de copier-initialisation avec le type auto / modèle ( type déduit en général), tout comme :

template <typename A>
void foo(A x) {}

foo(features(w)[5]);

ainsi que :

auto bar()
{
    return features(w)[5];
}

ainsi que :

auto lambda = [] (auto x) {};
lambda(features(w)[5]);

Donc le fait est que nous ne pouvons pas toujours _"déplacer le type T de static_cast<T> à la partie gauche de l'affectation"_ .

Au contraire, dans tous les exemples ci-dessus, nous devons spécifier explicitement le type désiré plutôt que de laisser le compilateur en déduire un par lui-même, si ce dernier peut conduire à comportement indéfini :

Respectivement à mes exemples, ce serait :

/*1*/ foo(static_cast<bool>(features(w)[5]));

/*2*/ return static_cast<bool>(features(w)[5]);

/*3*/ lambda(static_cast<bool>(features(w)[5]));

Ainsi, l'utilisation de static_cast<T> est une manière élégante de forcer un type désiré, qui peut être exprimé de manière alternative par un appel explicite au contructeur :

foo(bool{features(w)[5]});

Pour résumer, je ne pense pas que le livre dise :

Chaque fois que vous voulez forcer le type d'une variable, utilisez auto x = static_cast<T>(y); au lieu de T x{y}; .

Pour moi, ça ressemble plus à un mot d'avertissement :

L'inférence de type avec auto est cool, mais peut aboutir à un comportement indéfini s'il est utilisé de manière imprudente.

Et comme solution pour les scénarios impliquant une déduction de type il est proposé ce qui suit :

Si le mécanisme régulier de déduction de type du compilateur n'est pas ce que vous voulez, utilisez static_cast<T>(y) .


UPDATE

Et pour répondre à votre nouvelle question, Quelle est l'initialisation à privilégier parmi les suivantes :

bool priority = features(w)[5];

auto priority = static_cast<bool>(features(w)[5]);

auto priority = bool(features(w)[5]);

auto priority = bool{features(w)[5]};

Scénario 1

D'abord, imaginez le std::vector<bool>::reference est pas implicitement convertible en bool :

struct BoolReference
{
    explicit operator bool() { /*...*/ }
};

Maintenant, le bool priority = features(w)[5]; sera ne pas compiler car il ne s'agit pas d'un contexte booléen explicite. Les autres fonctionneront sans problème (tant que l'option operator bool() est accessible).

Scénario 2

Deuxièmement, supposons que le std::vector<bool>::reference est mis en œuvre dans un vieille méthode et bien que le opérateur de conversion n'est pas explicit il renvoie int à la place :

struct BoolReference
{
    operator int() { /*...*/ }
};

Le changement de signature s'éteint le site auto priority = bool{features(w)[5]}; l'initialisation, comme l'utilisation de {} empêche le rétrécissement (qui convertit un int à bool est).

Scénario 3

Troisièmement, et si nous ne parlions pas de bool du tout, mais sur certains défini par l'utilisateur qui, à notre grande surprise, déclare explicit constructeur :

struct MyBool
{
    explicit MyBool(bool b) {}
};

De manière surprenante, une fois de plus, le MyBool priority = features(w)[5]; l'initialisation sera ne pas compiler car la syntaxe d'initialisation de la copie nécessite un constructeur non explicite. Les autres fonctionnent cependant.

Attitude personnelle

Si je devais choisir une initialisation parmi les quatre candidates citées, je choisirais :

auto priority = bool{features(w)[5]};

parce qu'il introduit un contexte booléen explicite (ce qui est bien dans le cas où nous voulons assigner cette valeur à une variable booléenne) et empêche le rétrécissement (dans le cas d'autres types, difficilement convertibles en bool), de sorte que lorsqu'une erreur/alerte est déclenchée, nous pouvons diagnostiquer ce qui est à l'origine de l'erreur. features(w)[5] est vraiment .


MISE À JOUR 2

J'ai récemment regardé le discours d'Herb Sutter de CppCon 2014 intitulé Retour aux sources ! L'essentiel du style C++ moderne où il présente quelques points sur les raisons pour lesquelles il faut préférer le initialisateur de type explicite de auto x = T{y}; (bien que ce ne soit pas la même chose qu'avec le formulaire auto x = static_cast<T>(y) (tous les arguments ne s'appliquent donc pas) sur T x{y}; qui sont :

  1. auto Les variables doivent toujours être initialisées. Autrement dit, vous ne pouvez pas écrire auto a; tout comme vous pouvez écrire des erreurs int a;

  2. Le site C++ moderne préfère le type sur le côté droit, tout comme dans :

    a) Les littéraux :

    auto f = 3.14f;
    //           ^ float

    b) Les littéraux définis par l'utilisateur :

    auto s = "foo"s;
    //            ^ std::string

    c) Les déclarations de fonctions :

    auto func(double) -> int;

    d) lambdas nommés :

    auto func = [=] (double) {};

    e) Alias :

    using dict = set<string>;

    f) Alias de modèles :

    template <class T>
    using myvec = vector<T, myalloc>;

    en tant que tel en ajoutant un autre :

    auto x = T{y};

    est cohérent avec le style où nous avons le nom sur le côté gauche, et le type avec initialisateur sur le côté droit, ce qui peut être brièvement décrit comme :

    <category> name = <type> <initializer>;
  3. Avec la copie-élision et les constructeurs non explicites de copie/déplacement, il a coût nul par rapport à T x{y} la syntaxe.

  4. Elle est plus explicite lorsqu'il existe des différences subtiles entre les types :

     unique_ptr<Base> p = make_unique<Derived>(); // subtle difference
    
     auto p = unique_ptr<Base>{make_unique<Derived>()}; // explicit and clear
  5. {} garantit l'absence de conversions implicites et de rétrécissement.

Mais il mentionne également certains inconvénients de la auto x = T{} en général, qui a déjà été décrit dans ce post :

  1. Même si le compilateur peut élider le temporaire du côté droit, il faut un copieur-constructeur accessible, non supprimé et non explicite :

     auto x = std::atomic<int>{}; // fails to compile, copy constructor deleted
  2. Si l'élision n'est pas activée (par ex. -fno-elide-constructors ), alors le déplacement des types non mobiles entraîne une copie coûteuse :

     auto a = std::array<int,50>{};

0 votes

Merci pour votre contribution, dans ces cas, je vois certains avantages à utiliser static_cast, mais je ne vois pas en quoi c'est lié à ma question initiale ? Je veux dire, ce n'est pas parce qu'il y a aussi un auto Quelque part, cela ne signifie pas que vous devez obéir aux mêmes règles.

1 votes

@Mario : parce que les mêmes règles de déduction du type sont appliquées.

0 votes

@Mario : Je ne pense pas que le livre dise : "Chaque fois que vous voulez forcer le type d'une variable, utilisez auto x = static_cast<T>(y); au lieu de T x{y}; ". Pour moi, il s'agit simplement d'un indice que le livre donne pour forcer le type désiré, en sautant le mécanisme normal de déduction de type.

15voto

Ben Voigt Points 151460

Je n'ai pas le livre devant moi, donc je ne peux pas dire s'il y a plus de contexte.

Mais pour répondre à votre question, non, utiliser auto + static_cast dans cet exemple particulier n'est pas une bonne solution. Elle viole une autre directive (une pour laquelle je n'ai jamais vu d'exceptions justifiées) :

  • Utilisez la fonte/conversion la plus faible possible.

Les casts inutilement forts subvertissent le système de types et empêchent le compilateur de générer des messages de diagnostic au cas où un changement se produirait ailleurs dans le programme et affecterait la conversion d'une manière incompatible. (action à distance, le croque-mitaine de la programmation de maintenance)

Ici, le static_cast est inutilement forte. Une conversion implicite fera l'affaire. Évitez donc le cast.

0 votes

J'aime beaucoup votre réponse, mais j'ai l'impression que la réponse de @Piotr S. va plus dans le sens de ce qui est dit dans le livre.

0 votes

@Mario : L'appel explicite au constructeur T var{expression} est en effet meilleur, parce qu'il ne défait pas le système de type aussi fortement que static_cast . Et, dans la mesure du possible, T var = expression; peut être préféré parce qu'il est encore plus faible, bien qu'autoriser inutilement l'invocation explicite d'un constructeur soit sans doute beaucoup moins problématique que l'introduction d'un cast.

8voto

Sergey Points 2692

Contexte du livre :

Bien que std::vector<bool> tient conceptuellement bool s, operator[] pour std::vector<bool> ne renvoie pas une référence à un élément du conteneur (ce qui est le cas de la fonction std::vector::operator[] pour tous les types sauf bool ). Au lieu de cela, il renvoie un objet de type std::vector<bool>::reference (une classe imbriquée dans std::vector<bool> ).

Il n'y a pas d'avantage, il s'agit plutôt d'une prévention des erreurs, lorsque vous utilisez auto avec une bibliothèque externe.

Je pense que c'est l'idée principale de cet idiome. Vous devez être explicite et forcer auto à se comporter correctement.

BTW, ici le bel article sur GotW sur l'automobile.

2 votes

Bien, std::vector<T>::operator[] toujours renvoie un std::vector<T>::reference ; il se trouve que std::vector<bool>::reference n'est pas bool& :) (remplacer par const_reference y bool const& para std::vector<T>::operator[] const ).

3voto

utnapistim Points 12060

Quelqu'un peut-il me dire pourquoi il est avantageux d'utiliser cet idiome ?

La raison à laquelle je pense : parce que c'est explicite. Considérez comment vous liriez (instinctivement) ce code (c'est-à-dire, sans savoir ce que features fait) :

bool priority = features(w)[5];

"Features" renvoie une séquence indexable de certaines valeurs "booléennes" génériques ; nous lisons la cinquième dans le fichier priority ".

auto priority = static_cast<bool>(features(w)[5]);

"Features renvoie une séquence indexable de valeurs explicitement convertibles en bool ; nous lisons le cinquième dans priority ".

Ce code n'est pas écrit pour optimiser le code flexible le plus court, mais pour l'explicitation du résultat (et apparemment la cohérence - puisque je suppose que ce ne serait pas la seule variable déclarée avec auto).

L'utilisation de auto dans la déclaration de priority permet de garder le code flexible, quelle que soit l'expression du côté droit.

Cela dit, je préférerais la version sans casting explicite.

0 votes

Sauf que (par parallélisme avec la seconde affirmation) la première est "valeurs compatibles avec bool", pas nécessairement déjà bool. Ou bien était-ce le point que vous vouliez faire passer en utilisant "boolean" contre "bool" ? bool ?

0 votes

"L'utilisation de auto dans la déclaration de priorité a pour but de garder le code flexible à n'importe quelle expression du côté droit. auto produire un type autre que bool lorsqu'il est initialisé par une expression static_cast<bool>(...) ? J'ai envie de dire "Bien sûr que non", auquel cas il n'y a pas d'avantage à choisir auto mais le C++ présente des aspects étranges.

0 votes

@j_random_hacker, la vérité est que je ne suis pas tout à fait convaincu moi-même (c'est-à-dire que je déclarerais probablement la variable en tant que bool et d'en finir). L'un des avantages de conserver le static_cast (Je viens d'y penser) : Lors du refactoring, le fait de voir le cast explicite peut être une indication que l'appel à features doit être extraite dans une fonction renvoyant bool (surtout si la construction se répète dans le code du client).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X