C++
std :: optionnel
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é renverraitnullptr
. - 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.
- Par exemple, appeler une fonction
- 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, sommetunsigned shortest_path_distance(Vertex a, Vertex b)
sur deux sommets non connectés peut renvoyer zéro pour indiquer ce fait.
- Par exemple, appeler une fonction
- 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 unint
indéfini et unbool
défini surfalse
.
- Par exemple, appeler une fonction
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.