¿Qué pasará con el manejo de errores en C ++ 2a?

imagen


Hace un par de semanas fue la conferencia principal en el mundo C ++: CPPCON .
Cinco días consecutivos de 8 a.m. a 10 p.m. hubo informes. Los programadores de todas las religiones discutieron el futuro de C ++, las bicicletas envenenadas y pensaron cómo hacer que C ++ sea más fácil.


Sorprendentemente, muchos informes se dedicaron al manejo de errores. Los enfoques bien establecidos no le permiten alcanzar el máximo rendimiento o pueden generar hojas de códigos.
¿Qué innovaciones nos esperan en C ++ 2a?


Poco de teoría


Convencionalmente, todas las situaciones erróneas en el programa se pueden dividir en 2 grandes grupos:


  • Errores fatales.
  • No son fatales ni se esperan errores.

Errores fatales


Después de ellos, no tiene sentido continuar la ejecución.
Por ejemplo, esto está desreferenciando un puntero nulo, pasando por la memoria, dividiendo entre 0 o violando otras invariantes en el código. Todo lo que debe hacerse cuando ocurren es proporcionar la máxima información sobre el problema y completar el programa.


En c ++ demasiado Ya hay suficientes formas de completar el programa:



Las bibliotecas incluso comienzan a aparecer para recopilar datos sobre bloqueos ( 1 , 2 , 3 ).


Errores no fatales


Estos son errores proporcionados por la lógica del programa. Por ejemplo, errores al trabajar con la red, convertir una cadena no válida a un número, etc. La aparición de tales errores en el programa está en el orden de las cosas. Para su procesamiento, hay varias tácticas generalmente aceptadas en C ++.
Hablaremos de ellos con más detalle usando un ejemplo simple:


Intentemos escribir una función void addTwo() usando diferentes enfoques para el manejo de errores.
La función debe leer 2 líneas, convertirlas a int e imprimir la suma. Necesidad de manejar errores IO, desbordamiento y conversión a número. Omitiré detalles de implementación poco interesantes. Consideraremos 3 enfoques principales.


1. Excepciones


 //     //   IO  std::runtime_error std::string readLine(); //    int //     std::invalid_argument int parseInt(const std::string& str); //  a  b //     std::overflow_error int safeAdd(int a, int b); void addTwo() { try { std::string aStr = readLine(); std::string bStr = readLine(); int a = parseInt(aStr); int b = parseInt(bStr); std::cout << safeAdd(a, b) << std::endl; } catch(const std::exeption& e) { std::cout << e.what() << std::endl; } } 

Las excepciones en C ++ le permiten manejar errores centralmente sin innecesarios ,
pero tienes que pagar por esto con un montón de problemas.


  • la sobrecarga involucrada en el manejo de excepciones es bastante grande; a menudo no se pueden lanzar excepciones.
  • es mejor no lanzar excepciones de constructores / destructores y observar RAII.
  • Por la firma de la función, es imposible entender qué excepción puede salir de la función.
  • el tamaño del archivo binario aumenta debido a un código de soporte de excepción adicional.

2. Códigos de retorno


Enfoque clásico heredado de C.


 bool readLine(std::string& str); bool parseInt(const std::string& str, int& result); bool safeAdd(int a, int b, int& result); void processError(); void addTwo() { std::string aStr; int ok = readLine(aStr); if (!ok) { processError(); return; } std::string bStr; ok = readLine(bStr); if (!ok) { processError(); return; } int a = 0; ok = parseInt(aStr, a); if (!ok) { processError(); return; } int b = 0; ok = parseInt(bStr, b); if (!ok) { processError(); return; } int result = 0; ok = safeAdd(a, b, result); if (!ok) { processError(); return; } std::cout << result << std::endl; } 

¿No se ve muy bien?


  1. No puede devolver el valor real de una función.
  2. Es muy fácil olvidarse de manejar el error (la última vez que verificó el código de retorno de printf?).
  3. Debe escribir el código de manejo de errores al lado de cada función. Tal código es más difícil de leer.
    El uso de C ++ 17 y C ++ 2a solucionará todos estos problemas en secuencia.

3. C ++ 17 y no descarte


El nodiscard en C ++ 17.
Si lo especifica antes de la declaración de la función, la ausencia de comprobación del valor de retorno provocará una advertencia del compilador.


 [[nodiscard]] bool doStuff(); /* ... */ doStuff(); //  ! bool ok = doStuff(); // . 

También puede especificar nodiscard para una clase, estructura o clase de enumeración.
En este caso, la acción de atributo se extiende a todas las funciones que devuelven valores del tipo etiquetado como nodiscard .


 enum class [[nodiscard]] ErrorCode { Exists, PermissionDenied }; ErrorCode createDir(); /* ... */ createDir(); 

No proporcionaré código con nodiscard .


C ++ 17 std :: opcional


En C ++ 17, std::optional<T> .
Veamos cómo se ve el código ahora.


 std::optional<std::string> readLine(); std::optional<int> parseInt(const std::string& str); std::optional<int> safeAdd(int a, int b); void addTwo() { std::optional<std::string> aStr = readLine(); std::optional<std::string> bStr = readLine(); if (aStr == std::nullopt || bStr == std::nullopt){ std::cerr << "Some input error" << std::endl; return; } std::optional<int> a = parseInt(*aStr); std::optional<int> b = parseInt(*bStr); if (!a || !b) { std::cerr << "Some parse error" << std::endl; return; } std::optional<int> result = safeAdd(*a, *b); if (!result) { std::cerr << "Integer overflow" << std::endl; return; } std::cout << *result << std::endl; } 

Puede eliminar argumentos entrantes y salientes de las funciones y el código se volverá más limpio.
Sin embargo, estamos perdiendo información de error. No quedó claro cuándo y qué salió mal.
Puede reemplazar std::optional con std::variant<ResultType, ValueType> .
El significado del código es el mismo que con std::optional , pero más engorroso.


C ++ 2a y std :: esperado


std::expected<ResultType, ErrorType> - un tipo de plantilla especial , probablemente caerá en el estándar incompleto más cercano.
Tiene 2 parámetros.


  • ReusltType es el valor esperado.
  • ErrorType : tipo de error.
    std::expected puede contener el valor esperado o un error. Trabajar con este tipo será algo como esto:
     std::expected<int, string> ok = 0; expected<int, string> notOk = std::make_unexpected("something wrong"); 

¿Cómo difiere esto de la variant habitual? ¿Qué lo hace especial?
std::expected será una mónada .
Se propone soportar un montón de operaciones en std::expected como en una mónada: map , catch_error , bind , unwrap , return y then .
Usando estas funciones, puede encadenar llamadas de función en una cadena.


 getInt().map([](int i)return i * 2;) .map(integer_divide_by_2) .catch_error([](auto e) return 0; ); 

Supongamos que tenemos funciones para devolver std::expected .


 std::expected<std::string, std::runtime_error> readLine(); std::expected<int, std::runtime_error> parseInt(const std::string& str); std::expected<int, std::runtime_error> safeAdd(int a, int b); 

A continuación se muestra solo un pseudocódigo; no se puede forzar a trabajar en ningún compilador moderno.
Puede intentar tomar prestada de Haskell la sintaxis do para grabar operaciones en mónadas. ¿Por qué no permitir que lo haga?


 std::expected<int, std::runtime_error> result = do { auto aStr <- readLine(); auto bStr <- readLine(); auto a <- parseInt(aStr); auto b <- parseInt(bStr); return safeAdd(a, b) } 

Algunos autores sugieren esta sintaxis:


 try { auto aStr = try readLine(); auto bStr = try readLine(); auto a = try parseInt(aStr); auto b = try parseInt(bStr); std::cout result << std::endl; return safeAdd(a, b) } catch (const std::runtime_error& err) { std::cerr << err.what() << std::endl; return 0; } 

El compilador convierte automáticamente dicho bloque de código en una secuencia de llamadas a funciones. Si en algún momento la función devuelve no lo que se espera de ella, la cadena de cálculo se romperá. Sí, y como tipo de error, puede usar los tipos de excepción que ya existen en el estándar: std::runtime_error , std::out_of_range , etc.


Si puede diseñar bien la sintaxis, entonces std::expected le permitirá escribir código simple y eficiente.


Conclusión


No hay una forma ideal de manejar los errores. Hasta hace poco, en C ++ había casi todos los métodos posibles de manejo de errores, excepto las mónadas.
En C ++ 2a, es probable que aparezcan todos los métodos posibles.


Qué leer y ver sobre el tema


  1. Propuesta actual
  2. Discurso sobre std :: esperado con CPPCON .
  3. Andrei Alexandrescu sobre std :: esperado en C ++ Rusia .
  4. Discusión más o menos reciente de la propuesta sobre Reddit .

Source: https://habr.com/ru/post/es426965/


All Articles