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 la App que no tenía un delegado devolvería nullptr .
    • 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.
  • 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.
  • 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 un int indefinido y un bool establecido en false .

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.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow