Comment utiliser std :: optionnel?

134

Je lis la documentation de std::experimental::optionalet j'ai une bonne idée de ce qu'il fait, mais je ne comprends pas quand je devrais l'utiliser ou comment je devrais l'utiliser. Le site ne contient pas encore d'exemples, ce qui me laisse plus de mal à saisir le vrai concept de cet objet. Quand est-il std::optionalun bon choix à utiliser et comment compense-t-il ce qui n'était pas trouvé dans la norme précédente (C ++ 11).

0x499602D2
la source
19
Les docs boost.optional pourraient nous éclairer.
juanchopanza
On dirait que std :: unique_ptr peut généralement servir les mêmes cas d'utilisation. Je suppose que si vous avez un truc contre nouveau, alors facultatif pourrait être préférable, mais il me semble que (les développeurs | applications) avec ce genre de vue sur le nouveau sont dans une petite minorité ... AFAICT, facultatif n'est PAS une bonne idée. À tout le moins, la plupart d'entre nous pourraient vivre confortablement sans cela. Personnellement, je serais plus à l'aise dans un monde où je n'ai pas à choisir entre unique_ptr et facultatif. Appelez-moi fou, mais Zen of Python a raison: qu'il y ait une bonne façon de faire quelque chose!
allyourcode
19
Nous ne voulons pas toujours allouer quelque chose sur le tas qui doit être supprimé, donc aucun unique_ptr n'est pas un substitut à optionnel.
Krum
5
@allyourcode Aucun pointeur ne remplace optional. Imaginez que vous vouliez un optional<int>ou même <char>. Pensez-vous vraiment que c'est "Zen" d'être obligé d'allouer, de déréférencer et de supprimer dynamiquement - quelque chose qui autrement ne nécessiterait aucune allocation et s'insérerait de manière compacte sur la pile, et peut-être même dans un registre?
underscore_d
1
Une autre différence, résultant sans aucun doute de l'allocation pile / tas de la valeur contenue, est que l'argument modèle ne peut pas être un type incomplet dans le cas de std::optional(alors qu'il peut l'être pour std::unique_ptr). Plus précisément, la norme exige que T [...] satisfasse aux exigences de Destructible .
dan_din_pantelimon

Réponses:

173

L'exemple le plus simple auquel je puisse penser:

std::optional<int> try_parse_int(std::string s)
{
    //try to parse an int from the given string,
    //and return "nothing" if you fail
}

La même chose peut être accomplie avec un argument de référence à la place (comme dans la signature suivante), mais l'utilisation std::optionalrend la signature et l'utilisation plus agréables.

bool try_parse_int(std::string s, int& i);

Une autre façon de procéder est particulièrement mauvaise :

int* try_parse_int(std::string s); //return nullptr if fail

Cela nécessite une allocation de mémoire dynamique, se soucier de la propriété, etc. - préférez toujours l'une des deux autres signatures ci-dessus.


Un autre exemple:

class Contact
{
    std::optional<std::string> home_phone;
    std::optional<std::string> work_phone;
    std::optional<std::string> mobile_phone;
};

C'est extrêmement préférable à la place d'avoir quelque chose comme un std::unique_ptr<std::string>pour chaque numéro de téléphone! std::optionalvous donne la localité des données, ce qui est idéal pour les performances.


Un autre exemple:

template<typename Key, typename Value>
class Lookup
{
    std::optional<Value> get(Key key);
};

Si la recherche ne contient pas une certaine clé, nous pouvons simplement renvoyer «aucune valeur».

Je peux l'utiliser comme ceci:

Lookup<std::string, std::string> location_lookup;
std::string location = location_lookup.get("waldo").value_or("unknown");

Un autre exemple:

std::vector<std::pair<std::string, double>> search(
    std::string query,
    std::optional<int> max_count,
    std::optional<double> min_match_score);

Cela a beaucoup plus de sens que, disons, avoir quatre surcharges de fonctions qui prennent toutes les combinaisons possibles de max_count(ou non) et min_match_score(ou pas)!

Il élimine également le maudit "Passer -1pour max_countsi vous ne voulez pas de limite" ou "Passer std::numeric_limits<double>::min()pour min_match_scoresi vous ne voulez pas un score minimum"!


Un autre exemple:

std::optional<int> find_in_string(std::string s, std::string query);

Si la chaîne de requête n'est pas incluse s, je veux "non int" - pas la valeur spéciale que quelqu'un a décidé d'utiliser à cette fin (-1?).


Pour des exemples supplémentaires, vous pouvez consulter la boost::optional documentation . boost::optionalet std::optionalsera fondamentalement identique en termes de comportement et d'utilisation.

Timothy Shields
la source
13
@gnzlbg std::optional<T>est juste un Tet un bool. Les implémentations des fonctions membres sont extrêmement simples. Les performances ne devraient pas vraiment être un problème lors de son utilisation - il y a des moments où quelque chose est facultatif, auquel cas c'est souvent l'outil approprié pour le travail.
Timothy Shields
8
@TimothyShields std::optional<T>est beaucoup plus complexe que cela. Il utilise le placement newet bien d'autres choses avec un alignement et une taille appropriés afin d'en faire un type littéral (c'est-à-dire utiliser avec constexpr) entre autres. La naïve Tet l' boolapproche échoueraient assez rapidement.
Rapptz
15
@Rapptz Line 256: union storage_t { unsigned char dummy_; T value_; ... }Line 289: struct optional_base { bool init_; storage_t<T> storage_; ... }Comment n'est-ce pas "a Tet a bool"? Je suis tout à fait d'accord que la mise en œuvre est très délicate et non triviale, mais conceptuellement et concrètement, le type est a Tet a bool. "La naïve Tet l' boolapproche échoueraient assez rapidement." Comment pouvez-vous faire cette déclaration en regardant le code?
Timothy Shields
12
@Rapptz stocke toujours un booléen et l'espace pour un int. L'union n'est là que pour que le optionnel ne construise pas un T s'il n'est pas réellement voulu. C'est toujours struct{bool,maybe_t<T>}le syndicat est juste là pour ne pas faire struct{bool,T}ce qui construirait un T dans tous les cas.
PeterT
12
@allyourcode Très bonne question. Les deux std::unique_ptr<T>et std::optional<T>remplissent en un certain sens le rôle d '«facultatif T». Je décrirais la différence entre eux comme des "détails d'implémentation": allocations supplémentaires, gestion de la mémoire, localité des données, coût du déplacement, etc. Je ne l'aurais jamais fait std::unique_ptr<int> try_parse_int(std::string s);par exemple, car cela provoquerait une allocation pour chaque appel même s'il n'y a aucune raison de . Je n'aurais jamais une classe avec un std::unique_ptr<double> limit;- pourquoi faire une allocation et perdre une localité de données?
Timothy Shields
35

Un exemple est cité dans le nouveau document adopté: N3672, std :: facultatif :

 optional<int> str2int(string);    // converts int to string if possible

int get_int_from_user()
{
     string s;

     for (;;) {
         cin >> s;
         optional<int> o = str2int(s); // 'o' may or may not contain an int
         if (o) {                      // does optional contain a value?
            return *o;                  // use the value
         }
     }
}
taocp
la source
13
Parce que vous pouvez transmettre les informations indiquant si vous avez intou non une hiérarchie d'appel ascendante ou descendante, au lieu de transmettre une valeur «fantôme» «supposée» avoir une signification «erreur».
Luis Machuca
1
@Wiz C'est en fait un excellent exemple. Il (A) permet d' str2int()implémenter la conversion comme il le souhaite, (B) quelle que soit la méthode d'obtention du string s, et (C) transmet tout son sens via la méthode au optional<int>lieu d'une stupide boolméthode basée sur les nombres magiques, les références ou l'allocation dynamique. je le fais.
underscore_d
10

mais je ne comprends pas quand je devrais l'utiliser ou comment je devrais l'utiliser.

Tenez compte du fait que vous écrivez une API et que vous souhaitez exprimer que la valeur «ne pas avoir de retour» n'est pas une erreur. Par exemple, vous devez lire des données à partir d'un socket, et lorsqu'un bloc de données est complet, vous l'analysez et le renvoyez:

class YourBlock { /* block header, format, whatever else */ };

std::optional<YourBlock> cache_and_get_block(
    some_socket_object& socket);

Si les données ajoutées ont complété un bloc analysable, vous pouvez le traiter; sinon, continuez à lire et à ajouter des données:

void your_client_code(some_socket_object& socket)
{
    char raw_data[1024]; // max 1024 bytes of raw data (for example)
    while(socket.read(raw_data, 1024))
    {
        if(auto block = cache_and_get_block(raw_data))
        {
            // process *block here
            // then return or break
        }
        // else [ no error; just keep reading and appending ]
    }
}

Edit: concernant le reste de vos questions:

Quand std :: optionnel est-il un bon choix à utiliser

  • Lorsque vous calculez une valeur et que vous devez la renvoyer, la sémantique est meilleure pour renvoyer par valeur que pour prendre une référence à une valeur de sortie (qui peut ne pas être générée).

  • Lorsque vous voulez vous assurer que le code client doit vérifier la valeur de sortie (celui qui écrit le code client peut ne pas rechercher d'erreur - si vous essayez d'utiliser un pointeur non initialisé, vous obtenez un vidage de mémoire; si vous essayez d'utiliser un initialized std :: optional, vous obtenez une exception catchable).

[...] et comment compense-t-il ce qui n'a pas été trouvé dans le précédent Standard (C ++ 11).

Avant C ++ 11, vous deviez utiliser une interface différente pour les «fonctions qui peuvent ne pas renvoyer une valeur» - soit retourner par pointeur et vérifier NULL, soit accepter un paramètre de sortie et renvoyer un code d'erreur / résultat pour «non disponible ".

Les deux imposent un effort et une attention supplémentaires de la part de l'implémenteur client pour bien faire les choses et les deux sont source de confusion (le premier poussant l'implémenteur client à penser à une opération comme une allocation et exigeant que le code client implémente une logique de gestion de pointeur et le second permettant code client pour éviter d'utiliser des valeurs invalides / non initialisées).

std::optional s'occupe bien des problèmes liés aux solutions précédentes.

utnapistim
la source
Je sais qu'ils sont fondamentalement les mêmes, mais pourquoi les utilisez-vous à la boost::place std::?
0x499602D2
4
Merci - je l'ai corrigé (je l'ai utilisé boost::optionalcar après environ deux ans d'utilisation, il est codé en dur dans mon cortex pré-frontal).
utnapistim
1
Cela me semble être un mauvais exemple, car si plusieurs blocs étaient terminés, un seul pourrait être retourné et le reste rejeté. La fonction devrait à la place retourner une collection de blocs éventuellement vide.
Ben Voigt
Vous avez raison; c'était un mauvais exemple; J'ai modifié mon exemple pour "analyser le premier bloc si disponible".
utnapistim
4

J'utilise souvent des options pour représenter des données optionnelles extraites de fichiers de configuration, c'est-à-dire où ces données (comme avec un élément attendu, mais pas nécessaire, dans un document XML) sont éventuellement fournies, de sorte que je puisse montrer explicitement et clairement si les données étaient effectivement présentes dans le document XML. Surtout quand les données peuvent avoir un état "non défini", par rapport à un état "vide" et un état "défini" (logique floue). Avec un optionnel, set and not set est clair, également vide serait clair avec la valeur 0 ou null.

Cela peut montrer comment la valeur de "non défini" n'est pas équivalente à "vide". Dans le concept, un pointeur vers un int (int * p) peut le montrer, où un nul (p == 0) n'est pas défini, une valeur de 0 (* p == 0) est définie et vide, et toute autre valeur (* p <> 0) est défini sur une valeur.

Pour un exemple pratique, j'ai un morceau de géométrie extrait d'un document XML qui avait une valeur appelée indicateurs de rendu, où la géométrie peut soit remplacer les indicateurs de rendu (ensemble), désactiver les indicateurs de rendu (définis sur 0), ou tout simplement pas affectent les indicateurs de rendu (non définis), une option serait un moyen clair de représenter cela.

Clairement, un pointeur vers un int, dans cet exemple, peut atteindre l'objectif, ou mieux, un pointeur de partage car il peut offrir une implémentation plus propre, cependant, je dirais qu'il s'agit de clarté du code dans ce cas. Un null est-il toujours un "non défini"? Avec un pointeur, ce n'est pas clair, car nul signifie littéralement non alloué ou créé, bien que cela puisse , mais ne signifie pas nécessairement «non défini». Il convient de souligner qu'un pointeur doit être libéré, et dans les bonnes pratiques mis à 0, cependant, comme avec un pointeur partagé, un optionnel ne nécessite pas de nettoyage explicite, donc il n'y a pas de souci de mélanger le nettoyage avec l'option n'ayant pas été définie.

Je pense que c'est une question de clarté du code. La clarté réduit le coût de la maintenance et du développement du code. Une compréhension claire de l'intention du code est extrêmement précieuse.

L'utilisation d'un pointeur pour représenter cela nécessiterait de surcharger le concept du pointeur. Pour représenter «null» comme «non défini», vous pouvez généralement voir un ou plusieurs commentaires dans le code pour expliquer cette intention. Ce n'est pas une mauvaise solution au lieu d'une option facultative, cependant, j'opte toujours pour une implémentation implicite plutôt que des commentaires explicites, car les commentaires ne sont pas exécutoires (comme par compilation). Des exemples de ces éléments implicites pour le développement (les articles en développement qui sont fournis uniquement pour appliquer l'intention) incluent les divers casts de style C ++, "const" (en particulier sur les fonctions membres) et le type "bool", pour n'en nommer que quelques-uns. On peut dire que vous n'avez pas vraiment besoin de ces fonctionnalités de code, tant que tout le monde obéit aux intentions ou aux commentaires.

Kit10
la source