长话短说。我在 hyle 库的实现中大量运用 tl::expected (对标 C++23 std::expected)配合 std::error_code 来处理错误。但是,在有大量不同返回类型 T 的 expected 时,Monadic members 并不好用,and_then() 并不适合表达这种逻辑。所以,我从中捻出一个例子,来展示一下如何用元编程解决这种情境中的问题。template<class T>using result = expected<T, std::error_code>;using error = unexpected<std::error_code>;result<void> dex_parser::parse() noexcept{ auto string_ids = read_string_ids(); auto type_ids = read_type_ids(); auto proto_ids = read_proto_ids(); auto field_ids = read_field_ids(); auto method_ids = read_method_ids(); auto class_defs = read_class_defs(); if (!string_ids.has_value()) { return error{ string_ids.error() }; } if (!type_ids.has_value()) { return error{ type_ids.error() }; } if (!proto_ids.has_value()) { return error{ proto_ids.error() }; } if (!field_ids.has_value()) { return error{ field_ids.error() }; } if (!method_ids.has_value()) { return error{ method_ids.error() }; } if (!class_defs.has_value()) { return error{ class_defs.error() }; } dex_.string_ids = std::move(*string_ids); dex_.type_ids = std::move(*type_ids); dex_.proto_ids = std::move(*proto_ids); dex_.field_ids = std::move(*field_ids); dex_.method_ids = std::move(*method_ids); dex_.class_defs = std::move(*class_defs); return {};}
这代码是在解析 dex 文件的不同区块,每个区块的结构类型都是不一样的,但全是返回 expected。代码本身没有问题,只是要做大量重复的结果检查,占了一半行数,很是麻烦。#define CHECK_EXPECTED(res) \ if (!res.has_value()) { \ return error{ res.error() }; \ }#define NO_ERROR_OCCURRED(...) GMP_FOR_EACH(CHECK_EXPECTED, __VA_ARGS__)result<void> dex_parser::parse() noexcept{ auto string_ids = read_string_ids(); auto type_ids = read_type_ids(); auto proto_ids = read_proto_ids(); auto field_ids = read_field_ids(); auto method_ids = read_method_ids(); auto class_defs = read_class_defs(); NO_ERROR_OCCURRED(string_ids, type_ids, proto_ids, field_ids, method_ids, class_defs) dex_.string_ids = std::move(*string_ids); dex_.type_ids = std::move(*type_ids); dex_.proto_ids = std::move(*proto_ids); dex_.field_ids = std::move(*field_ids); dex_.method_ids = std::move(*method_ids); dex_.class_defs = std::move(*class_defs); return {};}
NO_ERROR_OCCURRED 可以在预处理阶段生成前面的所有检查代码,它的参数是可变的,几十上百个检查也不是问题。但宏元编程是最后的手段,非必要不使用,这种情境下,它一定不是最优解法。template<typename... Args>result<void> no_error_occurred(const result<Args>&... args) noexcept{ result<void> ok; (void) ((args.has_value() || (ok = unexpected<std::error_code>(args.error()))) && ...); return ok;}
result<void> dex_parser::parse() noexcept{ auto string_ids = read_string_ids(); auto type_ids = read_type_ids(); auto proto_ids = read_proto_ids(); auto field_ids = read_field_ids(); auto method_ids = read_method_ids(); auto class_defs = read_class_defs(); auto ok = no_error_occurred(string_ids, type_ids, proto_ids, field_ids, method_ids, class_defs); if (!ok.has_value()) { return error{ ok.error() }; } dex_.string_ids = std::move(*string_ids); dex_.type_ids = std::move(*type_ids); dex_.proto_ids = std::move(*proto_ids); dex_.field_ids = std::move(*field_ids); dex_.method_ids = std::move(*method_ids); dex_.class_defs = std::move(*class_defs); return ok;}
no_error_occurred 的实现借助了 Variadic templates 和 Fold expressions,它们共同完成高抽象层次的编译期代码生成。实现的精妙在于,只借助 || 和 && 两个逻辑运算符,和其背后的 short-circuiting 运算规则,就表达了复杂的执行逻辑。这里面的深层思路,其实是充分运用了命题逻辑的思想,|| 对应析取,&& 对应合取,short-circuiting 对应蕴含。如果不用这种方式,就无法避开 ?: ternary operator。ternary operator 适合有两个分支的情况,是更高一层的逻辑符号,此时它并不合适。最后,大家还可以尝试使用 C++ Generative Metaprogramming6.2 节介绍的两种 Compile-Time Loop 方法(Expansion statements 和 IIFE)实现。这里便不写出来了,它们的实现难度,远不及前面介绍的那两种方式,效果则比不上第二种方式。