estándar::opcional
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 pasarnullptr
para 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ásbool
el 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 abool
los 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::any
std::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 comostd::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 usoboost::optional
del tipo es agradable, resume cuándo debemos usar este tipo, cuándo usar OpcionalRecomendado cuando
optional<T>
: solo hay una razón clara (para todas las partes) paraT
un valor sin tipo, y la ausencia de un valor es tanT
natural como tener un valor regular
Si bien optional
la 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 int
valor 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 int
valor devuelto y regresamos, de lo contrario regresamos std::nullopt
para indicar que no hay int
valor.
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()
0
stoi()
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_view
y 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 Name
que representa un nombre que consta de un nombre, un segundo nombre opcional y un apellido. Los miembros middle
se 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 middle
en cómo se utilizan los valores de los miembros. El uso directo n.middle
será una expresión booleana que indica si hay un segundo nombre. Use *n.middle
para 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 Name
podemos 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_t
typestd::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 queoptional
el tipo tiene un estado no inicializado. En particular,std::optional
haynullopt_t
un constructor de un solo argumento que no crea ningún valoroptional
.- De la clase de excepción
std::exception
derivada , 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_place
std::in_place_t
std::optional<>
operación
La tabla de operaciones std::optional
enumera 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_place
como 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_place
pará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 nullopt
usarse 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 o16
es 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::nullopt
convierte 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*
yoperator->
- devuelve un puntero al valor contenido o una referencia al mismo. El operador de desreferenciaoperator*()
no verifica sioptional
contiene un valor, lo que puede servalue()
más eficiente que . El comportamiento no está definido si*this
no hay ningún valor presente .value()
- Si es*this
un valor, devuelve una referencia al valor contenido. De lo contrario,std::bad_optional_access
se 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 bool
usarlo 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_access
directamente 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, r1
y r2
referirse a valores que no existen, y usarlos dará como resultado un comportamiento indefinido. Tenga en cuenta que for
este 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 (
==、<=、>=
retornotrue
, otras comparaciones devuelvenfalse
). - 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 int
un objeto opcional de tipo, posiblemente incluso menor que 0:
std::optional<unsigned> uo;
uo < 0 // 返回true
uo < -42 // 返回true
Para bool
objetos 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 bool
los 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::nullopt
elimina 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<>
move
La semántica también es compatible . Si move
selecciona todo el objeto opcional, se copiará el estado interno y se reemplazará el valor move
. Como resultado, el opcional move
seleccionado 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, os
todaví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á move
el 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 bool
puntero 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 bool
valores sería un mejor reemplazo.monostate
std::variant<>