模板错误与调试
概述
- 模板报错之所以经常出现几百行,不是因为真正存在几百个错误,而是因为编译器必须告诉你:
- 最外层调用了哪个模板
- 它实例化了哪个内部模板
- 内部模板又实例化了什么
- 最终哪个表达式不合法
最小示例
- 下面代码不能编译
|
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <cstddef> template<class T> std::size_t object_size(const T& object) { return object.size(); } int main() { return object_size(42); } |
|
1 2 3 |
In instantiation of ‘std::size_t object_size(const T&) [with T = int]’ required from here error: request for member ‘size’ in ‘object’, which is of non-class type ‘const int’ |
模板代码什么时候被检查
- 模板通常经历以下阶段:
| 阶段 | 主要工作 |
| 模板定义 | 检查语法和不依赖模板参数的名字 |
| 模板实参推导 | 从调用参数推导 T |
| 替换与约束检查 | 将推导出的类型代入声明 |
| 模板实例化 | 检查依赖模板参数的代码 |
| 代码生成与链接 | 生成具体函数并解析符号 |
- 考虑
|
1 2 3 4 5 6 |
template<class T> void process(T& value) { write_log(); // 不依赖 T value.serialize(); // 依赖 T } |
write_log()是非依赖名字- 编译器在看到模板定义时就要能找到它
serialize()依赖于T,只有实例化时才能判断是否合法
|
1 2 3 4 5 6 |
struct Message { void serialize(); }; Message message; process(message); // 合法 |
|
1 2 3 4 |
// 而 int value = 42; process(value); // 实例化后发现 int 没有 serialize() |
诊断信息应该从哪里开始看
- 第一步:找最先出现的用户代码
- 第二步:找到实际模板参数
- 第三步:找最底层的第一个真实错误
- 第四步:阅读候选函数被拒绝的原因
typename 错误
- 考虑
|
1 2 3 4 5 |
template<class T> void create_value() { T::value_type value{}; } |
- 编译器看到
T::value_type,不能确定它是:- 嵌套类型
- 静态数据成员
- 枚举值
- 其他名字
|
1 2 3 4 5 6 7 |
// 需要明确告诉编译器这是类型 template<class T> void create_value() { typename T::value_type value{}; } |
- 规则是
- 依赖于模板参数的限定名,如果要把它当作类型,通常需要在前面写
typename
- 依赖于模板参数的限定名,如果要把它当作类型,通常需要在前面写
|
1 2 3 4 5 6 |
template<class Container> typename Container::value_type first(const Container& container) { return container.front(); } |
|
1 2 3 4 |
// 在别名中也需要 template<class T> using ValueType = typename T::value_type; |
template 消歧义
- 考虑
|
1 2 3 4 5 |
template<class T> void convert(T& object) { object.convert<int>(); } |
- 因为
object的类型依赖于T,编译器可能无法确定:<是模板实参列表的开始,还是小于运算符
|
1 2 3 4 5 |
template<class T> void convert(T& object) { object.template convert<int>(); } |
- 规则是
- 在依赖对象或依赖限定名后调用成员模板时,可能需要使用
.template、->template或::template
- 在依赖对象或依赖限定名后调用成员模板时,可能需要使用
|
1 2 3 4 5 |
object.template get<0>(); pointer->template convert<int>(); T::template rebind<int>; |
|
1 2 3 |
// 如果最后一个名字还是类型,则可能同时需要 typename T::template rebind<int>::type |
依赖基类与this->
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
template<class T> struct Base { void run() { } }; template<class T> struct Derived : Base<T> { void start() { run(); // 可能报错:找不到 run } }; |
Base<T>是依赖基类- 编译器在定义
Derived<T>时不会直接到依赖基类中查找非依赖名字run
- 编译器在定义
|
1 2 3 |
// this 的类型依赖于模板参数,因此 this->run() 成为依赖表达式,查找会推迟到实例化阶段 this->run(); |
|
1 |
Base<T>::run(); |
|
1 2 3 4 5 6 |
using Base<T>::run; void start() { run(); } |
两阶段名字查找
- 考虑
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <iostream> void handle(long) { std::cout << "long\n"; } template<class T> void dispatch(T value) { handle(value); } void handle(int) { std::cout << "int\n"; } int main() { dispatch(42); } |
|
1 2 |
// 以为输出int // 但标准的两阶段查找下,通常会调用long |
- 原因是
- 模板定义时,普通名字查找只看到
handle(long)</li> <li>handle(int)在模板定义之后才声明 - 实例化时还会进行
ADL - 但内置类型
int没有关联命名空间,ADL找不到后面的重载
- 模板定义时,普通名字查找只看到
|
1 2 3 4 5 6 7 8 |
void handle(long); void handle(int); template<class T> void dispatch(T value) { handle(value); } |
SFINAE错误与硬错误
- 考虑
|
1 2 3 4 5 6 |
template<class T> auto get_size(const T& value) -> decltype(value.size()) { return value.size(); } |
|
1 2 3 4 5 |
// 如果调用 get_size(42); // 把int代入返回类型 decltype(value.size()) |
- 失败发生在函数模板声明的直接替换上下文中,因此该候选函数可以被移出重载集合
- 但如果失败发生在已经选中的函数体中
|
1 2 3 4 5 6 7 8 |
template<class T> auto get_size(const T& value) { return value.size(); } // 调用get_size(42); // 函数已经被选择,实例化函数体时才发现错误。这是硬错误,不是 SFINAE |
if constexpr 减少无效实例化
- 普通
if的两个分支都必须合法
|
1 2 3 4 5 6 7 8 9 |
template<class T> void print(const T& value) { if (std::is_integral_v<T>) { std::cout << value; } else { std::cout << value.size(); } } |
- 当
T = int时,虽然运行时不会进入else,但value.size()仍然必须能够编译,所以会报错
C++17使用- 未选择的分支不会针对当前
T实例化
- 未选择的分支不会针对当前
|
1 2 3 4 5 6 7 8 9 |
template<class T> void print(const T& value) { if constexpr (std::is_integral_v<T>) { std::cout << value; } else { std::cout << value.size(); } } |
使用 static_assert 提前报错
- 假设一个线程池任务要求回调能够接受
|
1 2 |
std::error_code std::size_t |
- 可以在
API边界检查
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <cstddef> #include <functional> #include <system_error> #include <type_traits> #include <utility> template<class Handler> void register_handler(Handler&& handler) { using H = std::remove_reference_t<Handler>; static_assert( std::is_invocable_v< H&, const std::error_code&, std::size_t >, "handler must accept " "(const std::error_code&, std::size_t)" ); // 保存 handler } |
延迟触发 static_assert
- 模板中经常需要一个只有特定分支被实例化时才触发的错误
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
template<class> inline constexpr bool always_false_v = false; template<class T> void encode(const T&) { if constexpr (std::is_integral_v<T>) { // 编码整数 } else if constexpr (std::is_floating_point_v<T>) { // 编码浮点数 } else { static_assert( always_false_v<T>, "encode() does not support this type" ); } } |
- 为什么不直接写
- 在
C++17/C++20中,直接的false可能在模板定义时就导致错误,即使这个分支从未被实例化
- 在
|
1 |
static_assert(false, "unsupported type"); |
C++23放宽了模板中的static_assert(false),但为了兼容C++17/C++20编译器,工程中仍经常使用依赖类型的:
|
1 |
always_false_v<T> |
如何让编译器打印精确类型
- 方法一:故意实例化不完整类型
|
1 2 3 4 5 6 7 |
template<class> struct DumpType; int value = 42; const int& reference = value; DumpType<decltype((reference))> dump; |
|
1 2 |
decltype(reference) // 声明类型规则 decltype((reference)) // 表达式类型和值类别规则 |
- 方法二:依赖
static_assert
|
1 2 3 4 5 6 7 8 |
template<class> inline constexpr bool always_false_v = false; template<class T> void inspect_type() { static_assert(always_false_v<T>); } |
|
1 2 3 |
// 模板实例化上下文通常会显示 T inspect_type<decltype(expression)>(); |
- 方法三:编译器专用函数签名
|
1 2 3 4 5 6 7 8 9 |
template<class T> void print_type() { #if defined(__clang__) || defined(__GNUC__) std::cout << __PRETTY_FUNCTION__ << '\n'; #elif defined(_MSC_VER) std::cout << __FUNCSIG__ << '\n'; #endif } |
variant 报错应该怎样看
- 假设
|
1 2 |
using Message = std::variant<Login, Logout, Heartbeat>; |
- 访问器漏掉
Heartbeat
|
1 2 3 4 5 6 7 |
std::visit( overloaded{ [](const Login&) {}, [](const Logout&) {} }, message ); |
|
1 2 3 4 5 6 7 |
// 不要调试 std::variant 内部实现 std::invoke std::__invoke_result std::visit variant constexpr table |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 直接还原问题 访问器类型: overloaded<LoginLambda, LogoutLambda> variant 候选类型: Login Logout Heartbeat 逐个检查: visitor(Login) 合法 visitor(Logout) 合法 visitor(Heartbeat) 不合法 |
C++20 Concepts 改善诊断
C++17可以使用
|
1 2 3 4 5 6 |
template<class T> auto print_size(const T& value) -> decltype(value.size(), void()) { std::cout << value.size(); } |
C++20可以把要求直接写出来
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <concepts> #include <cstddef> #include <iostream> template<class T> concept HasSize = requires(const T& value) { { value.size() } -> std::convertible_to<std::size_t>; }; template<HasSize T> void print_size(const T& value) { std::cout << value.size(); } |
编译器诊断选项
GCC可以使用
|
1 2 3 4 5 6 |
g++ -std=c++20 \ -Wall -Wextra -pedantic-errors \ -ftemplate-backtrace-limit=20 \ -fconcepts-diagnostics-depth=4 \ -fno-elide-type \ main.cpp |
| 选项 | 作用 |
-ftemplate-backtrace-limit=20 |
限制实例化回溯长度 |
-ftemplate-backtrace-limit=0 |
不限制,显示完整回溯 |
-fconcepts-diagnostics-depth=4 |
展开更多 Concepts 失败原因 |
-fno-elide-type |
尽量不省略复杂类型 |
-fmax-errors=1 |
只看第一个错误,减少连锁噪声 |
Clang常用
|
1 2 3 4 5 |
clang++ -std=c++20 \ -Wall -Wextra -pedantic-errors \ -ftemplate-backtrace-limit=20 \ -ferror-limit=1 \ main.cpp |
MSVC推荐至少开启
|
1 2 3 |
/std:c++20 /permissive- /diagnostics:caret |
C++20 Concepts与requires
概述
Concepts是C++20的语言特性,用于直接表达模板参数必须满足的条件- 它主要解决三个问题:
- 防止不满足要求的类型进入模板
- 改善模板错误信息
- 根据约束强弱进行重载排序
从 SFINAE 到 Concepts
C++17中,约束“T必须是整数”可能写成
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <type_traits> template< class T, std::enable_if_t< std::is_integral_v<T>, int > = 0 > T twice(T value) { return value * 2; } |
C++20可以写成
|
1 2 3 4 5 6 7 |
#include <concepts> template<std::integral T> T twice(T value) { return value * 2; } |
|
1 2 |
twice(42); // 正确 twice(3.14); // 约束不满足 |
- 区别不仅是语法更短
Concepts正式加入了模板重载决议和约束偏序,编译器能够理解“哪个模板约束更强”
最小可运行示例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <concepts> #include <cstddef> #include <iostream> #include <string> #include <vector> template<class T> concept HasSize = requires(const T& value) { { value.size() } -> std::convertible_to<std::size_t>; }; template<HasSize T> void print_size(const T& value) { std::cout << value.size() << '\n'; } int main() { std::vector<int> numbers{1, 2, 3}; std::string text{"hello"}; print_size(numbers); // 3 print_size(text); // 5 // print_size(42); // 编译错误:HasSize<int> 不满足 } |
Concept
|
1 2 3 4 5 6 |
template<class T> concept HasSize = /* 编译期布尔约束 */; HasSize<std::vector<int>> // true HasSize<std::string> // true HasSize<int> // false |
四种约束函数模板的写法
- 约束模板参数
|
1 2 3 4 5 |
template<std::integral T> T twice(T value) { return value * 2; } |
- 前置
requires子句
|
1 2 3 4 5 6 |
template<class T> requires std::integral<T> T twice(T value) { return value * 2; } |
|
1 2 3 4 5 |
// 适合组合多个条件 template<class T> requires std::integral<T> && (sizeof(T) >= 4) T twice(T value); |
- 后置
requires子句- 常用于成员函数,因为约束可以靠近函数声明末尾
|
1 2 3 4 5 6 |
template<class T> T twice(T value) requires std::integral<T> { return value * 2; } |
- 简写函数模板
|
1 2 3 4 |
auto twice(std::integral auto value) { return value * 2; } |
|
1 2 3 4 5 6 7 |
// 编译器会为 auto 创建一个隐藏的模板参数,近似于 template<std::integral T> auto twice(T value) { return value * 2; } |
requires 子句与 requires 表达式
- 这两个东西名字相同,但作用不同
requires子句- 决定模板是否有资格参与重载
|
1 2 3 |
template<class T> requires HasSize<T> void process(const T&); |
requires表达式- 检查一组类型或表达式是否合法
|
1 2 3 4 5 |
template<class T> concept HasSize = requires(const T& value) { value.size(); }; |
|
1 2 3 4 5 6 7 8 9 10 |
// 有时会看到两个连续的requires // 第一个是 requires 子句,第二个是 requires 表达式 template<class T> requires requires(const T& value) { value.size(); } void process(const T& value) { } |
|
1 2 3 4 5 6 7 8 9 10 |
// 通常定义命名 Concept 更易读 template<class T> concept HasSize = requires(const T& value) { value.size(); }; template<HasSize T> void process(const T& value); |
四种 requirement
requires表达式中可以写四种要求:- 简单要求
|
1 2 3 4 5 |
template<class T> concept Addable = requires(T left, T right) { left + right; }; |
|
1 2 3 4 5 6 7 |
// 它只检查 left + right // 是否合法,不执行这个表达式,也不检查具体返回类型 // requires 表达式中的参数是用于类型检查的虚拟参数 requires(T left, T right) // 不会真正创建对象,也不要求 T 默认构造 |
- 类型要求
- 使用
typename检查某个嵌套类型是否存在
- 使用
|
1 2 3 4 5 |
template<class T> concept HasValueType = requires { typename T::value_type; }; |
|
1 2 |
HasValueType<std::vector<int>> // true HasValueType<int> // false |
|
1 2 3 4 5 6 7 8 9 10 |
// 还可以检查多个类型 // 类型要求只检查名字是否代表合法类型,不检查该类型是否能完成其他操作 template<class T> concept ContainerTypes = requires { typename T::value_type; typename T::iterator; typename T::const_iterator; }; |
- 复合要求
|
1 2 3 |
{ expression } noexcept -> ReturnTypeConstraint; |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// 它同时检查 // left.swap(right) 合法; // 表达式保证不抛异常; // 返回类型严格为 void template<class T> concept SwappableMember = requires(T& left, T& right) { { left.swap(right) } noexcept -> std::same_as<void>; }; |
|
1 2 3 4 5 |
// noexcept 和返回类型约束都可以省略 { value.size() }; { value.size() } noexcept; { value.size() } -> std::convertible_to<std::size_t>; |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// 注意,箭头后的 Concept 检查的是: decltype((expression)) // 因此 { value.front() } -> std::same_as<int&>; // 会精确检查引用和const // 对于 requires(const std::vector<int>& value) { { value.front() } -> std::same_as<const int&>; } 返回类型是 const int& |
- 嵌套要求
|
1 |
requires condition; |
|
1 2 3 4 5 6 |
template<class T> concept SmallInteger = requires { requires std::integral<T>; requires sizeof(T) <= 4; }; |
|
1 2 3 4 5 6 |
// 更常见是直接组合 template<class T> concept SmallInteger = std::integral<T> && (sizeof(T) <= 4); |
|
1 2 3 4 5 6 7 8 9 10 |
// 嵌套要求在条件依赖前面声明的虚拟参数时很有用 template<class T> concept PositiveSizedRange = requires(const T& value) { value.size(); requires sizeof(typename T::value_type) > 0; }; |
一个完整的 Range Concept
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <concepts> #include <cstddef> template<class T> concept SimpleRange = requires(T& range, const T& const_range) { typename T::value_type; range.begin(); range.end(); const_range.begin(); const_range.end(); { const_range.size() } -> std::convertible_to<std::size_t>; }; |
|
1 2 3 4 5 6 7 |
template<SimpleRange Range> void inspect(const Range& range) { for (const auto& value : range) { // ... } } |
- 不过这个
Concept仍然只是检查了一些语法,不能证明:begin()和end()表示同一个范围- 迭代一定会结束
size()与元素数量一致- 迭代器不会悬空
- 遍历一定是线程安全的
size()一定是O(1)
标准库常用 Concepts
|
1 |
#include <concepts> |
- 常见类型关系
|
1 2 3 4 |
std::same_as<T, U> std::derived_from<Derived, Base> std::convertible_to<From, To> std::common_reference_with<T, U> |
- 常见对象能力
|
1 2 3 4 5 6 |
std::constructible_from<T, Args...> std::default_initializable<T> std::move_constructible<T> std::copy_constructible<T> std::movable<T> std::copyable<T> |
- 常见类型分类
|
1 2 3 4 |
std::integral<T> std::signed_integral<T> std::unsigned_integral<T> std::floating_point<T> |
- 可调用对象
|
1 2 3 |
std::invocable<F, Args...> std::regular_invocable<F, Args...> std::predicate<F, Args...> |
- 赋值和交换
|
1 2 3 |
std::assignable_from<L, R> std::swappable<T> std::swappable_with<T, U> |
- 注意
|
1 2 3 4 |
std::integral<bool> // true std::integral<char> // true // 因为它基于语言中的整数类型分类,不等于“适合参与所有数值运算” |
|
1 2 3 4 5 6 7 8 9 |
// 如果业务接口不应该接受 bool,要额外排除 template<class T> concept NumericInteger = std::integral<T> && (!std::same_as< std::remove_cv_t<T>, bool >); |
same_as 与 convertible_to
- 考虑
|
1 2 3 4 5 |
{ value.size() } -> std::same_as<std::size_t>; // 这要求返回类型精确等于 std::size_t |
|
1 2 |
// 如果自定义容器返回 unsigned int // 即使能转换成 std::size_t,约束仍然失败 |
|
1 2 3 4 5 |
// 如果接口只需要能够转换,应写 { value.size() } -> std::convertible_to<std::size_t>; |
Concept |
使用条件 |
same_as<T> |
类型本身是接口语义的一部分 |
convertible_to<T> |
只需要能安全用于目标位置 |
| 简单要求 | 只关心表达式是否合法 |
约束参与重载排序
Concepts不只是移除非法候选,还可以判断哪个候选“更受约束”
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <concepts> #include <iostream> template<class T> concept SmallIntegral = std::integral<T> && (sizeof(T) <= 2); void send(std::integral auto) { std::cout << "integral\n"; } void send(SmallIntegral auto) { std::cout << "small integral\n"; } int main() { send(short{1}); send(int{1}); } |
|
1 2 |
small integral integral |
SmallIntegral<T>包含std::integral<T>的全部要求,并增加了尺寸约束,因此它更具体
为什么推荐复用命名 Concept
- 下面两个重载看起来一个更严格
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
template<class T> requires requires(T value) { --value; } void process(T); template<class T> requires requires(T value) { --value; } && requires(T value) { *value; } void process(T); |
|
1 2 3 |
// 但两处分别书写的 requires(T value) { --value; } // 可能产生不同的原子约束,编译器不一定认为第二个包含第一个,最终可能产生重载歧义 |
- 更可靠的方式是复用命名
Concept
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
template<class T> concept Decrementable = requires(T value) { --value; }; template<class T> concept Dereferenceable = requires(T value) { *value; }; template<class T> concept ReverseIterator = Decrementable<T> && Dereferenceable<T>; |
|
1 2 3 4 5 |
template<Decrementable T> void process(T); template<ReverseIterator T> void process(T); |
requires 的短路行为
|
1 2 3 4 5 6 |
template<class T> concept IntegralValueType = requires { typename T::value_type; } && std::integral<typename T::value_type>; |
- 如果
T没有value_type,左侧约束为false,右侧不再进行会产生硬错误的无效替换
万能引用中的 Concept 陷阱
- 下面的函数不一定接受整数左值
|
1 2 3 |
void process(std::integral auto&& value) { } |
|
1 2 3 4 5 6 7 8 9 10 11 |
int number = 42; process(number); // 近似展开 template<std::integral T> void process(T&& value); // 对于左值 number,T 可能推导为 int& // 但 std::integral<int&> // false |
- 如果希望接受任意
cv/ref形式的整数,应约束去掉引用后的类型
|
1 2 3 4 5 6 7 |
template<class T> requires std::integral< std::remove_cvref_t<T> > void process(T&& value) { } |
|
1 2 3 4 5 |
// 或者如果不需要完美转发,直接按值接收 void process(std::integral auto value) { } |
- 因此,写
Concept时必须明确- 约束的是调用表达式中的原始类型、引用类型,还是最终存储的衰变类型?
异步回调的工程 Concept
- 假设
async_receive()会保存回调,并在收到数据后这样调用
|
1 |
handler(error, bytes_received); |
- 可以定义
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <concepts> #include <cstddef> #include <functional> #include <system_error> #include <type_traits> #include <utility> template<class Handler> concept ReceiveHandler = std::move_constructible<Handler> && requires( Handler& handler, const std::error_code& error, std::size_t bytes_received ) { { std::invoke( handler, error, bytes_received ) } -> std::same_as<void>; }; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 公共接口 template<class Handler> requires ReceiveHandler<std::decay_t<Handler>> void async_receive(Handler&& handler) { using StoredHandler = std::decay_t<Handler>; StoredHandler stored{ std::forward<Handler>(handler) }; // 将 stored 移入异步操作 } |
- 为什么约束
std::decay_t<Handler>- 因为真正保存到异步操作中的通常不是原始
Handler&&,而是衰变后的拥有型对象 - 去掉引用
- 去掉顶层
const - 数组和函数发生退化
- 因为真正保存到异步操作中的通常不是原始
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 是否必须返回 void // 如果框架会忽略回调返回值,可以只检查调用是否合法 template<class Handler> concept ReceiveHandler = std::move_constructible<Handler> && requires( Handler& handler, const std::error_code& error, std::size_t bytes ) { std::invoke(handler, error, bytes); }; |
Concept 检查不了生命周期
- 下面的
lambda满足ReceiveHandler:- 但如果异步操作执行时
response已经销毁,仍然会产生悬空引用
- 但如果异步操作执行时
|
1 2 3 4 5 6 7 8 9 10 11 12 |
std::string response; async_receive( [&response]( const std::error_code&, std::size_t ) { response += "received"; } ); // Concept 只能知道 handler(error, size) 在类型系统中能否调用 |
- 它不能证明:
- 引用捕获对象是否存活
- 回调是否线程安全
- 回调是否阻塞
- 回调是否会抛异常
- socket 是否已关闭
- 执行线程是否正确
- 回调是否只执行一次
Concept、SFINAE、static_assert和tag dispatch
| 技术 | 作用 |
SFINAE |
替换失败时移除候选 |
Concepts |
声明约束、移除候选并参与重载排序 |
static_assert |
候选已选中后给出硬错误 |
tag dispatch |
根据编译期类别选择不同实现 |
if constexpr |
在一个模板内部选择合法分支 |
|
1 2 3 |
template<class T> requires Serializable<T> void send(const T&); |
Concept决定send()是否参与重载
|
1 2 3 4 5 6 7 |
// 进入函数后选择具体实现 if constexpr (HasFastEncoding<T>) { // 快速编码 } else { // 通用编码 } |
|
1 2 3 |
// 或者 encode(value, fast_encoding_tag{}); |
不要过度约束
- 假设函数只需要
|
1 2 |
buffer.data(); buffer.size(); |
|
1 2 3 4 5 6 7 |
// 不要随便要求 // 除非函数真的需要所有这些性质 std::ranges::contiguous_range<Buffer> && std::ranges::sized_range<Buffer> && std::ranges::common_range<Buffer> && std::copyable<Buffer> |
项目中的阅读方式
- 看到
|
1 |
template<SomeConcept T> |
|
1 2 3 4 5 6 7 8 |
// 先找到 Concept 定义,不要直接进入函数体 // 将 Concept 拆成 基础 Concept + 表达式要求 + 类型关系 + noexcept 要求 + 返回类型要求 |
- 检查约束顺序和是否存在更具体重载
|
1 |
requires ConceptA<T> && ConceptB<T> |
- 区分外层约束子句和内层要求表达式
|
1 |
requires requires(...) |
- 说明库关心的是底层对象类型
|
1 |
Concept<std::remove_cvref_t<T>> |
- 明库很可能准备按值保存或转发后保存对象
|
1 |
Concept<std::decay_t<T>> |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ ASIO:学习一08/01
- ♥ C++并发编程 _管理线程05/07
- ♥ C++标准库_chrono03/28
- ♥ Photoshop CEP扩展和插件开发04/27
- ♥ Boost 程序库完全开发指南:容器算法数学文件08/24
- ♥ C++_volatile10/08