Guía completa de C++17 - std::opcional de componentes

Cebador

  • Representar un objeto posiblemente vacío de una manera expresiva
    Cuando programamos, a menudo nos encontramos con escenarios en los que un objeto de cierto tipo puede devolverse, pasarse y usarse . Es decir, el objeto puede o no tener un valor de cierto tipo . Por lo tanto, necesitamos una forma de emular la semántica similar a un puntero : los punteros se pueden pasar nullptrpara indicar que no tienen valor . La solución es definir un valor adicional de tipo bool como un indicador para indicar si el objeto tiene un valor al definir el objeto . std::optional<>Proporciona una forma segura de tipos para implementar dichos objetos.
  • Sobrecarga
    La memoria necesaria para un objeto opcional es igual al tamaño del objeto contenido más boolel tamaño de un tipo. Por lo tanto, los objetos opcionales son generalmente un byte más grandes que los objetos contenidos (más posiblemente la sobrecarga de espacio de la alineación de la memoria). ** Los objetos opcionales no necesitan asignar memoria de almacenamiento dinámico y la alineación es la misma que la del objeto contenido. **Sin embargo, los objetos opcionales no son simplemente equivalentes a boollos objetos incluidos con banderas adjuntas. Por ejemplo, en ausencia de un valor, no se llamará al constructor del objeto incorporado (de esta manera, los tipos incorporados sin un constructor predeterminado también pueden estar en un estado predeterminado efectivo).

Si bien puede lograr la "capacidad nula" mediante el uso de valores únicos (-1, infinito, nullptr), no es tan claro como el tipo de contenedor separado. Alternativamente, incluso podría usar std::unique_ptr y tratar el puntero vacío como no inicializado - esto funciona, pero viene con el costo de asignar memoria para el objeto.Si
bien puede lograr una "capacidad nula" usando valores únicos (-1, infinito, nullptr), no es claro como un separado tipo de envoltorio . Alternativamente, incluso puede usar std::unique_ptr y tratar los punteros nulos como no inicializados; esto funciona, pero tiene el costo de asignar memoria para el objeto .

  • Admite semántica de valor y semántica de movimiento
    Al igual que los objetos opcionalesstd::variant<> , tienen semántica de valor . Es decir, la operación de copia se implementará como una copia profunda : se creará un nuevo objeto independiente, que tiene una copia de las etiquetas del objeto original y valores incrustados (si los hay) en su propio espacio de memoria. El costo de copiar un valor sin un valor incrustado es pequeño, pero el costo de copiar un valor con un valor incrustado es aproximadamente igual al costo de copiar el valor incrustado. Además, los objetos también admiten la semántica de movimiento .std::anystd::optional<>std::optional<>std::optional<>

cuándo usar

En general, los envoltorios opcionales se pueden usar cuando:

  • Si desea representar bien un tipo anulable.
    • No se recomiendan valores únicos. (como -1, nullptr, NO_VALUE o lo que sea)
    • Por ejemplo, el segundo nombre de un usuario es opcional. Puede suponer que una cadena vacía es válida aquí, pero podría ser importante saber si el usuario ingresó algo. Utilice std::optional<std::string>usted puede obtener más información.
  • Devuelve el resultado de algún cálculo (procesamiento) que no pudo producir un valor y no es un error.
    Por ejemplo, buscar un elemento en un diccionario: si no hay ningún elemento debajo de una clave, no es un error, pero debemos manejar este caso.
  • Realice la carga diferida de recursos
    Por ejemplo, los tipos de recursos no tienen constructores predeterminados y están muy construidos. Entonces puede definirlo como std::optional<Resource>(puede pasarlo en el sistema) y luego cargarlo solo más tarde cuando lo necesite.
  • Pase parámetros opcionales a la función.
    La descripción del uso boost::optionaldel tipo es agradable, resume cuándo debemos usar este tipo, cuándo usar Opcional

    Recomendado cuando optional<T>: solo hay una razón clara (para todas las partes) para Tun valor sin tipo, y la ausencia de un valor es tan Tnatural como tener un valor regular

Si bien optionalla decisión de usar puede ser ambigua a veces, no debe usarse para el manejo de errores . Porque funciona mejor cuando el valor está vacío y es el estado normal del programa.

usarstd::optional<>

std::optional<>Simula una instancia anulable de cualquier tipo . Se puede utilizar como miembro, parámetro, valor de retorno, etc.

valor de retorno opcional

El siguiente programa de ejemplo muestra std::optional<>algunas funciones que se utilizarán como valores de retorno:

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

// 如果可能的话把string转换为int:
std::optional<int> asInt(const std::string& s) {
    
    
    try {
    
    
        return std::stoi(s);
    } catch (...) {
    
    
        return std::nullopt;
    }
}

int main() {
    
    
    for (auto s : {
    
    "42", "  077", "hello", "0x33"}) {
    
    
        // 尝试把s转换为int,并打印结果:
        std::optional<int> oi = asInt(s);
        if (oi) {
    
    
            std::cout << "convert '" << s << "' to int: " << *oi << "\n";
        } else {
    
    
            std::cout << "can't convert '" << s << "' to int\n";
        }
    }
}

resultado de la operación:

convert '42' to int: 42
convert '  077' to int: 77
can't convert 'hello' to int
convert '0x33' to int: 0

El código de preprocesamiento es el siguiente:

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

// 如果可能的话把string转换为int:
std::optional<int> asInt(const std::basic_string<char, std::char_traits<char>, std::allocator<char> > & s)
{
    
    
  try 
  {
    
    
    return std::optional<int>(std::stoi(s, 0, 10));
  } catch(...) {
    
    
    return std::optional<int>(std::nullopt_t(std::nullopt));
  }
  ;
}

int main()
{
    
    
  {
    
    
    std::initializer_list<const char *> && __range1 = std::initializer_list<const char *>{
    
    "42", "  077", "hello", "0x33"};
    const char *const * __begin1 = __range1.begin();
    const char *const * __end1 = __range1.end();
    for(; __begin1 != __end1; ++__begin1) {
    
    
      const char * s = *__begin1;
      std::optional<int> oi = asInt(std::basic_string<char, std::char_traits<char>, std::allocator<char> >(s, std::allocator<char>()));
      if(static_cast<bool>(oi.operator bool())) {
    
    
        std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout, "convert '"), s), "' to int: ").operator<<(oi.operator*()), "\n");
      } else {
    
    
        std::operator<<(std::operator<<(std::operator<<(std::cout, "can't convert '"), s), "' to int\n");
      } 
      
    }
    
  }
  return 0;
}

Este programa incluye una asInt()función para convertir una cadena entrante en un número entero. Sin embargo, esta operación puede fallar, por lo que el valor de retorno se define como std::optional<>, de modo que podemos devolver "ningún valor entero" en lugar de aceptar un intvalor especial, o lanzar una excepción a la persona que llama para indicar una falla.
Por lo tanto, cuando la conversión es exitosa inicializamos el valor devuelto con stoi()el intvalor devuelto y regresamos, de lo contrario regresamos std::nulloptpara indicar que no hay intvalor.

También podemos lograr el mismo comportamiento así:

std::optional<int> asInt(const std::string& s)
{
    
    
    std::optional<int> ret; // 初始化为无值
    try {
    
    
        ret = std::stoi(s);
    }
    catch (...) {
    
    
    }
    return ret;
}

En main(), llamamos a esta función con diferentes cadenas:

for (auto s : {
    
    "42", "  077", "hello", "0x33"} ) {
    
    
    // 尝试把s转换为int,并打印结果:
    std::optional<int> oi = asInt(s);
    ...
}

std::optional<int>Para el tipo devuelto oi, podemos determinar si contiene un valor ( usar el objeto como una expresión booleana ) y acceder al valor del objeto opcional "desreferenciando":

if (oi) {
    
    
    std::cout << "convert '" << s << "' to int: " << *oi << "\n";
}

Tenga en cuenta que se devolverá "0x33"la llamada con una cadena , ya que la cadena no se analizará en hexadecimal . Hay otras formas de manejar los valores devueltos, por ejemplo:asInt()0stoi()

std::optional<int> oi = asInt(s);
if (oi.has_value()) {
    
    
    std::cout << "convert '" << s << "' to int: " << oi.value() << "\n";
}

Se utiliza aquí has_value()para comprobar si se devuelve un valor y value()para acceder al valor. Más segurovalue() que el operador : lanza una excepción cuando no hay valor. Los operadores solo deben usarse en escenarios donde se sabe que existen valores, de lo contrario, el programa puede tener un comportamiento indefinido . Un ejemplo completo es el siguiente:**

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

std::optional<int> asInt1(const std::string& s) {
    
    
    std::optional<int> ret;  // 初始化为无值
    try {
    
    
        ret = std::stoi(s);
    } catch (...) {
    
    
    }
    return ret;
}

int main() {
    
    
    for (auto s : {
    
    "42", "  077", "hello", "0x33"}) {
    
    
        // 尝试把s转换为int,并打印结果:
        std::optional<int> oi = asInt1(s);
        if (oi) {
    
    
            std::cout << "convert '" << s << "' to int: " << *oi << "\n";
        } else {
    
    
            std::cout << "can't convert '" << s << "' to int\n";
        }
    }
    std::cout << "--------------" << std::endl;
    for (auto s : {
    
    "42", "  077", "hello", "0x33"}) {
    
    
        std::optional<int> oi = asInt1(s);
        if (oi.has_value()) {
    
    
            std::cout << "convert '" << s << "' to int: " << oi.value() << "\n";
        } else {
    
    
            std::cout << "can't convert '" << s << "' to int\n";
        }
    }
}

resultado de la operación:

convert '42' to int: 42
convert '  077' to int: 77
can't convert 'hello' to int
convert '0x33' to int: 0
--------------
convert '42' to int: 42
convert '  077' to int: 77
can't convert 'hello' to int
convert '0x33' to int: 0

**NOTA** Ahora podemos mejorar con nuevos tipos std::string_viewy nuevas funciones de acceso directo .std::from_chars()asInt()

Parámetros opcionales y miembros de datos

Otro std::optional<>ejemplo de uso es pasar parámetros opcionales y establecer miembros de datos opcionales :

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

using namespace std;

class Name {
    
    
   private:
    string first;
    optional<std::string> middle;
    string last;

   public:
    Name(string f, optional<std::string> m, string l)
        : first{
    
    std::move(f)}, middle{
    
    std::move(m)}, last{
    
    std::move(l)} {
    
    }
    friend ostream& operator<<(ostream& strm, const Name& n) {
    
    
        strm << n.first << ' ';
        if (n.middle) {
    
    
            strm << *n.middle << ' ';
        }
        return strm << n.last;
    }
};

int main() {
    
    
    Name n{
    
    "Jim", std::nullopt, "Knopf"};
    cout << n << endl;

    Name m{
    
    "Donald", "Ervin", "Knuth"};
    cout << m << endl;
}

resultado de la operación:

Jim Knopf
Donald Ervin Knuth

Clase Nameque representa un nombre que consta de un nombre, un segundo nombre opcional y un apellido. Los miembros middlese definen como opcionales , y se puede pasar uno cuando no hay un segundo nombre std::nullopt, que es un estado diferente a que el segundo nombre es una cadena vacía .
Tenga en cuenta que, como es habitual para los tipos con semántica de valor , la mejor manera de definir constructores es pasar parámetros por valor y luego mover los valores de los parámetros a los miembros.

Tenga en cuentastd::optional<> los cambios middleen cómo se utilizan los valores de los miembros. El uso directo n.middleserá una expresión booleana que indica si hay un segundo nombre. Use *n.middlepara acceder al valor actual (si lo hay).
Otra forma de acceder a los valores es usar funciones miembro value_or(), que le permiten especificar un valor alternativo cuando no hay ningún valor . Por ejemplo, en una clase Namepodemos implementarlo como:

std::cout << middle.value_or("");   // 打印中间名或空

Sin embargo, de esta manera habrá dos espacios entre el nombre y el apellido en lugar de uno cuando no haya ningún valor .

otro ejemplo

#include <iostream>
#include <optional>

class UserRecord {
    
    
   public:
    UserRecord(const std::string& name, std::optional<std::string> nick,
               std::optional<int> age)
        : mName{
    
    name}, mNick{
    
    nick}, mAge{
    
    age} {
    
    }

    friend std::ostream& operator<<(std::ostream& stream,
                                    const UserRecord& user);

   private:
    std::string mName;
    std::optional<std::string> mNick;
    std::optional<int> mAge;
};

std::ostream& operator<<(std::ostream& os, const UserRecord& user) {
    
    
    os << user.mName << ' ';
    if (user.mNick) {
    
    
        os << *user.mNick << ' ';
    }
    if (user.mAge) os << "age of " << *user.mAge;

    return os;
}

int main() {
    
    
    UserRecord tim{
    
    "Tim", "SuperTim", 16};
    UserRecord nano{
    
    "Nathan", std::nullopt, std::nullopt};

    std::cout << tim << "\n";
    std::cout << nano << "\n";
}

resultado de la operación:

Tim SuperTim age of 16
Nathan 

std::optional<>tipo y funcionamiento

std::optional<>tipo

<optional>La biblioteca estándar define la clase en el archivo de encabezado de la siguiente manera std::optional<>:

namespace std {
    
    
    template<typename T> class optional;
}

Además, se definen los siguientes tipos y objetos:

  • std::nullopt_ttype std::nullopt, como el "valor" cuando el objeto opcional no tiene valor.
    std::nullopt_t es un tipo de clase vacío que se usa para indicar que optional el tipo tiene un estado no inicializado. En particular, std::optional hay nullopt_t un constructor de un solo argumento que no crea ningún valor optional.
  • De la clase de excepción std::exceptionderivada , esta excepción se lanzará al acceder a un valor cuando no hay valor . Los objetos opcionales también usan el objeto definido en el archivo de encabezado (de tipo ) para admitir la inicialización de objetos opcionales con múltiples parámetros.std::bad_optional_access
    <utility>std::in_placestd::in_place_t

std::optional<>operación

La tabla de operaciones std::optionalenumera std::optional<>todas las operaciones:

operador Efecto
Constructor Cree un objeto opcional (puede o no llamar al constructor del tipo incorporado)
make_optional<>() Crear un objeto opcional inicializado con parámetros
incinerador de basuras destruye un objeto opcional
= asignar un nuevo valor
emplace() asigna un nuevo valor a un tipo incrustado
reset() Destruir el valor (hacer del objeto un estado sin valor)
has_value() Devuelve si el objeto opcional contiene un valor
convertido abool Devuelve si el objeto opcional contiene un valor
* acceder al valor interno (comportamiento indefinido si no hay valor)
-> Miembros que acceden a valores internos (comportamiento indefinido si no hay valor)
value() Acceder al valor interno (lanza una excepción si no hay valor)
value_or() Accede al valor interno (devuelve el valor del parámetro si no hay valor)
swap() intercambia los valores de dos objetos
==、!=、<、<=、>、>= comparar alternativas
hash<> El tipo de objeto de función que calcula el valor hash

Constructor

Los constructores especiales le permiten pasar valores de tipos incorporados directamente como argumentos.

  • Puede crear un objeto opcional sin un valor . En este caso, debe especificar el tipo contenido:
std::optional<int> o1;
std::optional<int> o2(std::nullopt);

En este caso, no se llamará a ningún constructor del tipo incrustado .

  • Puede pasar un valor para inicializar el tipo incrustado . Gracias a la guía de deducción, ya no necesita especificar el tipo interno:
std::optional o3{
    
    42};       // 推导出optional<int>
std::optional o4{
    
    "hello"};  // 推导出optional<const char*>
using namespace std::string_literals;
std::optional o5{
    
    "hello"s}; // 推导出optional<string>
  • Para inicializar un opcional con varios argumentos, debe pasar un objeto construido o agregarlo std::in_placecomo primer argumento (en cuyo caso no se deduce el tipo incrustado):
std::optional o6{
    
    std::complex{
    
    3.0, 4.0}};
std::optional<std::complex<double>> o7{
    
    std::in_place, 3.0, 4.0};

Tenga en cuenta que la segunda forma evita la creación de variables temporales . Con este enfoque, incluso puede pasar una columna de valor inicial más otros parámetros:

// 用lambda作为排序准则初始化set:
auto sc = [] (int x, int y) {
    
    
              return std::abs(x) < std::abs(y);
          };
std::optional<std::set<int, decltype(sc)>> o8{
    
    std::in_place, {
    
    4, 8, -7, -2, 0, 5}, sc};

Sin embargo, esto solo es posible si todos los valores iniciales coinciden con el tipo de los elementos en el contenedor. De lo contrario, debe pasar uno explícitamente std::initializer_list<>:

// 用lambda作为排序准则初始化set
auto sc = [] (int x, int y) {
    
    
              return std::abs(x) < std::abs(y);
          };
std::optional<std::set<int, decltype(sc)>> o8{
    
    std::in_place,
                                              std::initializer_list<int>{
    
    4, 5L}, sc};
  • Los objetos opcionales también se pueden copiar si el tipo subyacente admite la copia (admite la conversión de tipos):
std::optional o9{
    
    "hello"};          // 推导出optional<const char*>
std::optional<std::string> o10{
    
    o9}; // OK

Sin embargo, tenga en cuenta que si el tipo interno en sí se puede construir a partir de un objeto opcional, entonces el objeto opcional se usará para construir el objeto interno en lugar de copiarlo:

std::optional<int> o11;
std::optional<std::any> o12{
    
    o11}; // o12内含了一个any对象,该对象的值是一个空的optional<int>

Tenga en cuenta que también hay una función de acceso directo make_optional<>()que puede inicializar un objeto opcional con parámetros únicos o múltiples (no es necesario usar in_placeparámetros). Como make...funciones normales, sus argumentos degeneran:

auto o13 = std::make_optional(3.0);     // optional<double>
auto o14 = std::make_optional("hello"); // optional<const char*>
auto o15 = std::make_optional<std::complex<double>>(3.0, 4.0);

Tenga en cuenta , sin embargo, que no hay un constructor que pueda juzgar si un valor debe usarse o nulloptusarse para . En este caso, solo se pueden utilizar operadores ?:.

Por ejemplo:

std::multimap<std::string, std::string> englishToGerman;
...
auto pos = englishToGerman.find("wisdom");
auto o16 = pos != englishToGerman.end() ? std::optional{
    
    pos->second} : std::nullopt;

En este ejemplo, según la deducción del parámetro de plantilla de clase, el tipo que std::optional{pos->second}se puede deducir a través de la expresión o16es std::optional<std::string>. La deducción de argumentos de plantilla de clase no funciona en un solo tipo std::nullopt, pero mediante el uso de un operador ?:, también se std::nulloptconvierte automáticamente en un tipo, porque las dos expresiones del operador deben tener el mismo tipo .std::optional<string>?:

Ejemplo completo:

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

int main() {
    
    
    std::optional<int> o1,  // 空
        o2 = 1,             // 从右值初始化
        o3 = o2;            // 复制构造函数

    // 调用 std::string( initializer_list<CharT> ) 构造函数
    std::optional<std::string> o4(std::in_place, {
    
    'a', 'b', 'c'});

    // 调用 std::string( size_type count, CharT ch ) 构造函数
    std::optional<std::string> o5(std::in_place, 3, 'A');

    // 从 std::string 移动构造,用推导指引拾取类型

    std::optional o6(std::string{
    
    "deduction"});

    std::cout << *o2 << ' ' << *o3 << ' ' << *o4 << ' ' << *o5 << ' ' << *o6
              << '\n';

    // empty:
    std::optional<int> oEmpty;
    std::optional<float> oFloat = std::nullopt;

    // direct:
    std::optional<int> oInt(10);
    std::optional oIntDeduced(10);  // 推到指引

    // make_optional
    auto oDouble = std::make_optional(3.0);
    auto oComplex = std::make_optional<std::complex<double>>(3.0, 4.0);

    // in_place
    std::optional<std::complex<double>> o7{
    
    std::in_place, 3.0, 4.0};

    // will call vector with direct init of {1, 2, 3}
    std::optional<std::vector<int>> oVec(std::in_place, {
    
    1, 2, 3});

    // copy/assign:
    auto oIntCopy = oInt;

    std::cout << oInt.value() << ' ' << oIntDeduced.value() << ' '
              << oDouble.value() << ' ' << oComplex.value() << ' ' << o7.value()
              << " " << oVec.value().size() << " " << oInt.value() << std::endl;
}

La salida es la siguiente:

1 1 abc AAA deduction
10 10 3 (3,4) (3,4) 3 10

El código de preprocesamiento es el siguiente:

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

int main()
{
    
    
  std::optional<int> o1 = std::optional<int>();
  std::optional<int> o2 = std::optional<int>(1);
  std::optional<int> o3 = std::optional<int>(o2);
  std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > o4 = std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::in_place_t(std::in_place), std::initializer_list<char>{
    
    'a', 'b', 'c'});
  std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > o5 = std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::in_place_t(std::in_place), 3, 'A');
  std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > o6 = std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::basic_string<char, std::char_traits<char>, std::allocator<char> >{
    
    "deduction", std::allocator<char>()});
  std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout.operator<<(o2.operator*()), ' ').operator<<(o3.operator*()), ' '), o4.operator*()), ' '), o5.operator*()), ' '), o6.operator*()), '\n');
  std::optional<int> oEmpty = std::optional<int>();
  std::optional<float> oFloat = std::optional<float>(std::nullopt_t(std::nullopt));
  std::optional<int> oInt = std::optional<int>(10);
  std::optional<int> oIntDeduced = std::optional<int>(10);
  std::optional<double> oDouble = std::make_optional(3.0);
  std::optional<std::complex<double> > oComplex = std::make_optional<std::complex<double> >(3.0, 4.0);
  std::optional<std::complex<double> > o7 = std::optional<std::complex<double> >{
    
    std::in_place_t(std::in_place), 3.0, 4.0};
  std::optional<std::vector<int, std::allocator<int> > > oVec = std::optional<std::vector<int, std::allocator<int> > >(std::in_place_t(std::in_place), std::initializer_list<int>{
    
    1, 2, 3});
  std::optional<int> oIntCopy = std::optional<int>(oInt);
  std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout.operator<<(oInt.value()), ' ').operator<<(oIntDeduced.value()), ' ').operator<<(oDouble.value()), ' '), oComplex.value()), ' '), o7.value()), " ").operator<<(oVec.value().size()), " ").operator<<(oInt.value()).operator<<(std::endl);
  return 0;
}

Ejemplo 2

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

int main() {
    
    
    auto op1 = std::make_optional<std::vector<char>>({
    
    'a', 'b', 'c'});
    std::cout << "op1: ";
    for (char c : op1.value()) {
    
    
        std::cout << c << ",";
    }
    auto op2 = std::make_optional<std::vector<int>>(5, 2);
    std::cout << "\nop2: ";
    for (int i : *op2) {
    
    
        std::cout << i << ",";
    }
    std::string str{
    
    "hello world"};
    auto op3 = std::make_optional<std::string>(std::move(str));
    std::cout << "\nop3: " << quoted(op3.value_or("empty value")) << '\n';
    std::cout << "str: " << std::quoted(str) << '\n';
}

El resultado de la operación es el siguiente:

op1: a,b,c,
op2: 2,2,2,2,2,
op3: "hello world"
str: ""

El código de preprocesamiento es el siguiente:

int main()
{
    
    
  std::optional<std::vector<char, std::allocator<char> > > op1 = std::make_optional<std::vector<char, std::allocator<char> > >(std::initializer_list<char>{
    
    'a', 'b', 'c'});
  std::operator<<(std::cout, "op1: ");
  {
    
    
    std::vector<char, std::allocator<char> > & __range1 = op1.value();
    __gnu_cxx::__normal_iterator<char *, std::vector<char, std::allocator<char> > > __begin1 = __range1.begin();
    __gnu_cxx::__normal_iterator<char *, std::vector<char, std::allocator<char> > > __end1 = __range1.end();
    for(; !__gnu_cxx::operator==(__begin1, __end1); __begin1.operator++()) {
    
    
      char c = __begin1.operator*();
      std::operator<<(std::operator<<(std::cout, c), ",");
    }
    
  }
  std::optional<std::vector<int, std::allocator<int> > > op2 = std::make_optional<std::vector<int, std::allocator<int> > >(5, 2);
  std::operator<<(std::cout, "\nop2: ");
  {
    
    
    std::vector<int, std::allocator<int> > & __range1 = op2.operator*();
    __gnu_cxx::__normal_iterator<int *, std::vector<int, std::allocator<int> > > __begin1 = __range1.begin();
    __gnu_cxx::__normal_iterator<int *, std::vector<int, std::allocator<int> > > __end1 = __range1.end();
    for(; !__gnu_cxx::operator==(__begin1, __end1); __begin1.operator++()) {
    
    
      int i = __begin1.operator*();
      std::operator<<(std::cout.operator<<(i), ",");
    }
    
  }
  std::basic_string<char, std::char_traits<char>, std::allocator<char> > str = std::basic_string<char, std::char_traits<char>, std::allocator<char> >{
    
    "hello world", std::allocator<char>()};
  std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > op3 = std::make_optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::move(str));
  std::operator<<(std::__detail::operator<<(std::operator<<(std::cout, "\nop3: "), std::quoted(op3.value_or<const char (&)[12]>("empty value"), char('"'), char('\\'))), '\n');
  std::operator<<(std::__detail::operator<<(std::operator<<(std::cout, "str: "), std::quoted(str, char('"'), char('\\'))), '\n');
  return 0;
}

valor de acceso

Varias opciones:

  • operator* y operator->- devuelve un puntero al valor contenido o una referencia al mismo. El operador de desreferencia operator*()no verifica si optionalcontiene un valor, lo que puede ser value()más eficiente que . El comportamiento no está definido si *this no hay ningún valor presente .
  • value()- Si es *thisun valor, devuelve una referencia al valor contenido. De lo contrario, std::bad_optional_accessse lanza una excepción.
  • value_or(defaultVal) - el valor devuelto si está disponible; de ​​lo contrario, el valor predeterminado.

Para verificar si un opcional tiene un valor, puede llamarlo has_value()o boolusarlo en una expresión:

std::optional o{
    
    42};

if (o) ...              // true
if (!o) ...             // false
if (o.has_value())...   // true

El código de preprocesamiento es el siguiente:

  std::optional<int> o = std::optional<int>{
    
    42};
  if(static_cast<bool>(o.operator bool())) {
    
    
  } 
  
  if(!static_cast<bool>(o.operator bool())) {
    
    
  } 
  
  if(o.has_value()) {
    
    
  } 

No se define ningún operador para las opciones opcionales I/O, porque no está seguro de qué se debe generar cuando la opción opcional no tiene valor :

std::cout << o;         // ERROR

Para acceder a los valores internos, puede usar la sintaxis de puntero. Es decir, a través de operadores *, puedes acceder directamente al valor interno del objeto opcional, o puedes usar ->los miembros que acceden al valor interno:

std::optional o{
    
    std::pair{
    
    42, "hello"}};
auto p = *o;            // 初始化p为pair<int, string>
std::cout << o->first;  // 打印出42

Tenga en cuenta que todos estos operadores requieren que el objeto opcional contenga un valor. Usarlo así sin un valor da como resultado un comportamiento indefinido:

std::optional<std::string> o{
    
    "hello"};

std::cout << *o;    // OK:打印出"hello"
o = std::nullopt;
std::cout << *o;    // 未定义行为

Tenga en cuenta que, en la práctica, la segunda declaración de salida aún se compila y puede imprimirse nuevamente "hello", porque la memoria del valor subyacente en el opcional no se modifica. Sin embargo, nunca debes confiar en esto. Si no sabe si un opcional tiene un valor, debe llamarlo así :

if (o) std::cout << *o;     // OK(可能输出为空字符串)

O puede usar una función miembro para acceder al valor, lo que generará una excepción value()si no hay ningún valor dentro :std::bad_optional_access

std::cout << o.value();     // OK(无值时会抛出异常)

std::bad_optional_accessdirectamente derivado de std::exception.

Tenga en cuenta que operator*y value()ambos devuelven una referencia al objeto contenido . Por lo tanto, tenga cuidado al utilizar directamente los objetos temporales devueltos por estas operaciones. Por ejemplo, para una función que devuelve una cadena opcional:

std::optional<std::string> getString();

Siempre es seguro asignar el valor de lo opcional que devuelve al nuevo objeto:

auto a = getString().value();           // OK:内含对象的拷贝或抛出异常

Sin embargo, usar el valor devuelto directamente (o pasarlo como argumento) es una fuente de problemas:

auto b = *getString();                  // ERROR:如果返回std::nullopt将会有未定义行为
const auto& r1 = getString().value();   // ERROR:引用销毁的内含对象
auto&& r2 = getString().value();        // ERROR:引用销毁的内含对象

El problema con el uso de referencias es que, por regla, una referencia extiende value()el tiempo de vida del valor devuelto, no getString() el tiempo de vida del Opcional devuelto. Por lo tanto, r1y r2referirse a valores que no existen, y usarlos dará como resultado un comportamiento indefinido. Tenga en cuenta que foreste problema puede surgir fácilmente al usar bucles de rango:

std::optional<std::vector<int>> getVector();
...
for (int i : getVector().value()) {
    
         // ERROR:迭代一个销毁的vector
    std::cout << i << '\n';
}

Tenga en cuenta que iterar sobre el valor de retorno de un tipo no opcional vector<int>está bien. Por lo tanto, no reemplace ciegamente el valor de retorno de la función con el tipo de objeto opcional correspondiente.

Finalmente, puede establecer un valor alternativo sin valor al obtener el valor. Esta suele ser la forma más fácil de escribir un opcional en un flujo de salida:

std::cout << o.value_or("NO VALUE");    // OK(没有值时写入NO VALUE)

Sin embargo, hay una diferencia a considerar entre value()yvalue_or()

value_or()Devuelve un valor, mientras que value()se devuelve una referencia. Esto significa llamadas como:

std::cout << middle.value_or("");

y:

std::cout << o.value_or("fallback");

asignará memoria implícitamente, y value()nunca lo hará.

Sin embargo, cuando se invoca en un objeto temporal (rvalue) value_or(), el valor del objeto adjunto se alejará y se devolverá por valor, en lugar de llamar al constructor de la función de copia. Este es el único método que funciona para los tipos de solo movimiento, ya que la versión sobrecargada value_or()llamada en un lvalue requiere que el objeto subyacente sea copiable.value_or()

Por lo tanto, la implementación más eficiente del ejemplo anterior sería:

std::cout << o ? o->c_str() : "fallback";

en lugar de:

std::cout << o.value_or("fallback");

value_or()es una interfaz que expresa la intención más claramente, pero puede tener un poco más de sobrecarga .

ejemplo

#include <iomanip>
#include <iostream>
#include <optional>

int main() {
    
    
    // by operator*
    std::optional<int> oint = 10;
    std::cout << "oint " << *oint << '\n';

    // by value()
    std::optional<std::string> ostr("hello");
    try {
    
    
        std::cout << "ostr " << ostr.value() << '\n';
    } catch (const std::bad_optional_access& e) {
    
    
        std::cout << e.what() << "\n";
    }

    // by value_or()
    std::optional<double> odouble;  // empty
    std::cout << "odouble " << odouble.value_or(10.0) << '\n';
}

resultado de la operación:

oint 10
ostr hello
odouble 10

El código de preprocesamiento es el siguiente:

#include <iomanip>
#include <iostream>
#include <optional>

int main()
{
    
    
  std::optional<int> oint = std::optional<int>(10);
  std::operator<<(std::operator<<(std::cout, "oint ").operator<<(oint.operator*()), '\n');
  std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > ostr = std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >("hello");
  try 
  {
    
    
    std::operator<<(std::operator<<(std::operator<<(std::cout, "ostr "), ostr.value()), '\n');
  } catch(const std::bad_optional_access & e) {
    
    
    std::operator<<(std::operator<<(std::cout, e.what()), "\n");
  }
  ;
  std::optional<double> odouble = std::optional<double>();
  std::operator<<(std::operator<<(std::cout, "odouble ").operator<<(odouble.value_or<double>(10.0)), '\n');
  return 0;
}

Comparar

Puede utilizar los operadores de comparación habituales. Los operandos pueden ser objetos opcionales, objetos de tipos incorporados, std::nullopt.

  • Si ambos operandos son objetos valorados, se invocará el operador correspondiente del tipo incrustado.
  • Si ambos operandos son objetos sin valor, son iguales ( ==、<=、>=retorno true, otras comparaciones devuelven false).
  • Si exactamente un operando tiene un valor, el operando sin valor es más pequeño que el operando con valor.

Por ejemplo:

std::optional<int> o0;
std::optional<int> o1{
    
    42};

    o0 == std::nullopt  // 返回true
    o0 == 42            // 返回false
    o0 < 42             // 返回true
    o0 > 42             // 返回false
    o1 == 42            // 返回true
    o0 < o1             // 返回true

Esto significa unsigned intun objeto opcional de tipo, posiblemente incluso menor que 0:

std::optional<unsigned> uo;
    uo < 0              // 返回true
    uo < -42            // 返回true

Para boolobjetos opcionales de tipo, menos de false:

std::optional<bool> bo;
    bo < false          // 返回true

Para un código más legible, debe usar

if (!uo.has_value())

en lugar de

if (uo < 0)

También se admiten comparaciones híbridas entre objetos opcionales y el tipo subyacente, siempre que el tipo subyacente admita tales comparaciones:

std::optional<int> o1{
    
    42};
std::optional<double> o2{
    
    42.0};

o2 == 42            // 返回true
o1 == o2            // 返回true

Si el tipo subyacente admite la conversión de tipo implícita, el tipo de objeto opcional correspondiente también se somete a la conversión de tipo implícita.
Tenga en cuenta que boollos tipos opcionales o los punteros sin procesar pueden causar un comportamiento extraño.

Ejemplo:

#include <optional>
#include <iostream>

int main()
{
    
    
    std::optional<int> oEmpty;
    std::optional<int> oTwo(2);
    std::optional<int> oTen(10);

    std::cout << std::boolalpha;
    std::cout << (oTen > oTwo) << "\n";
    std::cout << (oTen < oTwo) << "\n";
    std::cout << (oEmpty < oTwo) << "\n";
    std::cout << (oEmpty == std::nullopt) << "\n";
    std::cout << (oTen == 10) << "\n";
}

El resultado de la operación es el siguiente:

true
false
true
true
true

El código de preprocesamiento es el siguiente:

  std::optional<int> oEmpty = std::optional<int>();
  std::optional<int> oTwo = std::optional<int>(2);
  std::optional<int> oTen = std::optional<int>(10);
  std::cout.operator<<(std::boolalpha);
  std::operator<<(std::cout.operator<<((std::operator>(oTen, oTwo))), "\n");
  std::operator<<(std::cout.operator<<((std::operator<(oTen, oTwo))), "\n");
  std::operator<<(std::cout.operator<<((std::operator<(oEmpty, oTwo))), "\n");
  std::operator<<(std::cout.operator<<((std::operator==(oEmpty, std::nullopt_t(std::nullopt)))), "\n");
  std::operator<<(std::cout.operator<<((std::operator==(oTen, 10))), "\n");

valor modificado

La asignación y emplace()las operaciones se pueden utilizar para modificar valores:

std::optional<std::complex<double>> o;  // 没有值
std::optional ox{
    
    77};   // optional<int>,值为77

o = 42;                 // 值变为complex(42.0, 0.0)
o = {
    
    9.9, 4.4};         // 值变为complex(9.9, 4.4)
o = ox;                 // OK,因为int转换为complex<double>
o = std::nullopt;       // o不再有值
o.emplace(5.5, 7.7);    // 值变为complex(5.5, 7.7)

Asignar a std::nulloptelimina el valor incrustado, llamando al destructor del tipo incrustado si había un valor anterior. También puede reset()lograr el mismo efecto llamando:

o.reset();              // o不再有值

O asigne llaves vacías:

o = {
    
    };                 // o不再有值

Finalmente, también podemos operator*modificar el valor con , ya que devuelve una referencia. Sin embargo, tenga en cuenta que este enfoque requiere que exista el valor:

std::optional<std::complex<double>> o;
*o = 42;                // 未定义行为
...
if (o) {
    
    
    *o = 88;            // OK:值变为complex(88.0, 0.0)
    *o = {
    
    1.2, 3.4};    // OK:值变为complex(1.2, 3.4)
}

Ejemplo:

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

class UserName {
    
    
   public:
    explicit UserName(const std::string& str) : mName(str) {
    
    
        std::cout << "UserName::UserName(\'";
        std::cout << mName << "\')\n";
    }
    ~UserName() {
    
    
        std::cout << "UserName::~UserName(\'";
        std::cout << mName << "\')\n";
    }

   private:
    std::string mName;
};

int main() {
    
    
    std::optional<UserName> oEmpty;

    // emplace:
    oEmpty.emplace("Steve");

    // calls ~Steve and creates new Mark:
    oEmpty.emplace("Mark");

    // reset so it's empty again
    oEmpty.reset();  // calls ~Mark
    // same as:
    // oEmpty = std::nullopt;

    // assign a new value:
    oEmpty.emplace("Fred");
    oEmpty = UserName("Joe");
}

resultado de la operación:

UserName::UserName('Steve')
UserName::~UserName('Steve')
UserName::UserName('Mark')
UserName::~UserName('Mark')
UserName::UserName('Fred')
UserName::UserName('Joe')
UserName::~UserName('Joe')
UserName::~UserName('Joe')

El código de preprocesamiento es el siguiente:

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

class UserName
{
    
    
  public: 
  inline explicit UserName(const std::basic_string<char, std::char_traits<char>, std::allocator<char> > & str)
  : mName{
    
    std::basic_string<char, std::char_traits<char>, std::allocator<char> >(str)}
  {
    
    
    std::operator<<(std::cout, "UserName::UserName('");
    std::operator<<(std::operator<<(std::cout, this->mName), "')\n");
  }
  inline ~UserName() noexcept
  {
    
    
    std::operator<<(std::cout, "UserName::~UserName('");
    std::operator<<(std::operator<<(std::cout, this->mName), "')\n");
  }
  private: 
  std::basic_string<char, std::char_traits<char>, std::allocator<char> > mName;
  public: 
  // inline constexpr UserName(const UserName &) noexcept(false) = default;
  // inline UserName & operator=(const UserName &) noexcept(false) = default;
};

int main()
{
    
    
  std::optional<UserName> oEmpty = std::optional<UserName>();
  oEmpty.emplace<const char (&)[6]>("Steve");
  oEmpty.emplace<const char (&)[5]>("Mark");
  oEmpty.reset();
  oEmpty.emplace<const char (&)[5]>("Fred");
  oEmpty.operator=(UserName(UserName(std::basic_string<char, std::char_traits<char>, std::allocator<char> >("Joe", std::allocator<char>()))));
  return 0;
}

mover la semántica

std::optional<>moveLa semántica también es compatible . Si moveselecciona todo el objeto opcional, se copiará el estado interno y se reemplazará el valor move. Como resultado, el opcional moveseleccionado permanece en su estado original, pero el valor se vuelve indefinido . Sin embargo, también puede mover los valores contenidos dentro y fuera individualmente. Por ejemplo:

std::optional<std::string> os;
std::string s = "a very very very long string";
os = std::move(s);                  // OK,move
std::string s2 = *os;               // OK,拷贝
std::string s3 = std::move(*os);    // OK,move

El código de preprocesamiento es el siguiente:

{
    
    
  std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > os = std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >();
  std::basic_string<char, std::char_traits<char>, std::allocator<char> > s = std::basic_string<char, std::char_traits<char>, std::allocator<char> >("a very very very long string", std::allocator<char>());
  os.operator=(std::move(s));
  std::basic_string<char, std::char_traits<char>, std::allocator<char> > s2 = std::basic_string<char, std::char_traits<char>, std::allocator<char> >(os.operator*());
  std::basic_string<char, std::char_traits<char>, std::allocator<char> > s3 = std::basic_string<char, std::char_traits<char>, std::allocator<char> >(std::move(os.operator*()));
  return 0;
}

Tenga en cuenta que después de la última llamada, ostodavía hay un valor de cadena, pero al igual que el objeto cuyo valor se eliminó, el valor no está definido. Por lo tanto, puede usarlo, pero no haga suposiciones sobre su valor. También puede asignarle una nueva cadena. También tenga en cuenta que algunas de las versiones sobrecargadas garantizarán que se utilice el objeto opcional temporal move. Considere la siguiente función que devuelve una cadena opcional:

std::optional<std::string> func();

En este caso, el siguiente código temporalizará moveel valor del objeto opcional:

std::string s4 = func().value();    // OK,move
std::string s5 = *func();           // OK,move

El comportamiento anterior se puede garantizar sobrecargando la versión rvalue de la función miembro correspondiente:

namespace std {
    
    
    template<typename T>
    class optional {
    
    
        ...
        constexpr T& operator*() &;
        constexpr const T& operator*() const&;
        constexpr T&& operaotr*() &&;
        constexpr const T&& operator*() const&&;
        constexpr T& value() &;
        constexpr const T& value() const&;
        constexpr T&& value() &&;
        constexpr const T&& value() const&&;
    };
}

En otras palabras, también puedes escribir así:

std::optional<std::string> os;
std::string s6 = std::move(os).value(); // OK,move

picadillo

El valor hash del objeto opcional es igual al valor hash del valor contenido (si hay un valor). El valor hash para objetos opcionales sin valor no está definido.

Caso especial

Ciertos tipos opcionales pueden causar un comportamiento peculiar o inesperado.

Objeto opcional de tipo bool o puntero sin procesar

El uso de operadores de comparación cuando se usan objetos opcionalesbool como valores tiene una semántica especial. Esto puede generar un comportamiento confuso si el tipo subyacente es un boolpuntero o un tipo de puntero . Por ejemplo:

std::optional<bool> ob{
    
    false};  // 值为false
if (!ob) ...                    // 返回false
if (ob == false) ...            // 返回true

std::optional<int*> op{
    
    nullptr};
if (!op) ...                    // 返回false
if (op == nullptr) ...          // 返回true

objeto opcional opcional

En teoría, podrías definir opcionales de opcionales:

std::optional<std::optional<std::string>> oos1;
std::optional<std::optional<std::string>> oos2 = "hello";
std::optional<std::optional<std::string>> oos3{
    
    std::in_place, std::in_place, "hello"};

std::optional<std::optional<std::complex<double>>> ooc{
    
    std::in_place, std::in_place, 4.2, 5.3};

Incluso puede asignar directamente a través de conversión de tipo implícito:

oos1 = "hello";         // OK:赋新值
ooc.emplace(std::in_place, 7.2, 8.3);

Debido a que ambas capas de opcionales pueden no tener valor, los opcionales de opcionales le permiten no tener ningún valor en la capa interna o ningún valor en la capa externa, lo que puede resultar en una semántica diferente:

*oos1 = std::nullopt;   // 内层可选对象无值
oos1 = std::nullopt;    // 外层可选对象无值

Esto significa que debe tener mucho cuidado al tratar con tales opciones:

if (!oos1) std::cout << "no value\n";
if (oos1 && !*oos1) std::cout << "no inner value\n";
if (oos1 && *oos1) std::cout << "value: " << **oos1 << '\n';

Sin embargo, semánticamente, este es solo un tipo con dos estados que no representan ningún valor. Entonces un OR con dos boolvalores sería un mejor reemplazo.monostatestd::variant<>

Supongo que te gusta

Origin blog.csdn.net/MMTS_yang/article/details/130773067
Recomendado
Clasificación