C ++ 2a中的错误处理将发生什么

图片


几周前是C ++世界的主要会议-CPPCON
从早上8点到晚上10点连续五天都有报道。 各种信仰的程序员讨论了C ++的未来,毒自行车,并思考了如何使C ++更容易。


令人惊讶的是,许多报告专门用于错误处理。 完善的方法不允许您获得最高性能或生成代码表。
C ++ 2a中有哪些创新等待着我们?


一点理论


按照惯例,程序中的所有错误情况都可以分为2个大类:


  • 致命错误。
  • 不是致命的或预期的错误。

致命错误


在它们之后,继续执行没有任何意义。
例如,这是取消引用空指针,通过内存,除以0或违反代码中的其他不变式。 当它们发生时,所有需要做的就是提供有关问题的最大信息并完成程序。


在C ++中 太多了 已经有足够的方法来完成程序:



图书馆甚至开始出现崩溃( 1、2、3 )数据的收集。


非致命错误


这些是程序逻辑提供的错误。 例如,在使用网络,将无效字符串转换为数字时出现错误等。 程序中此类错误的出现是按顺序出现的。 对于它们的处理,C ++有几种普遍接受的策略。
我们将通过一个简单的示例更详细地讨论它们:


让我们尝试使用不同的错误处理方法编写一个函数void addTwo()
该函数应读取2行,将其转换为int并打印总和。 需要处理IO错误,溢出和转换为数字。 我将省略无趣的实现细节。 我们将考虑3种主要方法。


1.例外


 //     //   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; } } 

C ++中的异常使您可以集中处理错误,而无需 不必要
但是您必须为此付出很多麻烦。


  • 处理异常涉及的开销非常大;您不能经常抛出异常。
  • 最好不要抛出构造函数/析构函数的异常并遵守RAII。
  • 通过功能的签名,无法理解哪些异常会从功能中溢出。
  • 二进制文件的大小由于附加的异常支持代码而增加。

2.返回码


继承自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; } 

看起来不是很好吗?


  1. 您不能返回函数的实际值。
  2. 忘记处理错误是很容易的(上次您检查printf的返回码吗?)。
  3. 您必须在每个函数旁边编写错误处理代码。 这样的代码很难阅读。
    使用C ++ 17和C ++ 2a将依次解决所有这些问题。

3. C ++ 17和nodiscard


nodiscard在C ++ 17中。
如果在函数声明之前指定它,则不检查返回值将导致编译器警告。


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

您还可以为类,结构或枚举类指定nodiscard
在这种情况下,属性操作扩展到所有返回标记为nodiscard的类型的值的函数。


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

我不会提供带有nodiscard代码。


C ++ 17 std ::可选


在C ++ 17中,出现了std::optional<T>
让我们看看现在的代码。


 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; } 

您可以从函数中删除in-out参数,代码将变得更加简洁。
但是,我们正在丢失错误信息。 目前尚不清楚何时以及发生了什么问题。
您可以将std::optional替换为std::variant<ResultType, ValueType>
该代码的含义与std::optional相同,但是比较麻烦。


C ++ 2a和std ::预期


std::expected<ResultType, ErrorType> -一种特殊的模板类型 ,它很可能属于最近的不完整标准。
它有2个参数。


  • ReusltType是期望值。
  • ErrorType错误类型。
    std::expected值可以包含预期值或错误。 使用这种类型将是这样的:
     std::expected<int, string> ok = 0; expected<int, string> notOk = std::make_unexpected("something wrong"); 

这与通常的variant有何不同? 有什么特别之处?
std::expected将是monad
建议在std::expected上支持一系列操作,就像在monad上一样: mapcatch_errorbindcatch_errorreturnthen
使用这些函数,可以将函数调用链接到一个链中。


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

假设我们有返回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); 

以下仅是伪代码;不能强制其在任何现代编译器中工作。
您可以尝试从Haskell借用do语法来对monad 进行录制操作。 为什么不允许它这样做:


 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) } 

一些作者建议使用以下语法:


 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; } 

编译器自动将这样的代码块转换为函数调用序列。 如果在某个时候函数返回的值不是预期的,则计算链将中断。 是的,作为错误类型,您可以使用标准中已经存在的异常类型: std::runtime_errorstd::out_of_range等。


如果您可以很好地设计语法,那么std::expected将允许您编写简单而有效的代码。


结论


没有理想的方法来处理错误。 直到最近,在C ++中,除了monads之外,几乎所有其他错误处理方法都可以使用。
在C ++ 2a中,可能会出现所有可能的方法。


在该主题上应读什么


  1. 实际提案
  2. 关于std ::的演讲,预计将与CPPCON一起使用
  3. Andrei Alexandrescu关于std ::预期在C ++俄罗斯中
  4. 最近关于Reddit提案的讨论

Source: https://habr.com/ru/post/zh-CN426965/


All Articles