C++
std :: opcional
Buscar..
Introducción
Los opcionales (también conocidos como tipos Maybe) se utilizan para representar un tipo cuyo contenido puede o no estar presente. Se implementan en C ++ 17 como la clase std::optional
. Por ejemplo, un objeto de tipo std::optional<int>
puede contener algún valor de tipo int
, o puede que no contenga ningún valor.
Los opcionales se utilizan comúnmente para representar un valor que puede no existir o como un tipo de retorno de una función que puede fallar en devolver un resultado significativo.
Otros enfoques opcionales
Hay muchos otros enfoques para resolver el problema que resuelve std::optional
, pero ninguno de ellos es completo: usar un puntero, usar un centinela o usar un pair<bool, T>
.
Opcional vs puntero
En algunos casos, podemos proporcionar un puntero a un objeto existente o nullptr
para indicar un error. Pero esto se limita a aquellos casos en los que ya existen objetos: optional
, como tipo de valor, también se puede usar para devolver nuevos objetos sin tener que recurrir a la asignación de memoria.
Opcional vs Sentinel
Un lenguaje común es usar un valor especial para indicar que el valor no tiene sentido. Esto puede ser 0 o -1 para tipos integrales, o nullptr
para punteros. Sin embargo, esto reduce el espacio de valores válidos (no puede diferenciar entre un 0 válido y un 0 sin sentido) y muchos tipos no tienen una opción natural para el valor centinela.
Opcional vs std::pair<bool, T>
Otro modismo común es proporcionar un par, donde uno de los elementos es un bool
indica si el valor es significativo o no.
Esto se basa en que el tipo de valor es constructible por defecto en el caso de error, que no es posible para algunos tipos y es posible pero no deseable para otros. Un optional<T>
, en el caso de error, no necesita construir nada.
Usando opcionales para representar la ausencia de un valor.
Antes de C ++ 17, tener punteros con un valor de nullptr
representaba comúnmente la ausencia de un valor. Esta es una buena solución para objetos grandes que han sido asignados dinámicamente y ya están gestionados por punteros. Sin embargo, esta solución no funciona bien para tipos pequeños o primitivos, como int
, que rara vez se asignan o administran dinámicamente por punteros. std::optional
proporciona una solución viable a este problema común.
En este ejemplo, se define struct Person
. Es posible que una persona tenga una mascota, pero no es necesario. Por lo tanto, el miembro pet
de Person
se declara con un envoltorio 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;
}
}
Usando opcionales para representar el fallo de una función.
Antes de C ++ 17, una función representaba el error de varias maneras:
- Se devolvió un puntero nulo.
- por ejemplo, llamar a una función
Delegate *App::get_delegate()
en una instancia de laApp
que no tenía un delegado devolveríanullptr
. - Esta es una buena solución para los objetos que han sido asignados dinámicamente o que son grandes y están administrados por punteros, pero no es una buena solución para los objetos pequeños que normalmente se asignan y se pasan copiando.
- por ejemplo, llamar a una función
- Se reservó un valor específico del tipo de retorno para indicar el fallo.
- por ejemplo, llamar a una función
unsigned shortest_path_distance(Vertex a, Vertex b)
en dos vértices que no están conectados puede devolver cero para indicar este hecho.
- por ejemplo, llamar a una función
- El valor se emparejó junto con un
bool
para indicar si el valor devuelto fue significativo.- por ejemplo, llamar a una función
std::pair<int, bool> parse(const std::string &str)
con un argumento de cadena que no es un número entero devolvería un par con unint
indefinido y unbool
establecido enfalse
.
- por ejemplo, llamar a una función
En este ejemplo, John recibe dos mascotas, Fluffy y Furball. Luego se llama a la función Person::pet_with_name()
para recuperar los bigotes de la mascota de John. Como John no tiene una mascota llamada Whiskers, la función falla y std::nullopt
se devuelve en su lugar.
#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;
}
}
opcional como valor de retorno
std::optional<float> divide(float a, float b) {
if (b!=0.f) return a/b;
return {};
}
Aquí devolvemos la fracción a/b
, pero si no está definida (sería infinito), devolvemos el opcional vacío.
Un caso más complejo:
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 )
busca en el contenedor o rango some_range
para algo igual al número 7
. find_if
hace con un predicado.
Devuelve un opcional vacío si no fue encontrado, o un opcional que contiene un iterador para el elemento si fue encontrado.
Esto te permite hacer:
if (find( vec, 7 )) {
// code
}
o incluso
if (auto oit = find( vec, 7 )) {
vec.erase(*oit);
}
sin tener que meterse con iteradores y pruebas de inicio / finalización.
valor_o
void print_name( std::ostream& os, std::optional<std::string> const& name ) {
std::cout "Name is: " << name.value_or("<name missing>") << '\n';
}
value_or
devuelve el valor almacenado en el opcional o el argumento si no hay nada almacenado allí.
Esto le permite tomar el opcional tal vez nulo y dar un comportamiento predeterminado cuando realmente necesita un valor. Al hacerlo de esta manera, la decisión de "comportamiento predeterminado" se puede retrasar hasta el punto en que se realice mejor y se necesite de inmediato, en lugar de generar algún valor predeterminado en las entrañas de algún motor.