¿Cómo se debe usar std :: opcional?

133

Estoy leyendo la documentación std::experimental::optionaly tengo una buena idea de lo que hace, pero no entiendo cuándo debería usarla o cómo debería usarla. Hasta el momento, el sitio no contiene ningún ejemplo, lo que me dificulta comprender el verdadero concepto de este objeto. Cuándo es std::optionaluna buena opción para usar, y cómo compensa lo que no se encontró en el Estándar anterior (C ++ 11).

0x499602D2
fuente
19
Los documentos boost.optional podrían arrojar algo de luz.
juanchopanza
Parece que std :: unique_ptr generalmente puede servir los mismos casos de uso. Supongo que si tienes algo en contra de lo nuevo, entonces lo opcional podría ser preferible, pero me parece que (los desarrolladores | aplicaciones) con ese tipo de visión sobre lo nuevo son una pequeña minoría ... AFAICT, opcional NO es una gran idea. Por lo menos, la mayoría de nosotros podría vivir cómodamente sin él. Personalmente, me sentiría más cómodo en un mundo donde no tengo que elegir entre unique_ptr versus opcional. Llámame loco, pero Zen of Python tiene razón: ¡que haya una forma correcta de hacer algo!
allyourcode
19
No siempre queremos asignar algo en el montón que deba eliminarse, por lo que no unique_ptr no sustituye a opcional.
Krum
55
@allyourcode Ningún puntero es un sustituto de optional. Imagina que quieres un optional<int>o incluso <char>. ¿Realmente cree que se requiere "Zen" para asignar, desreferenciar y luego eliminar dinámicamente, algo que de otro modo no podría requerir ninguna asignación y encajar de manera compacta en la pila, y posiblemente incluso en un registro?
underscore_d
1
Otra diferencia, resultante sin duda de la asignación de pila vs montón del valor contenido, es que el argumento de la plantilla no puede ser un tipo incompleto en el caso de std::optional(mientras que puede ser para std::unique_ptr). Más precisamente, la norma exige que T [...] satisfaga los requisitos de Destructible .
dan_din_pantelimon

Respuestas:

172

El ejemplo más simple que se me ocurre:

std::optional<int> try_parse_int(std::string s)
{
    //try to parse an int from the given string,
    //and return "nothing" if you fail
}

Lo mismo podría lograrse con un argumento de referencia (como en la siguiente firma), pero el uso std::optionalhace que la firma y el uso sean más agradables.

bool try_parse_int(std::string s, int& i);

Otra forma de hacer esto es especialmente mala :

int* try_parse_int(std::string s); //return nullptr if fail

Esto requiere una asignación de memoria dinámica, preocuparse por la propiedad, etc. Siempre prefiera una de las otras dos firmas anteriores.


Otro ejemplo:

class Contact
{
    std::optional<std::string> home_phone;
    std::optional<std::string> work_phone;
    std::optional<std::string> mobile_phone;
};

¡Esto es extremadamente preferible a tener algo como un std::unique_ptr<std::string>para cada número de teléfono! std::optionalle brinda la localidad de datos, que es excelente para el rendimiento.


Otro ejemplo:

template<typename Key, typename Value>
class Lookup
{
    std::optional<Value> get(Key key);
};

Si la búsqueda no tiene una clave determinada, simplemente podemos devolver "sin valor".

Puedo usarlo así:

Lookup<std::string, std::string> location_lookup;
std::string location = location_lookup.get("waldo").value_or("unknown");

Otro ejemplo:

std::vector<std::pair<std::string, double>> search(
    std::string query,
    std::optional<int> max_count,
    std::optional<double> min_match_score);

¡Esto tiene mucho más sentido que, por ejemplo, tener cuatro sobrecargas de funciones que toman todas las combinaciones posibles de max_count(o no) y min_match_score(o no)!

¡También elimina el maldito "Pase -1por max_countsi no quiere un límite" o "Pase std::numeric_limits<double>::min()por min_match_scoresi no quiere un puntaje mínimo"!


Otro ejemplo:

std::optional<int> find_in_string(std::string s, std::string query);

Si la cadena de consulta no está en s, quiero "no int", no cualquier valor especial que alguien haya decidido usar para este propósito (-1?).


Para ver ejemplos adicionales, puede consultar la boost::optional documentación . boost::optionaly std::optionalbásicamente será idéntico en términos de comportamiento y uso.

Timothy Shields
fuente
13
@gnzlbg std::optional<T>es solo una Ty una bool. Las implementaciones de funciones miembro son extremadamente simples. El rendimiento no debería ser realmente una preocupación al usarlo: hay veces en que algo es opcional, en cuyo caso esta suele ser la herramienta correcta para el trabajo.
Timothy Shields
8
@TimothyShields std::optional<T>es mucho más complejo que eso. Utiliza la colocación newy muchas otras cosas con la alineación y el tamaño adecuados para que sea un tipo literal (es decir, usar con constexpr) entre otras cosas. La ingenuidad Ty el boolenfoque fallarían bastante rápido.
Rapptz
15
@Rapptz Línea 256: union storage_t { unsigned char dummy_; T value_; ... }Línea 289: struct optional_base { bool init_; storage_t<T> storage_; ... }¿Cómo es que eso no es "a Ty a bool"? Estoy completamente de acuerdo en que la implementación es muy complicada y no trivial, pero conceptual y concretamente el tipo es a Ty a bool. "La ingenuidad Ty el boolenfoque fallarían bastante rápido". ¿Cómo puedes hacer esta declaración cuando miras el código?
Timothy Shields
12
@Rapptz todavía está almacenando un bool y el espacio para un int. La unión solo está ahí para hacer que lo opcional no construya una T si realmente no se desea. Aún así, struct{bool,maybe_t<T>}el sindicato está ahí para no hacer struct{bool,T}lo que construiría una T en todos los casos.
PeterT
12
@allyourcode Muy buena pregunta. Ambos std::unique_ptr<T>y std::optional<T>en cierto sentido cumplen el papel de "un opcional T". Describiría la diferencia entre ellos como "detalles de implementación": asignaciones adicionales, administración de memoria, localidad de datos, costo de traslado, etc. Nunca tendría, std::unique_ptr<int> try_parse_int(std::string s);por ejemplo, porque causaría una asignación para cada llamada aunque no haya razón para . Nunca tendría una clase con un std::unique_ptr<double> limit;- ¿por qué hacer una asignación y perder la localidad de datos?
Timothy Shields
35

Se cita un ejemplo del nuevo documento adoptado: N3672, std :: opcional :

 optional<int> str2int(string);    // converts int to string if possible

int get_int_from_user()
{
     string s;

     for (;;) {
         cin >> s;
         optional<int> o = str2int(s); // 'o' may or may not contain an int
         if (o) {                      // does optional contain a value?
            return *o;                  // use the value
         }
     }
}
taocp
fuente
13
Debido a que puede pasar la información sobre si obtuvo into no una jerarquía de llamadas ascendente o descendente, en lugar de pasar algún valor "fantasma" "supuesto" que tiene un significado de "error".
Luis Machuca
1
@Wiz Este es realmente un gran ejemplo. (A) permite str2int()implementar la conversión como quiera, (B) independientemente del método para obtenerla string s, y (C) transmite el significado completo a través optional<int>de alguna forma estúpida de número mágico, bool/ referencia o asignación dinámica basada en haciéndolo.
underscore_d
10

pero no entiendo cuándo debería usarlo o cómo debería usarlo.

Considere cuando está escribiendo una API y desea expresar que el valor "no tener un retorno" no es un error. Por ejemplo, necesita leer datos de un socket, y cuando se completa un bloque de datos, lo analiza y lo devuelve:

class YourBlock { /* block header, format, whatever else */ };

std::optional<YourBlock> cache_and_get_block(
    some_socket_object& socket);

Si los datos adjuntos completaron un bloque analizable, puede procesarlo; de lo contrario, siga leyendo y agregando datos:

void your_client_code(some_socket_object& socket)
{
    char raw_data[1024]; // max 1024 bytes of raw data (for example)
    while(socket.read(raw_data, 1024))
    {
        if(auto block = cache_and_get_block(raw_data))
        {
            // process *block here
            // then return or break
        }
        // else [ no error; just keep reading and appending ]
    }
}

Editar: con respecto al resto de sus preguntas:

When is std :: opcional es una buena opción para usar

  • Cuando calcula un valor y necesita devolverlo, es mejor que la semántica regrese por valor que tomar una referencia a un valor de salida (que puede no generarse).

  • Cuando desee asegurarse de que el código del cliente tenga que verificar el valor de salida (el que escriba el código del cliente puede no verificar el error; si intenta usar un puntero no inicializado, obtendrá un volcado del núcleo; si intenta usar un no- Inicializado std :: opcional, obtienes una excepción capturable).

[...] y cómo compensa lo que no se encontró en el Estándar anterior (C ++ 11).

Antes de C ++ 11, tenía que usar una interfaz diferente para "funciones que pueden no devolver un valor": devolver por puntero y verificar NULL, o aceptar un parámetro de salida y devolver un código de error / resultado para "no disponible ".

Ambos imponen un esfuerzo extra y atención del implementador del cliente para hacerlo bien y ambos son una fuente de confusión (el primero empuja al implementador del cliente a pensar en una operación como una asignación y requiere que el código del cliente implemente la lógica de manejo del puntero y el segundo permite código de cliente para evitar el uso de valores no válidos / no inicializados).

std::optional Cuida muy bien los problemas que surgen con las soluciones anteriores.

utnapistim
fuente
Sé que son básicamente lo mismo, pero ¿por qué estás usando en boost::lugar de std::?
0x499602D2
44
Gracias, lo corregí (lo usé boost::optionalporque después de aproximadamente dos años de usarlo, está codificado en mi corteza prefrontal).
utnapistim
1
Esto me parece un mal ejemplo, ya que si se completan múltiples bloques, solo uno podría ser devuelto y el resto descartado. En su lugar, la función debería devolver una colección de bloques posiblemente vacía.
Ben Voigt
Tienes razón; fue un mal ejemplo; He modificado mi ejemplo para "analizar el primer bloque si está disponible".
utnapistim
4

A menudo uso opcionales para representar datos opcionales extraídos de los archivos de configuración, es decir, dónde se proporcionan opcionalmente esos datos (como con un elemento esperado, pero no necesario, dentro de un documento XML), para que pueda mostrar explícita y claramente si los datos estaban realmente presentes en el documento XML. Especialmente cuando los datos pueden tener un estado "no establecido", frente a un estado "vacío" y un estado "establecido" (lógica difusa). Con un opcional, establecer y no establecer es claro, también vacío sería claro con el valor de 0 o nulo.

Esto puede mostrar cómo el valor de "no establecido" no es equivalente a "vacío". En concepto, un puntero a un int (int * p) puede mostrar esto, donde no se establece un valor nulo (p == 0), se establece un valor de 0 (* p == 0) y está vacío, y cualquier otro valor (* p <> 0) se establece en un valor.

Para un ejemplo práctico, tengo una pieza de geometría extraída de un documento XML que tenía un valor llamado indicadores de representación, donde la geometría puede anular los indicadores de representación (conjunto), deshabilitar los indicadores de representación (establecer a 0), o simplemente no afectar las banderas de renderizado (no establecido), un opcional sería una forma clara de representar esto.

Claramente, un puntero a un int, en este ejemplo, puede lograr el objetivo, o mejor, un puntero compartido ya que puede ofrecer una implementación más limpia, sin embargo, diría que se trata de la claridad del código en este caso. ¿Es un nulo siempre un "no establecido"? Con un puntero, no está claro, ya que nulo significa literalmente no asignado o creado, aunque podría , pero no necesariamente significa "no establecido". Vale la pena señalar que se debe liberar un puntero y, en la buena práctica, se debe establecer en 0, sin embargo, como con un puntero compartido, un opcional no requiere una limpieza explícita, por lo que no existe la preocupación de mezclar la limpieza con el opcional no se ha configurado.

Creo que se trata de la claridad del código. La claridad reduce el costo de mantenimiento y desarrollo del código. Una comprensión clara de la intención del código es increíblemente valiosa.

El uso de un puntero para representar esto requeriría sobrecargar el concepto del puntero. Para representar "nulo" como "no establecido", normalmente puede ver uno o más comentarios a través del código para explicar esta intención. Esa no es una mala solución en lugar de una opción, sin embargo, siempre opto por la implementación implícita en lugar de comentarios explícitos, ya que los comentarios no son exigibles (como por compilación). Los ejemplos de estos elementos implícitos para el desarrollo (aquellos artículos en desarrollo que se proporcionan únicamente para imponer la intención) incluyen los diversos modelos de estilo C ++, "const" (especialmente en funciones miembro) y el tipo "bool", por nombrar algunos. Podría decirse que realmente no necesita estas características de código, siempre y cuando todos obedezcan intenciones o comentarios.

Kit10
fuente