SFINAE、std::enable_if 与C++20 Concepts
SFINAE 是什么
- 模板参数替换失败不算错误
Substitution Failure Is Not An Error
|
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 <iostream> #include <vector> template<class T> auto length(const T& value) -> decltype(value.size()) { return value.size(); } // 最低优先级的后备版本 std::size_t length(...) { return 0; } int main() { std::vector<int> values{1, 2, 3}; std::cout << length(values) << '\n'; std::cout << length(42) << '\n'; } |
|
1 2 3 4 5 6 7 8 |
3 0 // 当调用 length(values); // 编译器尝试生成 auto length(const std::vector<int>& value) -> decltype(value.size()); // value.size() 合法,模板保留 |
|
1 2 3 4 5 6 |
// 当调用 length(42); // 编译器尝试生成 auto length(const int& value) -> decltype(value.size()); // int 没有 size(),替换失败 // 根据 SFINAE,这个模板被移出候选集合,于是选择 length(...) |
- 这里不是说整个程序永远不会报错,而是
- 这个候选模板的替换失败本身不立即报错,编译器可以继续寻找其他候选
- 如果所有候选都被排除,最终仍然会出现“没有匹配函数”的编译错误
编译器到底做了什么
- 可以把一次模板函数调用理解为
- 最关键的分界线是:
- 模板声明中的替换失败:可能触发
SFINAE - 模板被选中后,函数体中的错误:通常是硬错误
- 模板声明中的替换失败:可能触发
SFINAE 只保护“直接上下文”
- 错误出现在声明中
value.size()位于返回类型中- 对于没有
size()的类型,模板可以被SFINAE掉
|
1 2 3 4 5 |
template<class T> auto good(T value) -> decltype(value.size()) { return value.size(); } |
- 错误只出现在函数体中
|
1 2 3 4 5 6 7 8 9 10 |
template<class T> std::size_t bad(T value) { return value.size(); } std::size_t bad(...) { return 0; } |
- 模板
bad<int>的参数匹配优于省略号版本,所以模板先被选中
|
1 2 3 4 |
bad(42); // 随后实例化函数体,才发现 value.size() // 不合法,于是产生硬错误。此时再退回省略号版本已经来不及了 |
-
因此,阅读模板代码时必须分清
- 限制条件出现在函数声明里,还是隐藏在函数体里?
-
SFINAE的直接上下文通常包括- 函数参数类型
- 函数返回类型
- 模板参数声明
- 这些位置中的
decltype、默认模板实参等表达式
-
通常不包括
- 函数体
- 为了替换而连带实例化的其他模板内部
- 某些默认成员函数或辅助类模板中的后续错误
std::enable_if原理
std::enable_if从C++11开始提供- 可以把它近似理解为
|
1 2 3 4 5 6 7 8 9 10 11 |
template<bool Condition, class T = void> struct enable_if { // Condition == false 时没有 type }; template<class T> struct enable_if<true, T> { using type = T; }; |
- 于是
- 如果这种失败发生在模板替换的直接上下文中,模板就会被
SFINAE掉
- 如果这种失败发生在模板替换的直接上下文中,模板就会被
|
1 2 3 4 5 6 |
std::enable_if<true, int>::type // 提到int // 但 std::enable_if<false, int>::type // 不存在, |
C++14提供了简写
|
1 2 3 4 5 |
std::enable_if_t<Condition, T> // == typename std::enable_if<Condition, T>::type |
如何使用 enable_if
- 放在返回类型中
- 缺点是:
- 构造函数和析构函数没有返回类型,不能这样使用
- 返回类型中的约束不够醒目
- 复杂条件会让函数声明很难读
|
1 2 3 4 5 6 7 8 9 10 |
// 这是 C++17 写法。它只接受整数类型 #include <type_traits> template<class T> std::enable_if_t<std::is_integral_v<T>, T> twice(T value) { return value * 2; } |
- 放在模板参数中
- 这是旧项目最常见的形式
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
template< class T, std::enable_if_t<std::is_integral_v<T>, int> = 0 > void describe(T value) { std::cout << "integral: " << value << '\n'; } template< class T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0 > void describe(T value) { std::cout << "floating: " << value << '\n'; } |
|
1 2 3 4 5 6 |
// 看到它时,可以直接翻译成 只有 T 是整数时,第一个 describe 才存在。 只有 T 是浮点数时,第二个 describe 才存在。 // 第二个模板参数的实际数值 0 并不重要,它只是为了让模板参数有默认值 |
- 放在普通函数参数中
- 这种形式在老代码里能看到,但通常不推荐,因为它污染了函数形参列表
|
1 2 3 4 5 6 |
template<class T> void process( T value, std::enable_if_t<std::is_integral_v<T>, int>* = nullptr) { } |
enable_if 经典陷阱
- 下面的写法不能用于定义两个重载
|
1 2 3 4 5 6 7 8 9 10 11 |
template< class T, class = std::enable_if_t<std::is_integral_v<T>> > void print(T); template< class T, class = std::enable_if_t<std::is_floating_point_v<T>> > void print(T); // 重定义 |
- 原因是
- 默认模板实参不属于函数模板签名的一部分
|
1 2 3 4 |
// 编译器看到的两个模板本质上都是 template<class T, class> void print(T); |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 可以改成非类型模板参数 // 不过在 C++20 项目中,应该优先使用 Concepts template< class T, std::enable_if_t<std::is_integral_v<T>, int> = 0 > void print(T); template< class T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0 > void print(T); |
修复完美转发构造函数
- 万能引用构造函数可能抢走复制构造函数
- 详细见上一编
|
1 2 3 4 5 |
class Person { public: template<class S> explicit Person(S&& name); }; |
C++17可以这样限制- 两个约束分别表示:
- 一:参数本身不能是
Person,否则可能抢占复制或移动构造 - 二:
std::string必须真的能够由表达式类型S&&构造
|
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 |
#include <string> #include <type_traits> #include <utility> class Person { private: std::string name_; public: template< class S, std::enable_if_t< !std::is_same_v<std::decay_t<S>, Person> && std::is_constructible_v<std::string, S&&>, int > = 0 > explicit Person(S&& name) : name_(std::forward<S>(name)) { } Person(const Person&) = default; Person(Person&&) = default; }; |
|
1 2 3 4 5 6 |
// 它检查的是隐式转换 std::is_convertible_v<S, std::string> // std::string 必须真的能够由表达式类型 S&& 构造 // 它还会考虑显式构造,并且参数顺序是“目标类型在前” std::is_constructible_v<std::string, S&&> |
std::void_t
C++17提供了- 检测某个表达式是否存在
- 看起来什么都没做,但它可以把任意待检测表达式带入
SFINAE上下文
|
1 2 |
template<class...> using void_t = void; |
- 检测类型是否有
size()
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <type_traits> #include <utility> template<class T, class = void> struct has_size : std::false_type { }; template<class T> struct has_size< T, std::void_t< decltype(std::declval<const T&>().size()) > > : std::true_type { }; // 这是 `C++17` 大型泛型库中常见的 `detection idiom` |
|
1 2 3 |
static_assert(has_size<std::string>::value); static_assert(has_size<std::vector<int>>::value); static_assert(!has_size<int>::value); |
- 理解过程
- 主模板默认继承
false_type - 如果
T的size()表达式合法,偏特化中的void_t<...>得到void - 偏特化匹配成功,继承
true_type - 如果表达式非法,偏特化被
SFINAE掉,回到主模板
- 主模板默认继承
- 不过它只证明表达式在语法和类型系统上合法,不证明
size()是常数时间- 结果在运行期间正确
- 对象线程安全
- 返回值符合业务语义
C++20 Concepts
C++20把约束正式加入语言,不再需要通过“让某个type消失”来间接控制模板
|
1 2 3 4 5 6 7 |
#include <concepts> #include <cstddef> template<class T> concept HasSize = requires(const T& value) { { value.size() } -> std::convertible_to<std::size_t>; }; |
- 它表示
- 对于
const T& value - 表达式
value.size()必须合法 - 结果必须可以转换成
std::size_t
- 对于
- 使用方式有四种:
- 一:
requires子句
|
1 2 3 4 5 6 |
template<class T> requires HasSize<T> std::size_t length(const T& value) { return value.size(); } |
- 二:约束模板参数
|
1 2 3 4 5 |
template<HasSize T> std::size_t length(const T& value) { return value.size(); } |
- 三:缩写函数模板
|
1 2 3 4 |
std::size_t length(const HasSize auto& value) { return value.size(); } |
- 四:尾部
requires
|
1 2 3 4 5 6 |
template<class T> std::size_t length(const T& value) requires HasSize<T> { return value.size(); } |
两个连续的 requires 是什么
- 大型项目里经常会看到
|
1 2 3 4 5 |
template<class T> requires requires(T value) { value.size(); } void process(const T&); |
- 两个
requires作用不同:- 第一个是
requires-clause:给模板添加约束 - 第二个是
requires-expression:产生一个编译期bool,检查表达式是否合法
- 第一个是
|
1 2 3 4 5 6 7 8 9 |
// 一般更建议给它命名 template<class T> concept Sized = requires(T value) { value.size(); }; template<Sized T> void process(const T&); |
Concepts 不等于 SFINAE
- 从使用效果看,两者都会让不满足条件的模板退出候选集合
- 但语言机制不同:
| 方面 | SFINAE / enable_if |
C++20 Concepts |
| 引入版本 | SFINAE 早已有之;enable_if C++11 |
C++20 |
| 表达方式 | 制造无效类型或表达式 | 直接声明约束 |
| 错误信息 | 往往很长 | 通常更明确 |
| 重载排序 | 依赖传统模板偏序技巧 | 约束参与重载排序 |
| 可读性 | 条件复杂后较差 | 更接近接口声明 |
| 老项目兼容 | C++11/14/17 可用 |
需要 C++20 |
- 严格来说
Concepts的约束不满足不是“SFINAE失败”,而是候选函数不满足关联约束- 只是从调用者视角看,它们效果相似
if constexpr不能替代约束
C++17- 这个模板对任何
T都是候选函数。只是在模板被选中后,根据类型选择函数体中的分支
- 这个模板对任何
|
1 2 3 4 5 6 7 8 9 10 11 12 |
template<class T> void serialize(const T& value) { if constexpr (std::is_integral_v<T>) { // 整数序列化 } else { static_assert( std::is_integral_v<T>, "unsupported serialization type" ); } } |
- 而下面的
concepts- 会在重载解析阶段排除非整数类型
|
1 2 3 |
template<class T> requires std::integral<T> void serialize(const T& value); |
- 因此
Concepts/SFINAE:控制接口是否存在if constexpr:控制已经选中的模板怎样实现
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// 工程代码经常同时使用 template<class T> requires Serializable<T> void serialize(const T& value) { if constexpr (std::integral<T>) { // 整数实现 } else if constexpr (std::floating_point<T>) { // 浮点实现 } } |
网络代码中的 Concept
Concept明确了模板所依赖的静态接口Concept是编译期类型约束,不是完整的业务契约
- 但它无法保证
data()返回的内存在发送期间一直有效size()没有超过实际缓冲区size()没有超过实际缓冲区command()已转换成网络字节序
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <concepts> #include <cstddef> #include <cstdint> template<class T> concept PacketLike = requires(const T& packet) { { packet.command() } -> std::same_as<std::uint16_t>; { packet.data() } -> std::convertible_to<const std::byte*>; { packet.size() } -> std::convertible_to<std::size_t>; }; template<PacketLike Packet> void send_packet(const Packet& packet) { // send_all(packet.data(), packet.size()); } |
函数模板重载解析、模板偏序与约束排序
重载解析的完整流程
- 调用
|
1 |
process(argument); |
- 编译器大致依次完成:
- 名字查找,收集名为
process的候选 - 对函数模板进行模板参数推导
- 替换模板参数,
SFINAE掉无效模板 - 检查
C++20约束是否满足 - 判断参数数量、参数转换等是否可行
- 比较隐式转换序列
- 如果转换一样好:
普通函数通常优先于函数模板;
函数模板之间进行模板偏序;
C++20还会比较约束谁更强 - 最佳候选确定后,实例化函数体
- 名字查找,收集名为
普通函数不一定优先于模板
|
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 |
#include <iostream> void process(int) { std::cout << "non-template int\n"; } template<class T> void process(T) { std::cout << "generic template\n"; } template<class T> void process(T*) { std::cout << "pointer template\n"; } int main() { int value = 10; process(10); process(3.14); process(&value); process<>(10); } |
|
1 2 3 4 |
non-template int generic template pointer template generic template |
process(10)- 转换质量相同,因此选择普通函数
|
1 2 |
void process(int); // 精确匹配 template<class T> process(T); // T = int,也是精确匹配 |
process(3.14)- 模板转换更好,所以选择模板
|
1 2 |
void process(int); // double -> int process<double>(double); // 精确匹配 |
- 因此规则不是“普通函数永远优先”,而是
- 转换质量相同时,普通函数优先
process(&value)- 这时模板偏序认为
T*版本更加专用,所以选择指针版本
- 这时模板偏序认为
|
1 2 |
process<int*>(int*); process<int>(int*); |
process<>(10)- 空模板实参列表表示
- 只考虑函数模板,因此普通函数
process(int)被排除
|
1 |
process<>(10); |
隐式转换的基本等级
- 简化后的排序是
| 等级 | 示例 |
| 精确匹配 | int -> int、T& -> T& |
| 提升 | char -> int、float -> double |
| 标准转换 | double -> int、派生类指针转基类指针 |
| 用户定义转换 | 转换构造函数、转换运算符 |
| 省略号 | f(...) |
- 前面的匹配通常优于后面的匹配
- 不过“精确匹配”内部还有:
cv限定转换- 引用绑定
- 继承关系
- 初始化列表
- 模板偏序
模板参数推导时通常不做类型协调
|
1 2 3 4 5 6 7 8 9 |
template<class T> void combine(T first, T second) { } int main() { combine(1, 2.0); // 错误 } |
|
1 2 |
第一个参数推导 T = int 第二个参数推导 T = double |
- 编译器不会主动寻找共同类型,也不会在推导期间先把
int转成double - 可以显式指定模板参数
|
1 2 3 4 |
combine<double>(1, 2.0); // 此时 T 已经固定为 double,之后普通重载规则允许 1 -> double |
- 或者将模板设计为两个参数
|
1 2 3 4 |
template<class T1, class T2> void combine(T1 first, T2 second) { } |
- 因此要区分
- 模板参数推导阶段:通常不依靠隐式转换解决推导冲突
- 推导完成以后:函数调用参数可以进行正常的隐式转换
什么是函数模板偏序
- 考虑
|
1 2 3 4 5 |
template<class T> void inspect(T); // 版本 1 template<class T> void inspect(T*); // 版本 2 |
- 对于
|
1 2 |
int* pointer = nullptr; inspect(pointer); |
- 两个模板都可以生成
- 转换质量完全相同,于是进行函数模板偏序
|
1 |
void inspect(int*); |
-
可以从“可接受类型的集合”理解:
inspect(T)能接受几乎所有可复制的类型inspect(T*)只能接受指针类型- 指针类型是所有类型中的一个子集
- 所以
T*版本更加专用
-
更接近编译器的思考方法是:
- 用虚构类型
X替换T*,得到X* - 普通版本的
T可以匹配X* - 用虚构类型
X替换普通版本 T*不能匹配任意的X- 因而
T*版本更加专用
- 用虚构类型
|
1 |
??? |
- 注意:函数模板不能偏特化
|
1 2 3 4 5 6 |
template<class T> void inspect(T); // 错误:函数模板不支持偏特化 template<class T> void inspect<T*>(T*); |
|
1 2 3 4 |
// 正确方式是重载 template<class T> void inspect(T*); |
参数包也参与模板偏序
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <iostream> template<class T> void print(T value) { std::cout << "one argument\n"; } template<class T, class... Rest> void print(T first, Rest... rest) { std::cout << "multiple arguments\n"; } |
- 调用
- 两个模板都能匹配,但没有参数包的版本更加专用
|
1 |
print(42); |
|
1 2 3 |
// 只有参数包版本可行 print(1, 2, 3); |
万能引用会干扰重载
- 考虑
|
1 2 3 4 5 6 7 8 9 10 11 |
template<class T> void handle(const T&) { std::cout << "const reference\n"; } template<class T> void handle(T&&) { std::cout << "forwarding reference\n"; } |
- 非
const左值
|
1 2 |
int value = 10; handle(value); |
|
1 2 3 4 |
// 得到精确的 int&,通常选择转发引用版本 T = int& T&& = int& && = int& |
const左值const T&版本更合适
|
1 2 |
const int value = 10; handle(value); |
- 右值
- 选择
T&&版本
- 选择
|
1 |
handle(10); |
- 这也是为什么万能引用可能抢占拷贝构造函数
|
1 2 |
template<class S> Person(S&&); |
|
1 2 3 |
// 对于非 const Person 左值,它可以生成 Persion(Person&); |
|
1 2 3 4 5 |
// 而普通复制构造通常是 Person(const Person&); // 前者不需要添加 const,转换更好 |
- 不要同时随意提供
T和T&&
|
1 2 3 4 5 6 7 |
template<class T> void execute(T); template<class T> void execute(T&&); execute(42); // 通常二义性 |
C++20 Concepts如何参与排序
- 首先定义两个具有明确层次的
Concept
|
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 |
#include <concepts> #include <iostream> #include <type_traits> template<class T> concept Integer = std::integral<T>; template<class T> concept SignedInteger = Integer<T> && std::is_signed_v<T>; template<Integer T> void classify(T) { std::cout << "integer\n"; } template<SignedInteger T> void classify(T) { std::cout << "signed integer\n"; } int main() { classify(42); classify(42u); } |
|
1 2 |
signed integer integer |
- 对于
intInteger<int>成立SignedInteger<int>也成立SignedInteger明确包含Integer的约束- 因此第二个模板更加受限
- 对于
unsigned int,只有Integer成立 - 这叫做约束的
subsumption,可以理解为:- 如果满足约束
B必然也满足约束A,并且编译器能从约束结构中识别这种包含关系,那么B比A更受限
- 如果满足约束
编译器不会证明任意逻辑关系
- 下面的代码看起来第二个条件明显更严格
|
1 2 3 4 5 6 7 8 9 10 |
template<class T> requires std::is_integral_v<T> void encode(T); template<class T> requires ( std::is_integral_v<T> && sizeof(T) >= 4 ) void encode(T); |
- 但对于
int,这可能产生二义性- 原因是两处
std::is_integral_v<T>来自不同的源代码位置,约束规范化后可能被视为不同的原子约束
- 原因是两处
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 应该复用命名 Concept // 现在 `WideIntegral` 明确建立在 `Integral` 上,编译器可以正确排序 template<class T> concept Integral = std::is_integral_v<T>; template<class T> concept WideIntegral = Integral<T> && (sizeof(T) >= 4); template<Integral T> void encode(T); template<WideIntegral T> void encode(T); |
多个互不相关的Concept可能二义
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
template<class T> concept HasData = requires(const T& value) { value.data(); }; template<class T> concept HasSerialize = requires(const T& value) { value.serialize(); }; template<HasData T> void send(const T&); template<HasSerialize T> void send(const T&); |
- 如果某个类型同时拥有
data()和serialize():- 两个候选都满足,而且约束之间没有包含关系,所以调用二义
|
1 |
send(object); |
Concepts不会自行判断- 直接发送应该比序列化发送优先
|
1 2 3 4 5 6 |
template<HasData T> void send(const T&); template<class T> requires HasSerialize<T> && (!HasData<T>) void send(const T&); |
网络代码中的重载设计
|
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 31 32 33 34 35 36 37 38 39 |
#include <concepts> #include <cstddef> #include <vector> class Socket; template<class T> concept DirectByteBuffer = requires(const T& buffer) { { buffer.data() } -> std::convertible_to<const std::byte*>; { buffer.size() } -> std::convertible_to<std::size_t>; }; template<class T> concept SerializablePacket = requires(const T& packet, std::vector<std::byte>& output) { { packet.serialize(output) } -> std::same_as<void>; }; template<DirectByteBuffer Buffer> void send(Socket& socket, const Buffer& buffer) { // 直接发送 buffer.data() 和 buffer.size() } template<class Packet> requires SerializablePacket<Packet> && (!DirectByteBuffer<Packet>) void send(Socket& socket, const Packet& packet) { std::vector<std::byte> output; packet.serialize(output); // 发送 output } |
名字查找也会影响候选集合
- 经典泛型代码
|
1 2 3 4 5 6 7 8 |
#include <utility> template<class T> void exchange(T& first, T& second) { using std::swap; swap(first, second); } |
- 这样候选集合可能同时包含:
std::swap- 通过
ADL找到的、定义在T所属命名空间里的专用swap
|
1 2 3 4 5 |
// 这里不用 std::swap(first, second); // 而是先引入标准版本,再无限定调用 swap(first, second); |
|
1 2 3 4 5 6 7 |
namespace network { class Connection { friend void swap(Connection&, Connection&) noexcept; }; } |
= delete 不会移除候选
|
1 2 3 4 |
void convert(int) = delete; void convert(long); convert(10); |
- 编译器会选择精确匹配的
|
1 |
convert(int) |
- 然后报错,因为它已被删除
- 它不会退而选择
convert(long)
- 它不会退而选择
| 技术 | 是否参与重载解析 |
SFINAE 失败 |
不参与 |
Concepts 约束不满足 |
不参与 |
= delete |
参与,选中后报错 |
| 私有函数 | 参与,选中后做访问检查 |
| 函数体编译失败 | 已经选中,实例化时报错 |
函数返回类型通常不参与重载选择
- 不能仅通过返回类型重载
|
1 2 |
int create(); double create(); // 错误 |
- 调用位置也不会帮助选择
- 编译器不会因为左边是
double就选择返回double的版本
- 编译器不会因为左边是
|
1 |
double result = create(); |
- 模板中通常通过显式模板参数表达目标类型
|
1 2 3 4 |
template<class T> T create(); auto value = create<double>(); |
|
1 2 3 |
// 或者使用 tag/type wrapper: create(std::type_identity<double>{}); |
其他
std::declval
- 可以理解成
- 在不真正创建对象的情况下,假想自己拥有一个指定类型的表达式,用于编译期类型推导和合法性检查
- 为什么需要
std::declval
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 假设想推导下面表达式的返回类型 obj.get_value() // 最直接可能想到: decltype(T{}.get_value()) // 但这要求 T 能够默认构造。如果 T 没有默认构造函数,就会失败 class Socket { public: explicit Socket(int fd); int native_handle() const; }; // 比如下面无法编译 using Result = decltype(Socket{}.native_handle()); // 因为 Socket{} 不合法 |
|
1 2 3 4 5 6 |
// 使用 std::declval using Result = decltype(std::declval<const Socket&>().native_handle()); // 编译器只分析表达式类型,并不会真正创建 Socket 对象 // Result 是 int |
- 需要考虑引用折叠
| 写法 | 表达式类型 |
std::declval<T>() |
T&& |
std::declval<T&>() |
T& |
std::declval<const T&>() |
const T& |
std::declval<T&&>() |
T&& |
- 只能用于“不求值语境”
std::declval只能出现在不会真正执行表达式的语境中,最常见的是decltype
|
1 |
using Type = decltype(std::declval<T>().func()); |
|
1 2 3 4 |
// 不可以 T&& obj = std::declval<T>(); // 错误:试图真正调用 declval obj.func(); |
|
1 2 3 4 5 |
// 其他典型的不求值语境还包括 sizeof(expression) noexcept(expression) requires { expression; } // C++20 |
阅读模板的翻译方法
- 只有
condition == true时,这个候选才存在
|
1 |
std::enable_if_t<condition, int> = 0 |
- 检测
expression对当前类型是否合法
|
1 |
std::void_t<decltype(expression)> |
- 这个函数的公开静态接口要求
T满足SomeConcept
|
1 |
requires SomeConcept<T> |
- 然后再检查:
- 条件发生在声明中还是函数体中?
- 失败后是否还有其他重载?
- 多个约束重载中,哪个更加受限?
- 查的只是表达式合法性,还是也约束了返回类型?
- 万能引用参与时,约束检查的是
T、T&&,还是remove_cvref_t<T>?
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ 51CTO:C++编程技巧与规范08/01
- ♥ C++_原子操作内存序07/29
- ♥ WTL 概述03/10
- ♥ krabsetw记述:概述使用05/22
- ♥ 深度探索C++对象模型一02/09
- ♥ 51CTO:C++语言高级课程一08/07
