
几周前是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; }
看起来不是很好吗?
- 您不能返回函数的实际值。
- 忘记处理错误是很容易的(上次您检查printf的返回码吗?)。
- 您必须在每个函数旁边编写错误处理代码。 这样的代码很难阅读。
使用C ++ 17和C ++ 2a将依次解决所有这些问题。
3. C ++ 17和nodiscard
nodiscard
在C ++ 17中。
如果在函数声明之前指定它,则不检查返回值将导致编译器警告。
[[nodiscard]] bool doStuff(); 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个参数。
这与通常的variant
有何不同? 有什么特别之处?
std::expected
将是monad 。
建议在std::expected
上支持一系列操作,就像在monad上一样: map
, catch_error
, bind
, catch_error
, return
和then
。
使用这些函数,可以将函数调用链接到一个链中。
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_error
, std::out_of_range
等。
如果您可以很好地设计语法,那么std::expected
将允许您编写简单而有效的代码。
结论
没有理想的方法来处理错误。 直到最近,在C ++中,除了monads之外,几乎所有其他错误处理方法都可以使用。
在C ++ 2a中,可能会出现所有可能的方法。
在该主题上应读什么
- 实际提案 。
- 关于std ::的演讲,预计将与CPPCON一起使用 。
- Andrei Alexandrescu关于std ::预期在C ++俄罗斯中 。
- 最近关于Reddit提案的讨论 。