Recherche…


introduction

Les options (également appelés types Maybe) sont utilisés pour représenter un type dont le contenu peut ou non être présent. Ils sont implémentés en C ++ 17 en tant que classe std::optional . Par exemple, un objet de type std::optional<int> peut contenir une valeur de type int ou ne contenir aucune valeur.

Les options sont généralement utilisées pour représenter une valeur qui n'existe pas ou comme type de retour à partir d'une fonction qui peut ne pas renvoyer un résultat significatif.

Autres approches optionnelles

Il y a beaucoup d'autres approches pour résoudre le problème que std::optional résout, mais aucune n'est assez complète: utiliser un pointeur, utiliser une sentinelle ou utiliser une pair<bool, T> .

Facultatif vs pointeur

Dans certains cas, nous pouvons fournir un pointeur vers un objet existant ou nullptr pour indiquer un échec. Mais ceci est limité aux cas où des objets existent déjà - optional , en tant que type de valeur, peut également être utilisé pour renvoyer de nouveaux objets sans avoir recours à l'allocation de mémoire.

Facultatif vs Sentinel

Un idiome commun consiste à utiliser une valeur spéciale pour indiquer que la valeur n'a pas de sens. Cela peut être 0 ou -1 pour les types intégraux, ou nullptr pour les pointeurs. Cependant, cela réduit l'espace des valeurs valides (vous ne pouvez pas différencier un 0 valide et un 0 sans signification) et de nombreux types n'ont pas le choix naturel pour la valeur sentinelle.

Facultatif vs std::pair<bool, T>

Un autre idiome commun est de fournir une paire, où l'un des éléments est un bool indiquant si la valeur est significative ou non.

Cela suppose que le type de valeur soit default-constructible en cas d'erreur, ce qui n'est pas possible pour certains types et possible mais indésirable pour d'autres. Un optional<T> , en cas d'erreur, n'a pas besoin de construire quoi que ce soit.

Utiliser des options pour représenter l'absence de valeur

Avant C ++ 17, avoir des pointeurs avec une valeur nullptr représentait généralement l'absence d'une valeur. C'est une bonne solution pour les objets volumineux qui ont été alloués dynamiquement et qui sont déjà gérés par des pointeurs. Cependant, cette solution ne fonctionne pas bien pour les types petits ou primitifs tels que int , qui sont rarement alloués ou gérés de manière dynamique par des pointeurs. std::optional fournit une solution viable à ce problème commun.

Dans cet exemple, struct Person est défini. Il est possible pour une personne d'avoir un animal de compagnie, mais pas nécessaire. Par conséquent, le membre pet de Person est déclaré avec un wrapper std::optional .

#include <iostream>
#include <optional>
#include <string>

struct Animal {
    std::string name;
};

struct Person {
    std::string name;
    std::optional<Animal> pet;
};

int main() {
    Person person;
    person.name = "John";

    if (person.pet) {
        std::cout << person.name << "'s pet's name is " <<
            person.pet->name << std::endl;
    }
    else {
        std::cout << person.name << " is alone." << std::endl;
    }
}

Utiliser des options pour représenter l'échec d'une fonction

Avant C ++ 17, une fonction représentait généralement l'échec de plusieurs manières:

  • Un pointeur nul a été renvoyé.
    • Par exemple, appeler une fonction Delegate *App::get_delegate() sur une instance d' App n'ayant pas de délégué renverrait nullptr .
    • C'est une bonne solution pour les objets qui ont été alloués dynamiquement ou qui sont grands et gérés par des pointeurs, mais qui ne sont pas une bonne solution pour les petits objets qui sont généralement affectés par pile et transmis par copie.
  • Une valeur spécifique du type de retour était réservée pour indiquer une défaillance.
    • Par exemple, appeler une fonction unsigned shortest_path_distance(Vertex a, Vertex b) sommet a, sommet unsigned shortest_path_distance(Vertex a, Vertex b) sur deux sommets non connectés peut renvoyer zéro pour indiquer ce fait.
  • La valeur a été associée à une valeur bool pour indiquer que la valeur renvoyée était significative.
    • Par exemple, appeler une fonction std::pair<int, bool> parse(const std::string &str) avec un argument de chaîne qui n'est pas un entier renverrait une paire avec un int indéfini et un bool défini sur false .

Dans cet exemple, John reçoit deux animaux, Fluffy et Furball. La fonction Person::pet_with_name() est ensuite appelée pour récupérer les Person::pet_with_name() John. Comme John n'a pas d'animal de compagnie nommé Whiskers, la fonction échoue et std::nullopt est renvoyé à la place.

#include <iostream>
#include <optional>
#include <string>
#include <vector>

struct Animal {
    std::string name;
};

struct Person {
    std::string name;
    std::vector<Animal> pets;
    
    std::optional<Animal> pet_with_name(const std::string &name) {
        for (const Animal &pet : pets) {
            if (pet.name == name) {
                return pet;
            }
        }
        return std::nullopt;
    }
};

int main() {
    Person john;
    john.name = "John";
    
    Animal fluffy;
    fluffy.name = "Fluffy";
    john.pets.push_back(fluffy);
    
    Animal furball;
    furball.name = "Furball";
    john.pets.push_back(furball);
    
    std::optional<Animal> whiskers = john.pet_with_name("Whiskers");
    if (whiskers) {
        std::cout << "John has a pet named Whiskers." << std::endl;
    }
    else {
        std::cout << "Whiskers must not belong to John." << std::endl;
    }
}

optionnel comme valeur de retour

std::optional<float> divide(float a, float b) {
  if (b!=0.f) return a/b;
  return {};
}

Ici, nous renvoyons soit la fraction a/b , mais si elle n’est pas définie (ce serait l’infini), nous renvoyons le vide facultatif.

Un cas plus complexe:

template<class Range, class Pred>
auto find_if( Range&& r, Pred&& p ) {
  using std::begin; using std::end;
  auto b = begin(r), e = end(r);
  auto r = std::find_if(b, e , p );
  using iterator = decltype(r);
  if (r==e)
    return std::optional<iterator>();
  return std::optional<iterator>(r);
}
template<class Range, class T>
auto find( Range&& r, T const& t ) {
  return find_if( std::forward<Range>(r), [&t](auto&& x){return x==t;} );
}

find( some_range, 7 ) recherche le conteneur ou la plage some_range pour quelque chose égal au nombre 7 . find_if fait avec un prédicat.

Il retourne soit une option vide si elle n'a pas été trouvée, soit une option contenant un itérateur à l'élément si c'était le cas.

Cela vous permet de faire:

if (find( vec, 7 )) {
  // code
}

ou même

if (auto oit = find( vec, 7 )) {
  vec.erase(*oit);
}

sans avoir à jouer avec les itérateurs de début / fin et les tests.

valeur_ou

void print_name( std::ostream& os, std::optional<std::string> const& name ) {
  std::cout "Name is: " << name.value_or("<name missing>") << '\n';
}

value_or renvoie soit la valeur stockée dans le value_or facultatif, soit l'argument s'il n'y a rien dans la mémoire.

Cela vous permet de prendre la valeur facultative-null et de donner un comportement par défaut lorsque vous avez réellement besoin d'une valeur. En procédant de cette façon, la décision "comportement par défaut" peut être repoussée au point où il est préférable et immédiatement nécessaire, au lieu de générer une valeur par défaut au plus profond des entrailles de certains moteurs.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow