#这段内容是 Vincent Reverdy 在 CppCon 2018 演讲的一部分,核心主题是:
如何在 C++17 中提高代码的通用性(Genericity),而不牺牲代码的可读性与简洁性?
你希望写出既通用又可维护的模板代码。换句话说,你希望能像这样思考和写代码:
if (some_condition) {
do_this();
}
你可以理解为:
在不引入代码爆炸的情况下,使用 C++17 中的
constexpr
,if constexpr
,decltype
,void_t
等工具,实现“在一堆模板可能性中,选择那个能编译通过的方案”。
这是一次关于“在现代 C++(特别是 C++17)中,如何用更优雅、更可维护的方式实现强大泛型功能”的演讲。
如果你想深入理解这套机制(比如如何自己实现函数列表选择、类型推导、模板条件判断等),我可以给你演示一些小例子,比如:
constexpr if
和 std::enable_if
的替代std::void_t
或 decltype
判断某个表达式是否有效我来帮你翻译并解释清楚它的原理、用途和限制。
SFINAE(Substitution Failure Is Not An Error) 是 C++ 模板系统中的一个规则,用来在模板实例化过程中优雅地处理失败。
当我们在使用模板时,将某些类型代入模板参数中,如果这会导致代码不合法(比如调用了不存在的成员函数),编译器不会报错,而是简单地忽略这一个重载或特化版本,继续尝试其他可能的重载。
替换失败不是错误(SFINAE)是指在 C++ 中,如果模板参数的某个替换导致无效,这本身不被认为是错误。
这个规则应用在函数模板重载解析过程中。
如果某个候选模板,在用实际类型替换模板参数时失败了,该模板会被从重载集合中排除,而不是立即引发编译错误。
这个特性是 模板元编程(template metaprogramming) 的核心技术之一。
这是标准中对 SFINAE 机制的技术性描述:
explicit
声明等来看一个典型的 SFINAE 使用例子:
#include // 引入类型萃取工具,如 std::true_type、std::false_type、std::void_t 等
// 定义一个模板结构体 has_foo,用于检测类型 T 是否有成员函数 foo()
// 这是主模板,假设类型 T 没有 foo(),因此继承自 std::false_type
template <typename, typename = std::void_t<>>
struct has_foo : std::false_type {};
// 偏特化版本:当 T 拥有 foo() 成员函数时,此模板匹配
// decltype(std::declval().foo()):尝试调用 T 类型的 foo() 方法
// 如果该表达式合法(即 T 有 foo()),std::void_t<...> 会生成 void 类型,从而选择此特化版本
template <typename T>
struct has_foo<T, std::void_t<decltype(std::declval<T>().foo())>> : std::true_type {};
// 定义结构体 A,拥有成员函数 foo()
struct A {
void foo();
};
// 定义结构体 B,没有 foo() 函数
struct B {
void bar();
};
int main() {
// 编译时断言 A 拥有 foo() 成员函数,编译通过
static_assert(has_foo<A>::value, "A has foo()");
// 编译时断言 B 不拥有 foo() 成员函数,编译通过
static_assert(!has_foo<B>::value, "B does not have foo()");
}
属于现代 C++ 中最推荐的写法之一,用于检测某个类型是否具有特定成员函数。下面我用中文详细解释每一部分的含义与工作原理,帮助你彻底理解这段代码。
#include
std::void_t
和类型萃取(type traits)所需的工具。// 默认模板:假设没有 foo(),所以继承自 false_type
template <typename, typename = std::void_t<>>
struct has_foo : std::false_type {};
std::void_t<>
,这种写法允许我们在偏特化中用 SFINAE 检测某个表达式是否合法。T
没有成员函数 foo()
,返回 false
。// 如果 T 有成员函数 foo(),则偏特化成功
template <typename T>
struct has_foo<T, std::void_t<decltype(std::declval<T>().foo())>> : std::true_type {};
decltype(std::declval().foo())
是合法表达式时才会匹配。T
有 foo()
成员函数,那么这个表达式是合法的,就会选择这个特化版本,结果为 true_type
。decltype(std::declval().foo())
是什么?std::declval()
返回一个 “假的” T
实例(仅用于推导,不会真的调用)。.foo()
尝试调用 T
的 foo()
方法。decltype(...)
用于检测是否编译通过。T
有 foo()
,否则 SFINAE 回退到主模板。struct A {
void foo();
};
struct B {
void bar(); // 没有 foo()
};
int main() {
static_assert(has_foo<A>::value, "A has foo()"); // 成功编译
static_assert(!has_foo<B>::value, "B does not have foo()"); // 成功编译
}
A
:decltype(std::declval().foo())
是合法的 → 匹配偏特化 → true
B
:没有 foo()
,表达式不合法 → 匹配主模板 → false
你也可以检测特定函数签名,比如:
// 检测是否有 `int foo(int)` 的成员函数
template <typename, typename = std::void_t<>>
struct has_int_foo : std::false_type {};
template <typename T>
struct has_int_foo<T, std::void_t<decltype(std::declval<T>().foo(std::declval<int>()))>>
: std::is_same<decltype(std::declval<T>().foo(std::declval<int>())), int> {};
名称 | 说明 |
---|---|
std::void_t |
用于在模板偏特化中测试表达式是否有效 |
std::declval |
获取类型 T 的右值引用,无需构造 |
decltype(...) |
获取表达式类型并用于类型判断 |
主模板继承自 false_type |
默认没有目标函数 |
偏特化继承自 true_type |
表达式合法就说明存在该函数 |
下面是这段代码的纠正版本,并附上逐行中文注释与理解解释:
#include
#include
// 默认兜底版本:无论传入什么参数,这个版本总是合法(... 参数匹配任何类型)
void test(...) {
std::cout << "Catch all overload called\n";
}
// 重载版本1:当 C 是类的引用,F 是成员函数指针时,此版本启用
template <class C, class F>
auto test(C&& c, F f) -> decltype((void)((c.*f)()), void()) {
std::cout << "Reference overload called\n";
}
// 重载版本2:当 C 是类的指针,F 是成员函数指针时,此版本启用
template <class C, class F>
auto test(C* c, F f) -> decltype((void)((c->*f)()), void()) {
std::cout << "Pointer overload called\n";
}
// 一个简单的类 X,包含成员函数 f
struct X {
void f() {}
};
int main() {
X x;
test(x, &X::f); // 调用成员函数指针,作用于对象 x → 匹配 Reference overload
test(&x, &X::f); // 调用成员函数指针,作用于指针 &x → 匹配 Pointer overload
test(42, 1337); // 不匹配上面的两个模板 → 回退到 ellipsis overload
return 0;
}
Reference overload called
Pointer overload called
Catch all overload called
test(...)
void test(...) { std::cout << "Catch all overload called\n"; }
test(C&& c, F f)
(Reference 重载)template <class C, class F>
auto test(C&& c, F f) -> decltype((void)((c.*f)()), void()) { ... }
decltype(...)
来判断表达式是否合法。c.*f
是成员函数指针调用的语法,只有当 c
是对象或引用时才合法。test(C* c, F f)
(Pointer 重载)template <class C, class F>
auto test(C* c, F f) -> decltype((void)((c->*f)()), void()) { ... }
C*
调用成员函数指针 f
的情况。C
是指针类型时 (c->*f)()
才是合法的语法。decltype(...)
推导时,如果表达式非法,不会报错,只是该重载不参与重载决议。行号 | 代码 | 中文解释 |
---|---|---|
4-5 | void test(...) |
通配版本,匹配任何参数,优先级最低 |
8-9 | template |
如果 c.*f 合法,说明是对象+成员函数指针,匹配该版本 |
12-13 | template |
如果 c->*f 合法,说明是指针+成员函数指针,匹配该版本 |
16 | struct X { void f(); }; |
测试结构体,有一个成员函数 f() |
19-24 | main() |
调用 test() 测试三个不同情况的匹配情况 |
decltype(..., void())
符号 | 含义 |
---|---|
decltype(...) |
编译期获取表达式的类型 |
, (逗号运算符) |
C++ 中的逗号表达式运算符:(a, b) 的结果是 b,但会先计算 a |
void() |
一个合法的返回 void 的值(空函数调用的意思) |
decltype(..., void())
是一个使用 逗号表达式 的 decltype
:decltype(expr1, expr2)
这会返回 expr2
的类型,但会在 编译时先验证 expr1
是否合法。
decltype((void)((c->*f)()), void())
这个表达式的行为就是:
如果
(c->*f)()
是一个合法的成员函数调用,
然后忽略它的返回值(用
(void)
转换),
最终整个
decltype(...)
返回的是void
类型。
(c->*f)()
不合法会怎样?(c->*f)()
;c
不是指针,或 f
不是成员函数指针),template <typename T>
auto check(T t) -> decltype(t.do_something(), void()) {
// 只有当 T 有 do_something() 成员函数时,这个函数才会参与重载
std::cout << "T has do_something()\n";
}
t.do_something()
是测试目标void()
是一个合法的返回值(我们只想关心前面的表达式能不能编译)decltype(..., void())
的意思是:
t.do_something()
合法,那整个类型是 void
decltype(expr1, void())
的作用是:
用逗号表达式忽略 expr1 的具体类型,只要它能编译,就让
decltype(...)
返回void
,否则 SFINAE。
std::enable_if
与类型萃取(std::is_integral
/ std::is_floating_point
)结合使用的 C++11 示例,展示了如何在函数模板和类模板特化中根据类型做分支(即编译时选择行为)。我将为你做三件事:
#include
#include // 包含 std::enable_if、std::is_integral 等类型工具
// 针对整数类型的函数模板
// std::enable_if_t<条件> 只有在 is_integral_v 为 true 时才有效
template <class T, class = std::enable_if_t<std::is_integral_v<T>>>
void function(T x) {
std::cout << "integer" << std::endl;
}
// 针对浮点类型的函数模板
// 注意函数签名不同:参数是 const T&,以避免与上面的模板冲突
template <class T, class = std::enable_if_t<std::is_floating_point_v<T>>>
void function(const T& x) {
std::cout << "floating point" << std::endl;
}
// 默认结构体模板
// 当没有任何启用条件时,选择这个版本
template <class T, class = void>
struct structure {
void operator()() const {
std::cout << "structure" << std::endl;
}
};
// 针对整数类型的特化结构体模板
template <class T>
struct structure<T, std::enable_if_t<std::is_integral_v<T>>> {
void operator()() const {
std::cout << "structure" << std::endl;
}
};
int main(int argc, char* argv[]) {
function(5); // 整数:输出 "integer"
function(0.5); // 浮点数:输出 "floating point"
structure<double>{}(); // 不满足整数要求 → 使用默认结构体模板
structure<int>{}(); // 满足整数要求 → 使用整数特化结构体
return 0;
}
integer
floating point
structure
structure
std::enable_if_t<...>
是什么?true
,返回 typename = void
,可以参与模板匹配;false
,替代失败(SFINAE),该模板将被忽略。function(5); // T = int → is_integral_v = true → 匹配整数版本
function(0.5); // T = double → is_floating_point_v = true → 匹配浮点版本
注意这两个模板参数必须略有不同(如参数引用 vs 值),否则会因“模板重定义”报错。
structure<double>{}(); // double 不是 integral → 匹配默认模板
structure<int>{}(); // int 是 integral → 匹配偏特化模板
这种方式常用于为不同类型提供编译期行为定制。
本例展示了如何使用
std::enable_if
结合类型 traits,根据类型在编译期选择不同的函数或结构体行为,是 C++ 模板元编程中的重要工具。
std::void_t
来实现一个类型特征 is_iterable
,判断一个类型是否可用 std::begin()
和 std::end()
迭代,即是否可迭代类型。我帮你加上详细注释并解释每部分含义。当然,这里是你代码的详细注释版,帮助理解每一部分:
#include
#include // 测试用容器 std::vector
#include // 测试用容器 std::map
// 自定义一个空类A,没有迭代相关成员函数
class A {};
// 基础模板:默认情况下,类型不可迭代,继承自 std::false_type
template <typename T, typename = void>
struct is_iterable : std::false_type {};
// 偏特化版本:当 T 满足下面条件时启用
// 使用 std::void_t 检查 T 是否有合法的 std::begin 和 std::end
// 注意这里使用 std::declval&>() 来确保传递的是左值引用
template <typename T>
struct is_iterable<
T,
std::void_t<decltype(std::begin(
std::declval<std::remove_reference_t<T>&>())), // 检测 std::begin(T) 是否有效
decltype(std::end(std::declval<std::remove_reference_t<T>&>())) // 检测 std::end(T)
// 是否有效
>> : std::true_type {}; // 如果以上均合法,继承 std::true_type
int main() {
std::cout << std::boolalpha; // 输出 bool 类型时显示 true/false 而不是 1/0
// 测试 std::vector,它支持 begin/end,输出 true
std::cout << is_iterable<std::vector<double>>::value << std::endl;
// 测试 std::map,它支持 begin/end,输出 true
std::cout << is_iterable<std::map<int, double>>::value << std::endl;
// 测试基本类型 double,不支持 begin/end,输出 false
std::cout << is_iterable<double>::value << std::endl;
// 测试自定义空类 A,没有 begin/end,输出 false
std::cout << is_iterable<A>::value << std::endl;
// 测试 C 风格数组,支持 std::begin 和 std::end,输出 true
int array[] = {1, 2, 3, 3};
std::cout << is_iterable<decltype(array)>::value << std::endl;
}
true
true
false
false
true
std::void_t<...>
:接收一组类型表达式,只要里面所有表达式都有效,就等价于 void
。用于 SFINAE 机制。std::declval&>()
:
int[4]
)传递时的匹配问题。std::begin
和 std::end
的参数是左值引用,因为 std::begin
对数组的重载接受左值引用。is_iterable
结构体:
false_type
表示不可迭代。true_type
表示可迭代。for
循环。std::enable_if
和 std::void_t
在 C++ 元编程中的优缺点,意思是:std::enable_if
和 std::void_t
是非常强大的元编程工具SFINAE 等模板技巧功能强大但用起来繁琐,代码不易读写,未来更简洁、更直接的约束表达方式(如 concepts)会更好,但 SFINAE 依然是重要工具。
如果想,我可以帮你举例对比一下 SFINAE 和 concepts,或者帮你写更简洁的模板约束示例!
#include
// trait 模板,用来获取类型 T 中的嵌套类型 T::type
template<class T>
struct trait {
using type = typename T::type; // 假设 T 有个嵌套成员 type
};
// immediate_context 函数重载组
// 1. 终极 catch-all 版本,参数为任意类型的省略号
void immediate_context(...) {}
// 2. 依赖于模板参数 T 中是否有嵌套类型 type 的版本
// 通过第二个模板参数的默认模板实参:typename T::type
template<class T, class = typename T::type>
void immediate_context(T = 0) {} // 只有当 T::type 存在时,这个版本才有效
// nonimmediate_context 函数重载组
// 1. 终极 catch-all 版本
void nonimmediate_context(...) {}
// 2. 依赖于 trait::type 的版本
// 注意这里的 SFINAE 发生在 trait::type,非直接依赖于 T
template<class T, class = typename trait<T>::type>
void nonimmediate_context(T = 0) {}
// structure 模板,定义了一个嵌套类型 type 为 void
template<class T>
struct structure {
using type = void;
}
int main(int argc, char* argv[]) {
immediate_context(0); // 调用 immediate_context,参数是 int 类型
immediate_context(structure<int>{}); // 调用 immediate_context,参数是 structure 类型
nonimmediate_context(0); // ERROR!int 没有 trait::type 成员
nonimmediate_context(structure<int>{}); // 正常,structure 有 trait>::type
}
immediate_context
typename T::type
,这是对模板参数 T
的直接依赖(immediate context),T
本身有嵌套类型 type
时,模板才能实例化成功,否则会 SFINAE(失败但不报错)。int
是基本类型,没有成员 type
,所以尝试实例化 immediate_context
的模板失败,退化调用到 immediate_context(...)
。immediate_context(0)
能“工作”,说明调用的是 catch-all 版本,不是模板版本。nonimmediate_context
typename trait::type
,这里依赖 trait
,也就是依赖 T
的包装类型,属于 非立即上下文。trait
试图访问 T::type
,如果 T
没有嵌套 type
,那么 trait::type
会报错(不是 SFINAE),因为 trait
不存在 type
,而模板参数默认值依赖于该 trait::type
,导致编译错误。nonimmediate_context(0)
直接导致错误,而不是降级到 catch-all。typename T::type
是直接上下文。typename trait::type
是非直接上下文(因为访问是通过 trait
,而不是直接在模板参数上)。调用 | 是否有效 | 原因 |
---|---|---|
immediate_context(0) |
有效,调用 catch-all | int 没有 type ,模板失败,使用非模板 catch-all 重载 |
immediate_context(structure |
有效,调用模板版本 | structure 有 type ,模板成功实例化 |
nonimmediate_context(0) |
编译错误 | trait 无 type ,非立即上下文错误导致编译失败 |
nonimmediate_context(structure |
有效,调用模板版本 | trait 有 type ,模板成功实例化 |
这段代码演示了:
element_count(T&&)
,根据类型 T
自动选择最合适的实现按照优先级:
T
有 .size()
成员函数 → 调用它;T
可以用 std::begin()
和 std::end()
→ 用 std::distance(begin, end)
计算数量;rank
来引导重载匹配;#include
#include
#include
// 排序标签,用于函数重载优先级
template <int N>
struct rank : rank<N - 1> {};
template <>
struct rank<0> {}; // 最低优先级
// 优先级 2:T 有 .size() 成员函数
template <typename T>
auto element_count(T&& t, rank<2>) -> decltype(t.size(), size_t{}) {
std::cout << "Called .size()" << std::endl;
return t.size();
}
// 优先级 1:T 支持 std::begin()/std::end()
template <typename T>
auto element_count(T&& t, rank<1>) -> decltype(std::begin(t), std::end(t), size_t{}) {
std::cout << "Called std::distance" << std::endl;
return std::distance(std::begin(t), std::end(t));
}
// 优先级 0:默认兜底版本,假设只有一个元素
template <typename T>
size_t element_count(T&&, rank<0>) {
std::cout << "Called default fallback (return 1)" << std::endl;
return 1;
}
// 用户接口:总是从最高优先级开始匹配
template <typename T>
size_t element_count(T&& t) {
return element_count(std::forward<T>(t), rank<2>{});
}
#include
#include
struct MyRange {
int* begin() { return data; }
int* end() { return data + 3; }
int data[3] = {1, 2, 3};
};
int main() {
std::vector<int> v{1, 2, 3};
std::array<int, 4> a{{1, 2, 3, 4}};
MyRange r;
int x = 42;
std::cout << element_count(v) << "\n"; // 调用 std::distance(现在优先)
std::cout << element_count(a) << "\n"; // 调用 std::distance(现在优先)
std::cout << element_count(r) << "\n"; // 调用 std::distance
std::cout << element_count(x) << "\n"; // fallback → 1
}
Called .size()
3
Called .size()
4
Called std::distance
3
Called default fallback (return 1)
1
你写的这段代码非常经典,是 C++ 模板元编程中“排序重载优先级 + SFINAE”技巧的完整范例。
下面是对整段代码的详细解释与理解,逐部分讲解:
element_count
.size()
成员函数 → 调用它;std::begin()
和 std::end()
→ 用 std::distance
;1
,表示一个默认元素。rank
:控制函数模板的匹配优先级template <int N> struct rank : rank<N - 1> {};
template <> struct rank<0> {};
rank<2>
是 rank<1>
的子类,以此类推;rank<2>
> rank<1>
> rank<0>
);element_count
函数模板(按优先级降序).size()
template <typename T>
auto element_count(T&& t, rank<2>) -> decltype(t.size(), size_t{}) {
std::cout << "Called .size()" << std::endl;
return t.size();
}
T
有 t.size()
成员函数,这个重载成立;decltype(t.size(), size_t{})
使用逗号表达式确保结果是 size_t
。std::begin/end
+ std::distance
template <typename T>
auto element_count(T&& t, rank<1>) -> decltype(std::begin(t), std::end(t), size_t{}) {
std::cout << "Called std::distance" << std::endl;
return std::distance(std::begin(t), std::end(t));
}
std::begin(t)
和 std::end(t)
都能调用,这个重载成立;std::distance()
计算元素数量。template <typename T>
size_t element_count(T&&, rank<0>) {
std::cout << "Called default fallback (return 1)" << std::endl;
return 1;
}
1
。template <typename T>
size_t element_count(T&& t) {
return element_count(std::forward<T>(t), rank<2>{});
}
rank
,我们从最高优先级 rank<2>
开始;rank
重载。std::vector<int> v{1, 2, 3}; // 有 .size()
std::array<int, 4> a{{1, 2, 3, 4}}; // 有 .size()
MyRange r; // 自定义类型,有 begin/end 但无 .size()
int x = 42; // 基本类型
Called .size()
3
Called .size()
4
Called std::distance
3
Called default fallback (return 1)
1
对象 | 是否有 .size() |
是否可迭代 | 匹配函数版本 | 输出 |
---|---|---|---|---|
vector |
.size() → rank<2> |
3 | ||
array |
.size() → rank<2> |
4 | ||
MyRange |
std::distance → rank<1> |
3 | ||
int |
fallback → rank<0> | 1 |
技术 | 描述 |
---|---|
rank |
用于模拟函数模板的优先级顺序 |
SFINAE | 通过 decltype(...) 检查类型特征是否合法 |
std::begin/std::end |
标准迭代接口,支持标准容器和数组 |
std::forward |
保持完美转发的值类别(左值/右值) |
fallback 策略 | 保证任何类型都能有返回结果 |
#include
#include
#include
#include
#include
#include
// 内部实现:优先使用 .size() 成员函数(非变参版本,优先级高)
template <class T,
class = decltype(std::declval<T>().size()) // 若 T 有 size() 成员
>
std::size_t container_element_count(T&& value) {
std::cout << "[.size() version] ";
return std::forward<T>(value).size();
}
// 内部实现:支持 std::begin/std::end 的迭代器版本(可变参数模板)
template <class T, class... X,
class = std::enable_if_t<sizeof...(X) == 0>, // 限制 X... 为空
class = decltype(std::begin(std::declval<T>())),
class = decltype(std::end(std::declval<T>()))>
std::size_t container_element_count(T&& value, X...) {
std::cout << "[std::distance version] ";
return std::distance(std::begin(std::forward<T>(value)), std::end(std::forward<T>(value)));
}
// 特化:匹配 initializer_list 情况
template <class T>
std::size_t element_count(const std::initializer_list<T>& value) {
std::cout << "[initializer_list version] ";
return std::distance(std::begin(value), std::end(value));
}
// 主接口:尝试调用 container_element_count(优先使用 .size() 或迭代器)
template <class T, class = decltype(container_element_count(std::declval<T>()))>
std::size_t element_count(T&& value) {
return container_element_count(std::forward<T>(value));
}
// fallback:什么都不支持时返回 1(变参版本,优先级最低)
template <class T, class... X, class = std::enable_if_t<sizeof...(X) == 0> >
std::size_t element_count(T&&, X...) {
std::cout << "[fallback version] ";
return 1;
}
struct MyRange {
int* begin() { return data; }
int* end() { return data + 3; }
int data[3] = {1, 2, 3};
};
int main() {
std::cout << element_count(42) << "\n"; // fallback → 1
std::cout << element_count(std::forward_list<int>{1, 2, 3}) << "\n"; // std::distance → 3
std::cout << element_count(std::vector<int>{1, 2, 3, 4}) << "\n"; // .size() → 4
std::cout << element_count({1, 2, 3}) << "\n"; // initializer_list → 3
std::cout << element_count(MyRange{}) << "\n"; // std::distance → 3
}
std::enable_if
和 decltype
来做条件启用element_count()
函数,根据类型不同行为如下:条件 | 使用方式 |
---|---|
如果有 .size() 成员函数 |
调用 .size() |
否则如果支持 begin() /end() |
使用 std::distance() 计算 |
否则如果是 std::initializer_list |
特化版本 |
否则 | 返回 1 ,假设是一个单元素 |
下面我们逐段标注注释与理解: |
container_element_count
(内部实现).size()
的类型 — 非可变参数版本优先(更高优先级)template <
class T,
class = decltype(std::declval<T>().size()) // 如果 T 有 size() 成员,则此版本可用
>
std::size_t container_element_count(T&& value) {
return std::forward<T>(value).size(); // 调用 .size()
}
std::begin
/ std::end
template <
class T,
class... X,
class = std::enable_if_t<sizeof...(X) == 0>, // 避免歧义,仅允许空变参
class = decltype(std::begin(std::declval<T>())),
class = decltype(std::end(std::declval<T>()))
>
std::size_t container_element_count(T&& value, X...) {
return std::distance(
std::begin(std::forward<T>(value)),
std::end(std::forward<T>(value))
);
}
std::distance()
计算 begin/end 差值;.size()
不存在时才被考虑。element_count
(外部接口)std::initializer_list
的特殊处理template <class T>
std::size_t element_count(const std::initializer_list<T>& value) {
return std::distance(std::begin(value), std::end(value));
}
{1,2,3}
这类临时初始化列表;container_element_count
template <
class T,
class = decltype(container_element_count(std::declval<T>()))
>
std::size_t element_count(T&& value) {
return container_element_count(std::forward<T>(value));
}
.size()
或 std::distance()
;container_element_count()
不适用,就替代失败,转向下面的变参模板。.size()
的类型template <
class T,
class... X,
class = std::enable_if_t<sizeof...(X) == 0> // 确保是 fallback 而不是多余参数匹配
>
std::size_t element_count(T&& value, X...) {
return 1; // 默认返回 1
}
.size()
或迭代器版本的类型都会调用这个;int main(int argc, char* argv[])
{
element_count(42); // fallback → 返回 1
element_count(std::forward_list({1, 2, 3})); // 没有 .size() → 用 std::distance
element_count(std::vector({1, 2, 3, 4})); // 有 .size() → 调用 .size()
element_count({1, 2, 3}); // 调用 initializer_list 特化版本
return 0;
}
情况 | 匹配的版本 | 原因 |
---|---|---|
element_count(42) |
fallback (返回 1) | 无 .size() 、无 begin/end |
element_count(std::vector) |
.size() |
优先匹配非变参 .size() 版本 |
element_count(std::forward_list) |
std::distance |
无 .size() ,但支持迭代器 |
element_count({1,2,3}) |
initializer_list 重载 |
专门匹配初始化列表 |
技巧 | 说明 |
---|---|
SFINAE | 判断类型是否有 .size() 、是否支持 std::begin |
非变参 vs. 变参模板 | 非变参模板优先匹配,可用于排序优先级 |
std::initializer_list 重载 |
用于 {...} 列表的精确匹配 |
std::enable_if_t |
限制变参模板只接受空参数,避免歧义 |
overload_rank
与 SFINAE 技巧 实现函数优先级选择(多重重载决议)的完整例子,并对其优缺点进行了总结。#include
#include
#include
#include
#include
// 定义 overload_rank:用于控制函数重载优先级(从高到低)
template <std::size_t N>
struct overload_rank : overload_rank<N - 1> {};
template <>
struct overload_rank<0> {};
// 优先级 3(最高):std::initializer_list 重载(用于 element_count({1,2,3}))
template <class T>
std::size_t element_count(const std::initializer_list<T>& value) {
std::cout << "[initializer_list version] ";
return std::distance(std::begin(value), std::end(value));
}
// 优先级 2:当类型 T 有 .size() 成员函数
template <class T, class = decltype(std::declval<T>().size())>
std::size_t element_count(overload_rank<2>, T&& x) {
std::cout << "[.size() version] ";
return std::forward<T>(x).size();
}
// 优先级 1:当类型 T 是可迭代的(有 begin/end)
template <class T, class = decltype(std::begin(std::declval<T>())),
class = decltype(std::end(std::declval<T>()))>
std::size_t element_count(overload_rank<1>, T&& x) {
std::cout << "[std::distance version] ";
return std::distance(std::begin(std::forward<T>(x)), std::end(std::forward<T>(x)));
}
// 优先级 0(最低):兜底 fallback,当类型既没有 size() 也不可迭代
template <class T>
std::size_t element_count(overload_rank<0>, T&& x) {
std::cout << "[fallback version] ";
return 1;
}
// 通用入口:从最高优先级开始匹配(不匹配就退化到低优先级)
template <class T>
std::size_t element_count(T&& x) {
return element_count(overload_rank<2>{}, std::forward<T>(x));
}
struct MyRange {
int* begin() { return data; }
int* end() { return data + 3; }
int data[3] = {1, 2, 3};
};
int main() {
std::cout << element_count(42) << "\n"; // fallback
std::cout << element_count(std::forward_list<int>{1, 2, 3}) << "\n"; // std::distance
std::cout << element_count(std::vector<int>{1, 2, 3, 4}) << "\n"; // .size()
std::cout << element_count({1, 2, 3}) << "\n"; // initializer_list
std::cout << element_count(MyRange{}) << "\n"; // std::distance
return 0;
}
[fallback version] 1
[std::distance version] 3
[.size() version] 4
[initializer_list version] 3
[fallback version] 1
下面我将:
overload_rank
技巧和 SFINAE 如何协同工作overload_rank
定义:模板继承链,表示函数匹配优先级template <std::size_t N>
struct overload_rank : overload_rank<N - 1> {};
template <>
struct overload_rank<0> {};
overload_rank<2> → overload_rank<1> → overload_rank<0>
;element_count
// 初始化列表特化:处理 element_count({1,2,3})
template <class T>
std::size_t element_count(const std::initializer_list<T>& value) {
return std::distance(std::begin(value), std::end(value));
}
// 优先级 2:T 有 .size()
template <class T, class = decltype(std::declval<T>().size())>
std::size_t element_count(overload_rank<2>, T&& x) {
return std::forward<T>(x).size();
}
// 优先级 1:T 可迭代(支持 begin/end)
template <
class T,
class = decltype(std::begin(std::declval<T>())),
class = decltype(std::end(std::declval<T>()))
>
std::size_t element_count(overload_rank<1>, T&& x) {
return std::distance(
std::begin(std::forward<T>(x)),
std::end(std::forward<T>(x))
);
}
// 优先级 0:兜底版本,返回 1
template <class T>
std::size_t element_count(overload_rank<0>, T&& x) {
return 1;
}
template <class T>
std::size_t element_count(T&& x) {
return element_count(overload_rank<2>{}, std::forward<T>(x));
}
#include
#include
#include
int main() {
std::cout << element_count(42) << "\n"; // → overload_rank<0> → 1
std::cout << element_count(std::forward_list<int>{1, 2, 3}) << "\n"; // → overload_rank<1> → 3
std::cout << element_count(std::vector<int>{1, 2, 3, 4}) << "\n"; // → overload_rank<2> → 4
std::cout << element_count({1, 2, 3}) << "\n"; // → initializer_list → 3
return 0;
}
技术 | 用途 |
---|---|
overload_rank |
编译期类型排序,用于函数重载优先级控制 |
decltype(std::declval |
用于判断类型是否有 .size() 成员函数 |
SFINAE + 默认模板参数 | 条件启用重载模板(当表达式合法时才匹配) |
变参与非变参版本结合 | 形成 fallback 机制 |
overload_rank
比递归模板更清晰、更具可控性。decltype
、重载优先级;如果你可以用 C++20,那这段逻辑可以用 concepts
重写为:
template <typename T>
concept HasSize = requires(T t) { t.size(); };
template <typename T>
concept Iterable = requires(T t) {
std::begin(t);
std::end(t);
};
template <HasSize T>
size_t element_count(T&& t) {
return t.size();
}
template <Iterable T>
size_t element_count(T&& t) {
return std::distance(std::begin(t), std::end(t));
}
template <typename T>
size_t element_count(const std::initializer_list<T>& t) {
return std::distance(t.begin(), t.end());
}
template <typename T>
size_t element_count(T&&) {
return 1;
}
这段代码:
你最后提到的总结:
- Amazing tools for metaprogramming
- But often too much trickery, hard to read
- Requires lots of boilerplate
- Concepts solve some of this
- SFINAE remains useful
这些评价是非常中肯的。SFINAE 是强大的底层能力,但不够表达式化。Concepts 是未来的方向,但两者仍然可以共存。
指令 | 作用 | 说明 |
---|---|---|
mov |
传送数据 | 将数据从源操作数复制到目的操作数 |
lea |
加载有效地址 | 将内存地址计算结果加载到寄存器 |
push |
压栈 | 把数据压入栈 |
pop |
弹栈 | 把数据从栈中弹出 |
指令 | 作用 | 说明 |
---|---|---|
add |
加法 | 将两个操作数相加 |
sub |
减法 | 两个操作数相减 |
inc |
加一 | 操作数加1 |
dec |
减一 | 操作数减1 |
imul |
有符号乘法 | 有符号整数乘法 |
idiv |
有符号除法 | 有符号整数除法 |
指令 | 作用 | 说明 |
---|---|---|
and |
按位与 | 两个操作数按位与 |
or |
按位或 | 两个操作数按位或 |
xor |
按位异或 | 两个操作数按位异或 |
not |
按位取反 | 对操作数按位取反 |
shl |
左移 | 将操作数左移若干位 |
shr |
右移 | 将操作数右移若干位 |
指令 | 作用 | 说明 |
---|---|---|
jmp |
无条件跳转 | 跳转到指定地址 |
je /jz |
条件跳转(等于/零) | 如果ZF=1,则跳转 |
jne /jnz |
条件跳转(不等于/非零) | 如果ZF=0,则跳转 |
jg /jnle |
条件跳转(大于) | 如果满足有符号大于条件跳转 |
jl /jnge |
条件跳转(小于) | 如果满足有符号小于条件跳转 |
call |
调用子程序 | 调用指定地址的函数 |
ret |
返回 | 从函数返回 |
指令 | 作用 | 说明 |
---|---|---|
cmp |
比较两个操作数 | 通过设置标志位(ZF、CF等)反映结果 |
指令 | 作用 | 说明 |
---|---|---|
nop |
空操作 | 什么都不做,常用于对齐 |
int |
软中断 | 触发中断,用于系统调用 |
cli |
清除中断标志 | 禁止中断 |
sti |
设置中断标志 | 允许中断 |
61E133D8: lea rcx, [rsp + 18h]
; 将 rsp+0x18 的地址加载到 rcx,准备作为栈地址起点(可能用于栈探测)
61E133DD: jb 61E133F8
; 如果之前某个条件失败则跳转(jump if below)→ 跳过循环部分
61E133DF: sub rcx, 1000h
; 每次往下一个页面探测(减去一页,0x1000 = 4KB)
61E133E6: or qword ptr [rcx], 0
; 访问该内存页,触发页表映射(不改变值,只是访问)
61E133EA: sub rax, 1000h
; 记录已访问的总字节数(或用于判断剩余)
61E133F0: cmp rax, 1000h
; 检查是否还有完整页可访问(> 4KB)
61E133F6: ja 61E133DF
; 如果还有更多页,继续循环探测
61E133F8: sub rcx, rax
; 最后剩下不足一整页的部分,继续向下探测 rcx
61E133FB: or qword ptr [rcx], 0
; 访问这段不足一页的栈空间,确保它也被映射
61E133FF: pop rax
; 恢复调用者保存的 rax
61E13400: pop rcx
; 恢复调用者保存的 rcx
61E13401: ret
; 返回到调用函数
if constexpr
语句结合 编译期判断表达式是否有效(即检测某个表达式是否能通过编译),来实现一个通用的 element_count
函数:#include
#include // std::begin / std::end / std::distance
#include // std::void_t / std::declval / type traits
#include
// 默认情况:类型没有 .size()
template <typename T, typename = void>
struct has_size : std::false_type {};
// 特化版本:如果 T::size() 是合法表达式,则为 true
template <typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>> : std::true_type {};
// 默认:不可迭代
template <typename T, typename = void>
struct is_iterable : std::false_type {};
// 特化:如果能调用 std::begin(std::declval()) / std::end(...),则为 true
template <typename T>
struct is_iterable<T, std::void_t<decltype(std::begin(std::declval<T&>())),
decltype(std::end(std::declval<T&>()))>> : std::true_type {};
template <class T>
constexpr std::size_t element_count(T&& x) {
using U = std::remove_reference_t<T>; // 移除引用,防止 auto&/const& 干扰
if constexpr (has_size<U>::value) {
return x.size(); // 如果有 size() 方法
} else if constexpr (is_iterable<U>::value) {
return std::distance(std::begin(x), std::end(x)); // 否则如果可迭代
} else {
return 1; // 否则 fallback:当作单个元素处理
}
}
struct MyRange {
int data[3] = {1, 2, 3};
int* begin() { return data; }
int* end() { return data + 3; }
const int* begin() const { return data; }
const int* end() const { return data + 3; }
};
int main() {
std::vector<int> v{1, 2, 3}; // 有 size() 方法
int arr[] = {4, 5, 6, 7}; // C 风格数组(注意没有显式数组特化)
int x = 42; // 普通值
std::cout << element_count(v) << "\n"; // 输出 3,调用 .size()
std::cout << element_count(arr) << "\n"; // 输出 4,可迭代,调用 std::distance
std::cout << element_count(x) << "\n"; // 输出 1,fallback
std::cout << element_count(42) << "\n"; // 输出 1,fallback
std::cout << element_count(std::vector<int>{1, 2, 3, 4}) << "\n"; // 输出 4,.size()
std::cout << element_count(MyRange{}) << "\n"; // 输出 3,调用 std::distance
return 0;
}
下面是你提供的完整 C++17 代码,附带了详细的中文注释和解读,帮助你彻底理解这段通用 element_count
元编程工具的实现原理与运行机制:
实现一个泛型函数 element_count(x)
,在编译期判断参数 x
的类型特性,并返回其“元素数量”:
.size()
方法(如 std::vector
).size()
,则判断是否可迭代(std::begin
+ std::end
)int
)#include
#include // std::begin / std::end / std::distance
#include // std::void_t / std::declval / type traits
#include
.size()
方法(优先级最高)// 默认情况:类型没有 .size()
template <typename T, typename = void>
struct has_size : std::false_type {};
// 特化版本:如果 T::size() 是合法表达式,则为 true
template <typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>> : std::true_type {};
std::begin()
/ std::end()
(第二优先)// 默认:不可迭代
template <typename T, typename = void>
struct is_iterable : std::false_type {};
// 特化:如果能调用 std::begin(std::declval()) / std::end(...),则为 true
template <typename T>
struct is_iterable<T, std::void_t<
decltype(std::begin(std::declval<T&>())),
decltype(std::end(std::declval<T&>()))>> : std::true_type {};
注意使用
T&
保持引用,防止数组退化为指针!
template <class T>
constexpr std::size_t element_count(T&& x) {
using U = std::remove_reference_t<T>; // 移除引用,防止 auto&/const& 干扰
if constexpr (has_size<U>::value) {
return x.size(); // 如果有 size() 方法
} else if constexpr (is_iterable<U>::value) {
return std::distance(std::begin(x), std::end(x)); // 否则如果可迭代
} else {
return 1; // 否则 fallback:当作单个元素处理
}
}
.size()
,但有 begin/end
struct MyRange {
int data[3] = {1, 2, 3};
int* begin() { return data; }
int* end() { return data + 3; }
const int* begin() const { return data; }
const int* end() const { return data + 3; }
};
int main() {
std::vector<int> v{1, 2, 3}; // 有 size() 方法
int arr[] = {4, 5, 6, 7}; // C 风格数组(注意没有显式数组特化)
int x = 42; // 普通值
std::cout << element_count(v) << "\n"; // 输出 3,调用 .size()
std::cout << element_count(arr) << "\n"; // 输出 4,可迭代,调用 std::distance
std::cout << element_count(x) << "\n"; // 输出 1,fallback
std::cout << element_count(42) << "\n"; // 输出 1,fallback
std::cout << element_count(std::vector<int>{1, 2, 3, 4}) << "\n"; // 输出 4,.size()
std::cout << element_count(MyRange{}) << "\n"; // 输出 3,调用 std::distance
return 0;
}
3
4
1
1
4
3
表达式 | 输出 | 原因说明 |
---|---|---|
element_count(v) |
3 | 调用 .size() |
element_count(arr) |
4 | 数组支持迭代,调用 std::distance |
element_count(x) |
1 | 基本类型,无迭代器,fallback |
element_count(42) |
1 | 字面量,fallback |
element_count(vector{1,2,3,4}) |
4 | 临时对象,有 .size() |
element_count(MyRange{}) |
3 | 支持 begin/end ,调用 std::distance |
如果你想支持数组特化(直接 return N
而不是用 std::distance
),可以添加:
template <typename T, std::size_t N>
constexpr std::size_t element_count(const T (&)[N]) {
return N;
}
std::is_invocable_v
检查函数是否可调用 的一个非常精彩的演示。这个技巧常用于 C++ 模板元编程中检查某个调用是否合法。#include
#include
#include
// 泛型函数:检查 lambda 是否对给定参数可调用
template <class F, class... Args>
constexpr bool check(F&&, Args&&...) {
return std::is_invocable_v<F, Args...>;
}
int main() {
std::cout << std::boolalpha;
// Lambda:尝试访问 x.size()
auto try_size = [](auto x) -> decltype(x.size()) { return x.size(); };
// 测试:int 没有 size(),std::vector 有
std::cout << "check(try_size, 42): " << check(try_size, 42) << '\n'; // false
std::cout << "check(try_size, std::vector{}): " << check(try_size, std::vector<int>{})
<< '\n'; // true
return 0;
}
我们逐步来理解这些例子背后的含义。
std::is_invocable_v
这是 C++17 中的一个类型 trait:
template <class F, class... Args>
bool check(F&&, Args&&...) {
return std::is_invocable_v<F, Args...>;
}
它可以判断:给定函数/函数对象 F
是否可以以参数 Args...
形式被调用。
.size()
,不做 SFINAE 检查check([](auto x){ return x.size(); }, 42);
这个 lambda 会立即对 x.size()
生成代码,但 x = 42
,即 int
没有 .size()
,所以:
check(...)
不会 SFINAE 成功,
编译器直接报错:“request for member ‘size’ in ‘x’, which is of non class type ‘int’”
.size()
放入 decltype(...)
作为返回类型check([](auto x) -> decltype(x.size()) { return x.size(); }, 42);
这里巧妙地使用了:
decltype(x.size())
作为返回类型;.size()
不存在,那么 lambda 的返回类型就不合法 → SFINAE 会踢出该重载;check(...)
利用 is_invocable_v
判定 lambda 是否能被合法调用。false
true
check([](auto x) -> decltype(x.size()) {}, 42);
不再返回任何值,只是通过 -> decltype(...)
来静态检测某个调用是否合法。
// F 是一个 lambda,Args 是参数
template <class F, class... Args>
bool check(F&&, Args&&...) {
return std::is_invocable_v<F, Args...>;
}
然后你可以这样写:
check([](auto x) -> decltype(x.size()) {}, 42); // false
check([](auto x) -> decltype(x.size()) {}, std::vector<int>{}); // true
std::is_invocable
来做“内联 SFINAE”(Substitution Failure Is Not An Error)检测某个表达式是否有效。让我帮你拆解并解释核心思路:#include
#include
#include
// check 函数模板,用来判断给定的 Callable F 是否可以用 Args... 调用
template <class F, class... Args>
constexpr std::is_invocable<F, Args...> check(F&&, Args&&...) noexcept {
return std::is_invocable<F, Args...>();
}
int main(int argc, char* argv[]) {
std::cout << std::boolalpha;
// Lambda 用于检测 x.size() 是否有效:返回类型用 decltype(x.size()),本身不执行任何代码
auto lambda = [](auto x) -> decltype(x.size()) {};
// 传入 int (没有 size() 方法),is_invocable 为 false
std::cout << check(lambda, 42) << std::endl;
// 传入 std::vector (有 size() 方法),is_invocable 为 true
std::cout << check(lambda, std::vector<int>{}) << std::endl;
return 0;
}
decltype(x.size())
:
x
没有 .size()
成员函数,编译器会认为这个 Lambda 的调用无效(substitution failure),不会报错,但该重载会被剔除。std::is_invocable
:
F
是否能被参数类型 Args...
调用。.size()
方法。check(lambda, 42)
返回 false:
int
没有 .size()
,Lambda 不可调用。check(lambda, std::vector{})
返回 true:
std::vector
有 .size()
,Lambda 可调用。decltype
特性,结合 std::is_invocable
实现编译期表达式有效性判断。std::is_invocable
。std::is_invocable
和 SFINAE 机制,将“表达式是否有效”检测封装为几个工具类(validator
,validate
,is_valid
),实现内联 SFINAE 风格的编译期检查。下面我帮你整理完整代码,并加详细注释,帮助你理解分析:
#include
#include
#include
#include
// ================= Validator 类 =================
// validator 接受一组可调用对象(通常是 lambda)
// 它会检查给定参数是否能被所有可调用对象调用成功
template <class... Callables>
struct validator {
// 检查所有 Callables 是否能被 Args 调用(全部调用成功才返回 true)
template <class... Args>
static constexpr bool is_invocable_v = (std::is_invocable_v<Callables, Args...> && ...);
// 把上述 bool 值封装成 std::bool_constant 类型,方便返回和使用
template <class... Args>
using is_invocable = std::bool_constant<is_invocable_v<Args...>>;
// 默认构造函数
constexpr validator() noexcept = default;
// 允许通过一组可调用对象构造 validator(确保传入数量和类型匹配)
template <
class... F,
class = std::enable_if_t<sizeof...(F) == sizeof...(Callables)>,
class = std::enable_if_t<std::is_constructible_v<std::tuple<Callables...>, F...>>
>
constexpr validator(F&&...) noexcept {}
// 函数调用运算符,传入参数,返回对应的 is_invocable 结果
template <class... Args>
constexpr is_invocable<Args...> operator()(Args&&...) const noexcept {
return is_invocable<Args...>();
}
};
// C++17 允许写类模板参数推导(deduction guide),简化使用
template <class... Callables>
validator(Callables&&...) -> validator<std::remove_cvref_t<Callables>...>;
// 判断一个类型是否是 validator
template <class T> struct is_validator : std::false_type {};
template <class... Callables> struct is_validator<validator<Callables...>> : std::true_type {};
template <class T> inline constexpr bool is_validator_v = is_validator<T>::value;
// ================= 辅助工具 remove_cvref (C++20有) =================
template <class T>
struct remove_cvref {
using type = std::remove_cv_t<std::remove_reference_t<T>>;
};
template <class T>
using remove_cvref_t = typename remove_cvref<T>::type;
// ================= validate 函数 =================
// 对一组 Callables 判断传入的 Args 是否都能调用成功(表达式都有效)
template <
class... Args,
class... Callables,
class = std::enable_if_t<((!is_validator_v<remove_cvref_t<Callables>>) && ...)> // 传入的都不是 validator 类型
>
constexpr std::bool_constant<(std::is_invocable_v<Callables, Args...> && ...)>
validate(Callables&&...) noexcept {
return std::bool_constant<(std::is_invocable_v<Callables, Args...> && ...)>();
}
// validate 对 validator 类型的重载版本,直接调用其成员模板
template <class... Args, class... Callables>
typename validator<Callables...>::template is_invocable<Args...>
validate(const validator<Callables...>&) {
return typename validator<Callables...>::template is_invocable<Args...>();
}
// ================= is_valid 类模板 =================
// is_valid 封装表达式检查的结果,可以像 bool 使用
template <class... Args>
struct is_valid {
// 构造时用任意多个 Callables(lambda),并保存所有表达式是否对 Args 都有效
template <class... Callables, class = std::enable_if_t<((!is_validator_v<remove_cvref_t<Callables>>) && ...)>>
constexpr is_valid(Callables&&...) noexcept
: value((std::is_invocable_v<Callables, Args...> && ...)) {}
// 也可以用 validator 来构造
template <class... Callables>
constexpr is_valid(const validator<Callables...>&) noexcept
: value(validator<Callables...>::template is_invocable_v<Args...>) {}
// 转 bool 操作符,方便直接用在 if 语句或布尔表达式中
constexpr operator bool() const { return value; }
private:
bool value;
};
// is_valid 的特化版本,支持传入类型参数和构造参数区分
template <class... Args>
struct is_valid<void, Args...> {
// 构造时只存储传入的值(不做检查)
template <
class... Types,
class = std::enable_if_t<std::is_constructible_v<std::tuple<Args...>, Types...>>
>
constexpr is_valid(Types&&...) {}
// 调用运算符重载,传入多个 Callables,返回对 Args 表达式有效性检测结果
template <
class... Callables,
class = std::enable_if_t<((!is_validator_v<remove_cvref_t<Callables>>) && ...)>
>
constexpr std::bool_constant<(std::is_invocable_v<Callables, Args...> && ...)>
operator()(Callables&&...) const noexcept {
return std::bool_constant<(std::is_invocable_v<Callables, Args...> && ...)>();
}
// 调用运算符重载,传入 validator,调用其 is_invocable 判断
template <class... Callables>
constexpr typename validator<Callables...>::template is_invocable<Args...>
operator()(const validator<Callables...>&) const noexcept {
return typename validator<Callables...>::template is_invocable<Args...>();
}
};
// is_valid 的推导引导
template <class... Args>
is_valid(Args&&...) -> is_valid<void, Args...>;
// ==================== 用法示例 ====================
int main() {
std::cout << std::boolalpha;
// validator 语法,构造一个检测 x.size() 是否有效的对象
auto v = validator([](auto x) -> decltype(x.size()) {});
std::cout << v(42) << "\n"; // false,int 没有 size()
std::cout << v(std::vector<int>{}) << "\n"; // true,vector 有 size()
// validate 语法,传入类型,和一堆 lambda 检测表达式
std::cout << validate<int>([](auto x) -> decltype(x.size()) {}) << "\n"; // false
std::cout << validate<std::vector<int>>([](auto x) -> decltype(x.size()) {}) << "\n"; // true
// is_valid 语法,实例化模板传入类型,构造时传入检测表达式
std::cout << is_valid<int>([](auto x) -> decltype(x.size()) {}) << "\n"; // false
std::cout << is_valid<std::vector<int>>([](auto x) -> decltype(x.size()) {}) << "\n"; // true
// is_valid 结合类型推导,传入具体对象
std::cout << is_valid(42)([](auto x) -> decltype(x.size()) {}) << "\n"; // false
std::cout << is_valid(std::vector<int>{})([](auto x) -> decltype(x.size()) {}) << "\n"; // true
// 更复杂示例:检查多个表达式都有效
std::cout << is_valid<int>(
[](auto i) -> decltype(i + i) {},
[](auto i) -> decltype(i * i) {}
) << "\n"; // true,int 支持加法和乘法
std::cout << is_valid<int, std::vector<int>>(
[](auto i, auto) -> decltype(i + i) {},
[](auto, auto v) -> decltype(v.size()) {}
) << "\n"; // true,int 可以加法,vector 有 size()
return 0;
}
这段代码实现了一套编译期表达式有效性检测(SFINAE + std::is_invocable
)工具,通过**传入可调用对象(通常是 lambda 表达式)**检测给定类型或变量是否支持某些操作(表达式)。它的主要用途是写出模板友好、类型安全的代码,避免无效表达式导致的编译错误。
validator
类模板.size()
)。is_invocable_v
,判断传入的参数类型 Args...
是否都能被所有可调用对象成功调用。operator()(Args&&...)
,直接返回是否所有表达式对给定参数都有效。validator
是“表达式有效性检查器”的封装体,用户定义lambda表达式,封装后即可复用。validate
函数模板validator
),判断它们对给定类型 Args...
是否都能调用成功。validator
对象,直接使用它的成员模板返回结果。validate
提供了两种灵活用法,方便直接验证lambda表达式或直接验证封装好的 validator
。is_valid
类模板validator
和 validate
的思想,封装了表达式是否有效的结果。Args...
有效。validator
构造,也支持无参构造后通过调用运算符传入 lambda 进行检测。is_valid
是“表达式有效性的值对象”,方便作为布尔值直接使用,增强了可读性和易用性。auto v = validator([](auto x) -> decltype(x.size()) {}); // 定义一个检查 x.size() 是否有效的 validator
v(42); // false,int 没有 size() 成员
v(std::vector<int>{}); // true,vector 有 size()
这个例子中:
[](auto x) -> decltype(x.size()) {}
是一个 lambda,尝试访问参数的 size()
成员。validator
将这个 lambda 包装起来,生成一个类型,能在编译期判断传入参数是否能调用该 lambda。v(42)
在编译期检查 decltype(42.size())
,无效,返回 false
。v(std::vector{})
检查 decltype(v.size())
有效,返回 true
。std::is_invocable
和折叠表达式 && ...
判断多个表达式是否全部有效。std::bool_constant
和类型别名方便静态布尔值封装。enable_if
防止错误用法。is_valid
的隐式 bool 转换方便在条件语句中直接使用。validator
类型的专门处理和类型判断,提升灵活度。这套代码利用了现代C++的类型特征(type traits)和模板技巧,实现了一个灵活强大的编译期表达式有效性检测框架。你只需要写出“你想测试的表达式”(用lambda表达式封装),就能在编译时自动判断是否对给定类型有效,从而提高泛型编程的鲁棒性和灵活性。
element_count
函数,根据传入对象的类型自动推断元素数量,核心利用了之前定义的 is_valid
表达式检测机制,实现了**“inline SFINAE”(即时表达式检测与条件分支)**。template <class T>
constexpr std::size_t element_count(T&& x) {
std::size_t count = 0;
// 第一种情况:对象有 size() 方法
if constexpr (is_valid<T>([](auto x) -> decltype(x.size()) {})) {
count = x.size();
}
// 第二种情况:对象有 begin() 和 end(),可以用迭代器计算距离
else if constexpr (is_valid<T>(
[](auto x) -> decltype(std::begin(x)) {},
[](auto x) -> decltype(std::end(x)) {}
)) {
count = std::distance(std::begin(x), std::end(x));
}
// 否则默认元素数量为 1(非容器或无法推断大小的情况)
else {
count = 1;
}
return count;
}
if constexpr
is_valid(...)
is_valid
机制,检测类型 T
是否满足某些表达式的有效性。T
是否支持 .size()
,或者是否有 std::begin()
和 std::end()
。[](auto x) -> decltype(x.size()) {}
用来检测传入对象是否有 .size()
成员。[](auto x) -> decltype(std::begin(x)) {}
, [](auto x) -> decltype(std::end(x)) {}
用来检测对象是否可以用 begin
和 end
迭代。element_count
能智能地选择:
size()
,用 size()
返回元素数量;distance(begin, end)
计算;这段代码是利用之前封装的表达式检测框架,实现了一个智能元素计数器,用到了:
if constexpr
(C++17)实现条件编译;is_valid
机制实现“inline SFINAE”——内联检测表达式有效性;f
,它接受一组不同参数数量和功能的重载函数(通常是多个 lambda),#include
#include
#include
#include
// overload_sequence:存储一组可调用对象,按顺序尝试调用第一个匹配的
template <typename... Fs>
struct overload_sequence {
std::tuple<Fs...> fs; // 保存所有可调用对象的 tuple
// 构造函数,接收一组函数对象(lambda),使用 std::move 初始化 tuple
overload_sequence(Fs... funcs) : fs(std::move(funcs)...) {}
// 函数调用运算符,转发参数,尝试调用重载链条中第一个匹配的函数
template <typename... Args>
auto operator()(Args&&... args) {
return call_impl<0>(std::forward<Args>(args)...);
}
private:
// 递归实现,从第 I 个函数开始尝试调用
template <std::size_t I, typename... Args>
auto call_impl(Args&&... args) {
// 如果递归到末尾仍无匹配,编译时报错
if constexpr (I == sizeof...(Fs)) {
static_assert(I < sizeof...(Fs), "No matching overload found");
} else {
// 获取第 I 个函数对象引用
auto& f = std::get<I>(fs);
// 判断第 I 个函数对象是否能用传入的参数调用
if constexpr (std::is_invocable_v<decltype(f), Args...>) {
// 如果能调用,直接调用并返回结果
return f(std::forward<Args>(args)...);
} else {
// 否则递归尝试下一个函数对象
return call_impl<I + 1>(std::forward<Args>(args)...);
}
}
}
};
// 辅助推导函数,自动推断模板参数类型,去除引用和 cv 限定符
template <typename... Fs>
auto make_overload_sequence(Fs&&... fs) {
return overload_sequence<std::remove_cv_t<std::remove_reference_t<Fs>>...>(
std::forward<Fs>(fs)...);
}
int main() {
// 创建重载序列:
// 1. 匹配带 size() 方法的对象,返回 size()
// 2. 匹配单参数,直接返回参数本身
// 3. 匹配两个参数,返回它们的和
// 4. 匹配三个参数,返回三者和
auto f = make_overload_sequence([](auto x) -> decltype(x.size()) { return x.size(); }, //
[](auto x) { return x; }, //
[](auto x, auto y) { return x + y; }, //
[](auto x, auto y, auto z) { return x + y + z; });
std::cout << f(10, 42) << "\n"; // 调用第三个 lambda,输出 52
std::cout << f(std::vector<int>{1, 2, 3}) << "\n"; // 调用第一个 lambda,输出 3
std::cout << f(10) << "\n"; // 调用第二个 lambda,输出 10
std::cout << f(1, 2, 3) << "\n"; // 调用第四个 lambda,输出 6
}
这段代码实现了一个“自定义的重载序列”overload_sequence
,它保存一组函数(通常是lambda),然后对调用时传入的参数,从头到尾按顺序尝试调用这些函数,直到找到第一个能调用成功的函数并返回结果。
overload_sequence
结构体模板std::tuple<Fs...> fs;
用 std::tuple
来保存所有传入的函数对象(lambda)。overload_sequence(Fs... funcs) : fs(std::move(funcs)...) {}
用传入的函数参数构造 tuple
。operator()
template <typename... Args>
auto operator()(Args&&... args) {
return call_impl<0>(std::forward<Args>(args)...);
}
用户调用 f(args...)
时,调用私有成员函数模板 call_impl
,从第 0 个函数开始尝试调用。call_impl
template <std::size_t I, typename... Args>
auto call_impl(Args&&... args) {
if constexpr (I == sizeof...(Fs)) {
static_assert(I < sizeof...(Fs), "No matching overload found");
} else {
auto& f = std::get<I>(fs);
if constexpr (std::is_invocable_v<decltype(f), Args...>) {
return f(std::forward<Args>(args)...);
} else {
return call_impl<I + 1>(std::forward<Args>(args)...);
}
}
}
I
个函数。I == sizeof...(Fs)
),则编译报错:没有找到匹配的重载。I
个函数对象。std::is_invocable_v
判断该函数能否接受传入参数调用。make_overload_sequence
template <typename... Fs>
auto make_overload_sequence(Fs&&... fs) {
return overload_sequence<std::remove_cv_t<std::remove_reference_t<Fs>>...>(
std::forward<Fs>(fs)...);
}
作用:方便用户创建 overload_sequence
,自动去除引用和 cv 限定符,避免传入引用导致的问题。
main()
函数中的测试auto f = make_overload_sequence(
[](auto x) -> decltype(x.size()) { return x.size(); }, // 有 size() 的调用
[](auto x) { return x; }, // 单参数,直接返回
[](auto x, auto y) { return x + y; }, // 两参数,返回和
[](auto x, auto y, auto z) { return x + y + z; } // 三参数,返回三者和
);
f(10, 42)
调用第三个 lambda(两个参数的那个),输出 52。f(std::vector{1, 2, 3})
调用第一个 lambda,输出 3(vector 的 size)。f(10)
调用第二个 lambda,输出 10。f(1, 2, 3)
调用第四个 lambda,输出 6。if constexpr
结合 std::is_invocable_v
实现调用匹配。overload_set
如何定义和使用,包含你的示例重载函数,以及测试调用:#include
#include
#include
#include
// overload_set:通过多重继承多个可调用对象,实现函数重载集合
template <class... Callables>
struct overload_set : Callables... {
// 继承所有基类(lambda)的 operator(),实现多重重载调用
using Callables::operator()...;
// 构造函数,接受多个可调用对象,分别完美转发给各基类构造函数
template <class... F,
// 约束:保证传入的参数可用于构造所有基类的元组
class = std::enable_if_t<std::is_constructible_v<std::tuple<Callables...>, F...>>>
constexpr overload_set(F&&... f) : Callables(std::forward<F>(f))... {}
};
// 模板参数推导引导,支持直接通过传入lambda自动推断类型
template <class... Callables>
overload_set(Callables...) -> overload_set<Callables...>;
int main() {
// 创建一个 overload_set,包含多种重载的lambda:
// 1. 接收 int 参数,返回 x*10
// 2. 接收 double 参数,返回 x/10
// 3. 接收任意有 size() 方法的对象,返回其 size()
// 4. 接收 std::string,直接返回字符串
// 5. 接收两个参数,返回它们的和
// 6. 接收三个参数,返回三者的和
// clang-format off
auto f = overload_set(
[](int x) { return x * 10; },
[](double x) { return x / 10.; },
[](auto x) -> decltype(x.size()) { return x.size(); },
[](std::string str) { return str; },
[](auto x, auto y) { return x + y; },
[](auto x, auto y, auto z) { return x + y + z; });
// clang-format on
std::cout << f(10) << "\n"; // 匹配 int 重载,输出 100
std::cout << f(10.) << "\n"; // 匹配 double 重载,输出 1
std::cout << f("hello") << "\n"; // 匹配 std::string 重载,输出 hello
std::cout << f(10, 42) << "\n"; // 匹配两个参数重载,输出 52
std::cout << f(std::vector<int>{1, 2, 3}) << "\n"; // 匹配有 size() 的重载,输出 3
std::cout << f(10) << "\n"; // 再次匹配 int 重载,输出 100
std::cout << f(1, 2, 3) << "\n"; // 匹配三个参数重载,输出 6
}
100
1
hello
52
3
100
6
overload_set
通过继承将多个 lambda 组合成一个“多重重载”函数对象。f(x)
时,会自动匹配第一个能够调用的重载。overload_set
真能解决所有情况吗?我们常见的 overload_set
(继承多个 lambda/functor 的结构)看起来很强大:
auto f = overload_set(
[](int x) { return x * 10; },
[](double x) { return x / 10.; },
[](auto x) { return x; }
);
std::cout << f(10.); // 输出 1.0
但实际情况比看起来复杂得多。这个方式适用于很多常规 lambda 表达式,但对一些特殊情况就行不通了。
overload_set
?最常见的用法,完全支持,因为 lambda 是匿名类,内部实现了 operator()
。
operator()
的普通类(functor)struct TimesTen {
int operator()(int x) const { return x * 10; }
};
auto f = overload_set(TimesTen{});
int foo(int);
auto f = overload_set(foo); // 错误,函数不是类,不能继承
正确方式:用 lambda 包裹函数指针:
auto f = overload_set([](int x) { return foo(x); });
struct X { int f(int) const; };
auto pmf = &X::f;
auto f = overload_set(pmf); // 错误,成员函数指针不是类
正确方式:
auto f = overload_set([](X const& obj, int val) { return (obj.*pmf)(val); });
struct Final final {
int operator()(int) const { return 1; }
};
Final f;
auto o = overload_set(f); // 报错,因为不能继承 final 类
正确方式:使用 lambda 包裹:
auto o = overload_set([&f](int x) { return f(x); });
这些都是封装器类型,不是普通类,也不能被继承。
std::function<int(int)> f = [](int x) { return x * 10; };
auto o = overload_set(f); // 报错
正确方式:同样使用 lambda 包裹。
overload_set
中?类型 | 是否可调用 | 是否可继承 | 能否用在 overload_set |
---|---|---|---|
lambda | |||
拥有 operator() 的类 |
|||
函数指针 | (需要 lambda 包裹) | ||
成员函数指针 | (需要 lambda 包裹) | ||
final 类 | (需要 lambda 包裹) | ||
std::function | (需要 lambda 包裹) | ||
std::reference_wrapper | (需要 lambda 包裹) |
因为你希望它支持“所有”可调用对象:
overload_set
基于的是“继承 + operator()
”,这注定无法适配所有类型。overload_set
是一个优雅且强大的技巧,但:
转发函数的目标:
透明地调用一个可调用对象 f
并完美转发参数 args...
给它。
template <class F, class... Args>
constexpr auto call(F&& f, Args&&... args)
noexcept(noexcept(std::forward<F>(f)(std::forward<Args>(args)...)))
-> decltype(std::forward<F>(f)(std::forward<Args>(args)...)) {
return std::forward<F>(f)(std::forward<Args>(args)...);
}
decltype(...)
推断返回值noexcept(...)
推断是否抛异常std::forward
保持参数的左/右值性质decltype(auto)
推断返回类型)template <class F, class... Args>
constexpr decltype(auto) call(F&& f, Args&&... args)
noexcept(noexcept(std::forward<F>(f)(std::forward<Args>(args)...))) {
return std::forward<F>(f)(std::forward<Args>(args)...);
}
简化了返回值书写:不需要尾置 -> decltype(...)
,直接写成 decltype(auto)
。
std::invoke
)template <class F, class... Args>
constexpr decltype(auto) call(F&& f, Args&&... args)
noexcept(std::is_nothrow_invocable_v<F, Args...>) {
return std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
}
std::invoke
的好处:
&Class::func
)&Class::member
)你希望写出这样的语法:
caller(f)(42, "Hello", "World");
也就是让 caller(f)
返回一个对象,这个对象可调用,就像函数一样自动转发参数。
caller
类?我们需要一个类包装器,它:
operator()
转发参数std::invoke
调用目标函数(支持最多种可调用类型)#include
#include
#include
template <typename F>
struct caller {
F func;
// 构造函数:支持右值
explicit caller(F&& f) noexcept(std::is_nothrow_move_constructible_v<F>) : func(std::move(f)) {}
// 构造函数:支持左值
explicit caller(const F& f) noexcept(std::is_nothrow_copy_constructible_v<F>) : func(f) {}
// 调用运算符:转发参数调用原函数
template <typename... Args>
constexpr decltype(auto) operator()(Args&&... args) noexcept(
std::is_nothrow_invocable_v<F, Args...>) {
return std::invoke(func, std::forward<Args>(args)...);
}
};
// 推导辅助函数
template <typename F>
caller(F&&) -> caller<std::decay_t<F>>;
#include
#include
// 推导规则
template <typename F>
caller(F&&) -> caller<std::decay_t<F>>;
int main() {
auto f = [](int a, const char* b, const char* c) {
std::cout << "a: " << a << ", b: " << b << ", c: " << c << '\n';
return a;
};
// f 是左值,允许调用 caller(f)
(caller(f))(42, "Hello", "World");
// 支持函数指针
int (*g)(int) = [](int x) { return x * 2; };
std::cout << caller(g)(5) << '\n'; // 输出 10
}
operator()
转发调用。下面逐步解释你这段代码的关键点和工作原理:
构造一个 caller
类:
caller
类模板template <typename F>
struct caller {
F func;
这个类模板接收一个函数类型 F
,并存储为成员变量 func
。
// 支持右值传入(如 caller(lambda))
explicit caller(F&& f) noexcept(std::is_nothrow_move_constructible_v<F>)
: func(std::move(f)) {}
// 支持左值传入(如 caller(f) 其中 f 是变量)
explicit caller(const F& f) noexcept(std::is_nothrow_copy_constructible_v<F>)
: func(f) {}
caller(f)
语法(尤其是 lambda 左值),你必须提供 const F&
构造函数。caller(std::move(f))
)。template <typename... Args>
constexpr decltype(auto) operator()(Args&&... args) noexcept(
std::is_nothrow_invocable_v<F, Args...>)
{
return std::invoke(func, std::forward<Args>(args)...);
}
std::invoke
调用 func
(兼容函数指针、成员指针等)。Args...
传递给内部的 func
。decltype(auto)
返回结果,保持原始返回值类型。noexcept(...)
自动推导是否 noexcept。template <typename F>
caller(F&&) -> caller<std::decay_t<F>>;
这个推导规则是 C++17 的类模板参数推导(CTAD)。它保证:
caller(f)
自动推导为 caller>
auto f = [](int a, const char* b, const char* c) {
std::cout << "a: " << a << ", b: " << b << ", c: " << c << '\n';
return a;
};
(caller(f))(42, "Hello", "World");
()
包裹 caller(f)
,避免了编译错误(语法规则:不能在声明语句中直接调用类模板推导出的对象)。operator()
,打印 a: 42, b: Hello, c: World
。int (*g)(int) = [](int x) { return x * 2; };
std::cout << caller(g)(5) << '\n'; // 输出 10
10
。这个 caller
实现了:
功能 | 实现方式 |
---|---|
接收任意可调用对象 | 类模板 + 构造函数 |
完美转发参数 | std::forward |
保持返回值类型 | decltype(auto) |
保持异常规范 | noexcept(std::is_nothrow_invocable_v) |
支持临时对象调用 | 使用括号 (caller(f))(...) |
支持函数指针、lambda、仿函数 | 统一通过 std::invoke 实现 |
你这段代码是 C++17 标准下 forwarding 类的优秀示例实现。如果你进一步希望它支持成员函数指针或捕获状态也没问题,它已经支持了。 |
#include
#include
// 模板结构体
template <class T>
struct structure {
using type = T;
type value;
constexpr structure() = default;
// 完美转发构造
template <class U, class = std::enable_if_t<std::is_constructible_v<T, U&&>>>
constexpr structure(U&& x) : value(std::forward<U>(x)) {}
// 非转发访问
decltype(auto) operator()() { return value; }
// 转发访问
decltype(auto) operator()(int) { return std::forward<T>(value); }
};
// 推导指南,关键
template <class U>
structure(U&&) -> structure<std::decay_t<U>>;
// 测试
int main() {
using A1 = decltype(structure{42}()); // A1 = int
using A2 = decltype(structure{42}(0)); // A2 = int&&
static_assert(std::is_same_v<A1, int>);
static_assert(std::is_same_v<A2, int&&>);
}
这段代码展示了一个使用 完美转发(perfect forwarding) 和 类模板参数推导(CTAD) 的结构体 structure
,以及它在转发与非转发调用时推导出的不同类型结果。
structure
template <class T>
struct structure {
using type = T;
type value;
T
是模板参数。value
是类型为 T
的成员变量。template <class U, class = std::enable_if_t<std::is_constructible_v<T, U&&>>>
constexpr structure(U&& x) : value(std::forward<U>(x)) {}
U&&
。std::forward
完美转发并构造 value
。enable_if
限制只有 T
能从 U&&
构造时才参与重载。decltype(auto) operator()() { return value; }
value
的类型值。value
的类型 T
。decltype(auto) operator()(int) { return std::forward<T>(value); }
std::forward(value)
保留 T
的值类别(左值/右值)和 cv 限定。template <class U>
structure(U&&) -> structure<std::decay_t<U>>;
structure
中的 T
类型。U
的引用和 cv 修饰符移除后作为 T
。structure{42}
→ structure
。structure{42}()
的类型推导using A1 = decltype(structure{42}());
过程:
42
是 int
→ 推导出 structure
。operator()()
→ 返回 value
,类型为 int
。A1 = int
。structure{42}(0)
的类型推导using A2 = decltype(structure{42}(0));
过程:
structure
。operator()(int)
,它返回 std::forward(value)
。int&&
(因为 T=int
,并被 std::forward
转发成右值引用)。A2 = int&&
。表达式 | 返回类型 | 原因 |
---|---|---|
structure{42}() |
int |
非转发,直接返回成员 value (类型为 int ) |
structure{42}(0) |
int&& |
转发,std::forward 保留 T=int 的值类别为右值 |
你可以用这个工具打印:
template <typename T>
void print_type() {
std::cout << __PRETTY_FUNCTION__ << std::endl; // GCC / Clang
// std::cout << __FUNCSIG__ << std::endl; // MSVC
}
然后调用:
print_type<A1>();
print_type<A2>();
即可输出 int
和 int&&
类型名。需要 #include
。
当然可以!我们来逐步对比解析这两个调用:
using A1 = decltype(structure{42}()); // 非转发调用
using A2 = decltype(structure{42}(0)); // 转发调用
定义的结构如下:
template <class T>
struct structure {
T value;
// 完美转发构造函数
template <class U, class = std::enable_if_t<std::is_constructible_v<T, U&&>>>
constexpr structure(U&& x) : value(std::forward<U>(x)) {}
// 普通 getter
decltype(auto) operator()() { return value; }
// 带参数 getter,使用 std::forward 转发 value
decltype(auto) operator()(int) { return std::forward<T>(value); }
};
// 推导规则
template <class U>
structure(U&&) -> structure<std::decay_t<U>>;
structure{42}()
using A1 = decltype(structure{42}());
42
是一个 int
,值是右值std::decay_t
→ int
,所以推导为 structure
operator()()
(不转发)value
,是 int
类型的变量int
A1 = int
structure{42}(0)
using A2 = decltype(structure{42}(0));
structure
operator()(int)
(转发版本)std::forward(value)
T
是 int
,value
是左值,但通过 std::forward
被转发为 int&&
(右值引用)int&&
A2 = int&&
表达式 | 调用函数 | 是否转发 | T 类型 |
返回表达式 | 返回类型 |
---|---|---|---|---|---|
structure{42}() |
operator()() |
否 | int |
value |
int |
structure{42}(0) |
operator()(int) |
是 | int |
std::forward |
int&& |
std::forward(value)
的行为取决于 T
本身:
T
是普通类型(如 int
),则结果是 T&&
,即 右值引用T
是 T&
,则 std::forward(...)
是左值引用A2 = int&&
structure{x}(0)
呢?int x = 10;
structure<int&> s{x};
auto&& r = s(0); // 返回 std::forward(x)
此时 r
是 int&,因为:
T = int&
std::forward(x)
展开为 static_cast(x)
,结果是 左值引用T
推导出函数返回类型或类模板成员类型。我帮你详细解析和总结这两大部分(The unqualified type 和 Template argument deduction and forwarding references),并结合表格对理解做辅助说明。
template <class X> X f0(X x);
using T0 = decltype(f0(std::declval<T>()));
template <class X> X f1(const X x);
using T1 = decltype(f1(std::declval<T>()));
template <class X> X f2(const X& x);
using T2 = decltype(f2(std::declval<T>()));
template <class X> auto f3(X x) -> decltype(x);
using T3 = decltype(f3(std::declval<T>()));
auto f4 = [](auto x) -> decltype(x) {};
using T4 = decltype(f4(std::declval<T>()));
template <class X> auto f5(X&& x) { return x; }
using T5 = decltype(f5(std::declval<T>()));
template <class X> auto f6(X&& x) { return std::forward<X>(x); }
using T6 = decltype(f6(std::declval<T>()));
template <class T> struct s7 { s7(T); using type = T; };
using T7 = typename decltype(s7(std::declval<T>()))::type;
template <class T> struct s8 { s8(const T); using type = T; };
using T8 = typename decltype(s8(std::declval<T>()))::type;
template <class T> struct s9 { s9(const T&); using type = T; };
using T9 = typename decltype(s9(std::declval<T>()))::type;
T
类型)T |
T0 | T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8 | T9 |
---|---|---|---|---|---|---|---|---|---|---|
type |
type | type | type | type | type | type | type | type | type | type |
type& |
type | type | type | type | type | type | type | type | type | type |
type&& |
type | type | type | type | type | type | type | type | type | type |
const type |
type | type | type | type | type | type | type | type | type | type |
const type& |
type | type | type | type | type | type | type | type | type | type |
const type&& |
type | type | type | type | type | type | type | type | type | type |
结论: | ||||||||||
这些模板或lambda都是按值传参(X x 、const X x 、const X& x 等)或构造,结果都会退化成不带引用、不带cv修饰的裸类型 type 。 |
||||||||||
即使传入引用或const类型,模板参数 X 都会被剥离引用和cv,推导成纯净类型。 |
template <class X> X f(X&& x);
using T0 = decltype(f(std::declval<T>()));
template <class T> struct s {
template <class U> s(U&&);
using type = T;
};
template <class U> s(U&&) -> s<U>;
using T1 = typename decltype(s(std::declval<T>()))::type;
T |
T0 | T1 |
---|---|---|
type |
type | type |
type& |
type& | const type& (这里根据你的内容是 const type& 但一般推导是 type&) |
type&& |
type | type |
const type |
const type | const type |
const type& |
const type& | const type& |
const type&& |
const type | const type |
X&&
其中 X
是模板参数) 能保持传入参数的引用类型(左值引用或右值引用):
X&
,函数参数类型变成 X& &&
,根据引用折叠规则变成 X&
;X
,参数类型是 X&&
。decltype(f(std::declval()))
中 T0
的结果依赖于传入 T
是左值引用还是右值引用。s
的模板参数推导是基于构造函数模板的参数,推导规则类似。它会把传入参数类型解引用或去掉引用,保持顶层const,但推导细节和编译器有关。传入类型 T |
普通值传递结果(T0-T9) | 转发引用推导结果(T0) | 类模板推导结果(T1) |
---|---|---|---|
type |
type |
type |
type |
type& |
type |
type& |
const type& (?) |
type&& |
type |
type |
type |
const type |
type |
const type |
const type |
const type& |
type |
const type& |
const type& |
const type&& |
type |
const type |
const type |
X x
),传入任何引用类型时,引用会被剥掉,因为实参绑定时会解引用到对象本身。const&
。#include
#include
// 用于打印类型名称(GCC/Clang专用,如果用MSVC需要换其他方式)
template <typename T>
constexpr const char* type_name() {
#ifdef __clang__
return __PRETTY_FUNCTION__;
#elif defined(__GNUC__)
return __PRETTY_FUNCTION__;
#else
return "Unsupported compiler for type_name";
#endif
}
// 判断类型是否为某种类型,用于打印辅助
template <typename T>
void print_type(const char* label) {
std::cout << label << ": " << type_name<T>() << "\n";
}
// struct type {} 代表我们的测试类型
struct type {};
// 简写const type,引用等
using const_type = const type;
using type_ref = type&;
using type_rref = type&&;
using const_type_ref = const type&;
using const_type_rref = const type&&;
// 函数模板组 —— 普通值传递及返回类型推导
template <class X>
X f0(X x);
template <class X>
X f1(const X x);
template <class X>
X f2(const X& x);
template <class X>
auto f3(X x) -> decltype(x);
auto f4 = [](auto x) -> decltype(x) { return x; };
template <class X>
auto f5(X&& x) {
return x;
}
template <class X>
auto f6(X&& x) {
return std::forward<X>(x);
}
// 类模板,带构造函数模板,用于演示CTAD
template <class T>
struct s7 {
template <class U>
s7(U&&) {}
using type = T;
};
template <class U>
s7(U&&) -> s7<U>;
template <class T>
struct s8 {
template <class U>
s8(const U) {}
using type = T;
};
template <class U>
s8(U) -> s8<U>;
template <class T>
struct s9 {
template <class U>
s9(const U&) {}
using type = T;
};
template <class U>
s9(const U&) -> s9<U>;
// 测试宏,方便打印所有类型
#define PRINT_TYPES(T) \
print_type<decltype(f0(std::declval<T>()))>("f0 " #T); \
print_type<decltype(f1(std::declval<T>()))>("f1 " #T); \
print_type<decltype(f2(std::declval<T>()))>("f2 " #T); \
print_type<decltype(f3(std::declval<T>()))>("f3 " #T); \
print_type<decltype(f4(std::declval<T>()))>("f4 " #T); \
print_type<decltype(f5(std::declval<T>()))>("f5 " #T); \
print_type<decltype(f6(std::declval<T>()))>("f6 " #T); \
print_type<typename decltype(s7(std::declval<T>()))::type>("s7 " #T); \
print_type<typename decltype(s8(std::declval<T>()))::type>("s8 " #T); \
print_type<typename decltype(s9(std::declval<T>()))::type>("s9 " #T); \
std::cout << "-----------------------\n";
// 下面是转发引用专门的测试函数
template <class X>
X ff(X&& x);
template <class T>
struct ss {
template <class U>
ss(U&&) {}
using type = T;
};
template <class U>
ss(U&&) -> ss<U>;
#define PRINT_FORWARDING(T) \
print_type<decltype(ff(std::declval<T>()))>("ff " #T); \
print_type<typename decltype(ss(std::declval<T>()))::type>("ss " #T); \
std::cout << "=======================\n";
int main() {
std::cout << "Testing ordinary template argument deduction and return types:\n";
PRINT_TYPES(type)
PRINT_TYPES(type_ref)
PRINT_TYPES(type_rref)
PRINT_TYPES(const_type)
PRINT_TYPES(const_type_ref)
PRINT_TYPES(const_type_rref)
std::cout << "\nTesting forwarding references and class template argument deduction:\n";
PRINT_FORWARDING(type)
PRINT_FORWARDING(type_ref)
PRINT_FORWARDING(type_rref)
PRINT_FORWARDING(const_type)
PRINT_FORWARDING(const_type_ref)
PRINT_FORWARDING(const_type_rref)
return 0;
}
decltype
、std::forward
、结构体模板推导指南 (CTAD) 等机制,全方位分析模板参数在不同上下文中的类型推导行为。下面是对该代码的详细理解与解析:
__PRETTY_FUNCTION__
(GCC/Clang)来获取推导后的类型。print_type
显示函数/模板调用后的类型信息。f0 ~ f6
: 测试值传递、const
修饰、引用、转发引用(完美转发)、lambda 等。s7 ~ s9
: 三种构造函数模板,分别演示如何通过不同构造签名影响类模板参数推导。PRINT_TYPES(T)
: 对一组函数模板 + 类模板进行全面 decltype
检查。PRINT_FORWARDING(T)
: 针对转发引用进行类型推导分析。你定义了如下 6 种测试类型:
using type = struct {};
using const_type = const type;
using type_ref = type&;
using type_rref = type&&;
using const_type_ref = const type&;
using const_type_rref = const type&&;
并对每种类型进行以下测试:
测试项 | 说明 |
---|---|
f0(X x) |
值传递(会丢掉引用、const) |
f1(const X x) |
值传递 + const 修饰(const 仅限形参) |
f2(const X&) |
常量左值引用,不丢引用 |
f3(X x) -> decltype(x) |
尾返回值为形参 x 的类型 |
f4 = [](auto x) -> decltype(x) |
lambda 值捕获参数,返回值推导 |
f5(X&& x) |
万能引用(但返回时没有 forward ) |
f6(X&& x) |
万能引用 + 完美转发 |
s7(U&&) |
使用完美转发构造函数模板进行类模板推导 |
s8(const U) |
const 值拷贝构造进行类模板推导 |
s9(const U&) |
const 左值引用进行类模板推导 |
表达式 | 推导类型说明 |
---|---|
decltype(f0(std::declval |
值传递退化,无 const、引用 |
decltype(f1(std::declval |
同上,形参上的 const 不保留 |
decltype(f2(std::declval |
保留引用/const |
decltype(f5(std::declval |
返回值为 X&& ,但不是转发 |
decltype(f6(std::declval |
真正完美转发,保持原始类型 |
s7/s8/s9 |
用不同构造函数风格推导类模板参数 |
ff
, ss
)这里你测试了:
template <class X>
X ff(X&& x); // 万能引用 + 返回类型为 X
template <class T>
struct ss {
template <class U>
ss(U&&);
using type = T;
};
你用 PRINT_FORWARDING(T)
宏验证 引用折叠法则 和 CTAD(类模板参数推导) 的影响:
ff
的返回值是 推导结果 Xss
通过构造函数模板推导出 T,并定义了 using type = T;
type
| type
|type&
| type&
|type&&
| type
(因右值实参折叠) |const type&
| const type&
|const type&&
| const type
|decltype(std::declval())
在实际模板分析中的用途decltype(std::declval())
是 C++ 类型萃取中最常用的技巧之一,它的作用是:std::declval
是 C++ 中的一个工具函数模板,它本身从不运行,只用于在编译期类型推导中“虚构”一个类型为 T 的值,特别是在 decltype
等不需要实际值、只需要“假设值类型”的上下文中使用。
std::declval()
是为了:构造一个 “类型为 T 的右值”,但不要求 T 有默认构造函数,也不需要实际构造出值。
decltype(std::declval<std::vector<int>>().size())
你想让编译器帮你推导 std::vector
的返回类型。但你又不想构造出 vector
实例(因为也许你根本不能),这时你就用:
std::declval<std::vector<int>>()
这“假装”生成了一个 std::vector
的右值,然后你可以继续调用 .size()
来让 decltype
推导其返回类型。
T()
?你可能想问:
decltype(T().foo())
也可以推导呀?
但——如果 T
是抽象类 / 没有默认构造函数 / 是右值引用类型,你写 T()
会出错!而 std::declval
是纯编译期假值,不构造、不实例化,对任何类型都安全。
#include
#include
#include
struct NoDefault {
NoDefault(int) {}
int value() const { return 42; }
};
int main() {
// 会编译错误:NoDefault 没有默认构造函数
// decltype(NoDefault().value()); 错
// 正确:不用构造对象
decltype(std::declval<NoDefault>().value()) x = 0;
std::cout << x << '\n'; // 编译成功,运行输出 0
}
decltype(std::forward(x))
的结果区别在模板中,T&&
被称为转发引用(forwarding reference)。它的行为取决于实参是左值还是右值:
template<typename T>
void foo(T&& x) {
auto y = std::forward<T>(x); // 精确保留原始值的引用类型(左值还是右值)
}
传入类型 | T 推导结果 | T&& 实际类型 | std::forward 结果 |
---|---|---|---|
int |
int |
int&& |
int&& |
int& |
int& |
int& && → int& |
int& |
const int& |
const int& |
const int& |
const int& |
decltype(std::declval())
:用来制造一个类型为 T 的值,但不实际创建它template <typename T>
using T0 = decltype(std::declval<T>());
这是所有转发的基础。比如我们测试:
decltype(f(std::declval<T>()))
可以知道函数 f()
接受类型 T 后的返回类型。
structure
vs structure
—— 类模板推导指南的区别这部分是整个 talk 的重点。
你有一个 structure
,它有如下构造:
template <class U>
structure(U&&) -> structure<U>; // 推导结果是 decayed 类型
选项 1:structure
(推荐 )
实参 42
(类型是 int
)→ 推导出 U = int
→ 结构体变成 structure
,成员 value
是 int
。
这样你后续:
structure{42}(); // 返回 int
structure{42}(0); // 返回 int&&,因为用了 std::forward(value)
选项 2:structure
(不推荐 )
实参 42
是右值 → 推导出 U = int
→ 所以 U&& = int&&
,没区别。
但如果你传的是左值,比如:
int x = 42;
structure{x}(); // 推导出 U = int&,value 类型是 int&,生命周期绑定原变量!
** 就可能出现悬垂引用等生命周期问题**
reporter
类展示了值的生命周期和转发方式这个类会在构造、复制、移动、调用等操作时打印日志,能帮我们观察:
.operator()()
是 &
还是 &&
被调用?structure1{reporter<>{}}();
使用 structure
(Option 1)时:
operator()()
是 &&
版本 → 说明临时值被转发了!structure
(Option 2)时:U&&
→ 但如果是左值传入,就变成 U = reporter&
,最终变成悬垂引用的可能。推导方式 | 解释 | 推荐? |
---|---|---|
structure(U&&) -> structure |
将 U 解引用并衰减为普通类型,例如 int , std::string |
推荐 |
structure(U&&) -> structure |
原样保留左值/右值引用,可能产生引用悬垂等生命周期问题 | 不推荐 |
对于模板类中的构造函数模板推导(CTAD),推导成
T
而不是T&&
更安全,除非你确定需要引用语义。
以及
std::invoke
到底可以调用哪些类型?有哪些 callable 的种类?
这是 C++ 高级泛型编程和元编程中的一个核心问题,尤其是在设计 invoke
、forwarding
、函数封装器等时极其重要。
在 C++ 中,std::invoke(f, args...)
可以调用的 f,就称为 Callable。
std::invoke
能调用哪些类型?int func(int x, int y) { return x + y; }
std::invoke(func, 1, 2); // OK
auto lam = [](int x, int y) { return x + y; };
std::invoke(lam, 1, 2); // OK
operator()
的类)struct Functor {
int operator()(int x, int y) const { return x + y; }
};
std::invoke(Functor{}, 1, 2); // OK
struct S {
int method(int x) { return x * 2; }
};
S s;
std::invoke(&S::method, s, 5); // OK,等价于 s.method(5)
struct S {
int value = 42;
};
S s;
std::invoke(&S::value, s); // OK,等价于 s.value
Callable 类型 | 示例 | 能否传给 std::invoke |
---|---|---|
函数指针 | int(*)(int) |
|
Lambda | [](int x){ return x; } |
|
函数对象(有 operator() ) |
struct F { int operator()(int); }; |
|
成员函数指针 | &S::func |
(第一个参数是实例) |
成员变量指针 | &S::member |
(第一个参数是实例) |
非静态成员函数 | int S::f() const |
|
函数引用 | int(&f)(int) |
从标准来看,如果 std::invoke(f, args...)
是合法的表达式,那么 f
就是一个 Callable:
template <class F, class... Args>
decltype(auto) invoke(F&& f, Args&&... args);
实际上,std::invoke
背后做了 SFINAE 分发,以支持:
&Class::member
+ 实例)operator()
的函数对象调用以下这些不符合“callable”要求:
operator()
的普通对象)std::invoke(S{}, ...)
会尝试调用 S::operator()
,如果没有就错)可以使用 std::is_invocable
/ std::is_invocable_r
template<typename F, typename... Args>
void check_callable() {
if constexpr (std::is_invocable_v<F, Args...>) {
std::cout << "F is callable with Args...\n";
} else {
std::cout << "F is NOT callable with Args...\n";
}
}
operator()
的关系?实现了 operator()
的对象(函数对象、Lambda)自然是 callable。它们可以用于:
auto f = [](int x) { return x * 2; };
f(10); // OK
std::invoke(f, 10); // OK
只要
std::invoke(f, args...)
是合法的表达式,f
就是 Callable。
好的!我帮你把你给的 4 类函数类型(普通函数、函数左值引用、函数右值引用、函数指针)和它们对应的 48 种限定符版本整理成表格形式。每类 48 种,4 类共 192 种。
序号 | 普通函数类型 | 函数左值引用类型 | 函数右值引用类型 | 函数指针类型 |
---|---|---|---|---|
1 | R(Args...) |
R(&)(Args...) |
R(&&)(Args...) |
R(*)(Args...) |
2 | R(Args..., ...) |
R(&)(Args..., ...) |
R(&&)(Args..., ...) |
R(*)(Args..., ...) |
3 | R(Args...) noexcept |
R(&)(Args...) noexcept |
R(&&)(Args...) noexcept |
R(*)(Args...) noexcept |
4 | R(Args..., ...) noexcept |
R(&)(Args..., ...) noexcept |
R(&&)(Args..., ...) noexcept |
R(*)(Args..., ...) noexcept |
5 | R(Args...) & |
R(&)(Args...) & |
R(&&)(Args...) & |
R(*)(Args...) & |
6 | R(Args...) && |
R(&)(Args...) && |
R(&&)(Args...) && |
R(*)(Args...) && |
7 | R(Args...) const |
R(&)(Args...) const |
R(&&)(Args...) const |
R(*)(Args...) const |
8 | R(Args...) const & |
R(&)(Args...) const & |
R(&&)(Args...) const & |
R(*)(Args...) const & |
9 | R(Args...) const && |
R(&)(Args...) const && |
R(&&)(Args...) const && |
R(*)(Args...) const && |
10 | R(Args...) volatile |
R(&)(Args...) volatile |
R(&&)(Args...) volatile |
R(*)(Args...) volatile |
11 | R(Args...) volatile & |
R(&)(Args...) volatile & |
R(&&)(Args...) volatile & |
R(*)(Args...) volatile & |
12 | R(Args...) volatile && |
R(&)(Args...) volatile && |
R(&&)(Args...) volatile && |
R(*)(Args...) volatile && |
13 | R(Args...) const volatile |
R(&)(Args...) const volatile |
R(&&)(Args...) const volatile |
R(*)(Args...) const volatile |
14 | R(Args...) const volatile & |
R(&)(Args...) const volatile & |
R(&&)(Args...) const volatile & |
R(*)(Args...) const volatile & |
15 | R(Args...) const volatile && |
R(&)(Args...) const volatile && |
R(&&)(Args...) const volatile && |
R(*)(Args...) const volatile && |
16 | R(Args..., ...) & |
R(&)(Args..., ...) & |
R(&&)(Args..., ...) & |
R(*)(Args..., ...) & |
17 | R(Args..., ...) && |
R(&)(Args..., ...) && |
R(&&)(Args..., ...) && |
R(*)(Args..., ...) && |
18 | R(Args..., ...) const |
R(&)(Args..., ...) const |
R(&&)(Args..., ...) const |
R(*)(Args..., ...) const |
19 | R(Args..., ...) const & |
R(&)(Args..., ...) const & |
R(&&)(Args..., ...) const & |
R(*)(Args..., ...) const & |
20 | R(Args..., ...) const && |
R(&)(Args..., ...) const && |
R(&&)(Args..., ...) const && |
R(*)(Args..., ...) const && |
21 | R(Args..., ...) volatile |
R(&)(Args..., ...) volatile |
R(&&)(Args..., ...) volatile |
R(*)(Args..., ...) volatile |
22 | R(Args..., ...) volatile & |
R(&)(Args..., ...) volatile & |
R(&&)(Args..., ...) volatile & |
R(*)(Args..., ...) volatile & |
23 | R(Args..., ...) volatile && |
R(&)(Args..., ...) volatile && |
R(&&)(Args..., ...) volatile && |
R(*)(Args..., ...) volatile && |
24 | R(Args..., ...) const volatile |
R(&)(Args..., ...) const volatile |
R(&&)(Args..., ...) const volatile |
R(*)(Args..., ...) const volatile |
25 | R(Args..., ...) const volatile & |
R(&)(Args..., ...) const volatile & |
R(&&)(Args..., ...) const volatile & |
R(*)(Args..., ...) const volatile & |
26 | R(Args..., ...) const volatile && |
R(&)(Args..., ...) const volatile && |
R(&&)(Args..., ...) const volatile && |
R(*)(Args..., ...) const volatile && |
27 | R(Args...) & noexcept |
R(&)(Args...) & noexcept |
R(&&)(Args...) & noexcept |
R(*)(Args...) & noexcept |
28 | R(Args...) && noexcept |
R(&)(Args...) && noexcept |
R(&&)(Args...) && noexcept |
R(*)(Args...) && noexcept |
29 | R(Args...) const noexcept |
R(&)(Args...) const noexcept |
R(&&)(Args...) const noexcept |
R(*)(Args...) const noexcept |
30 | R(Args...) const & noexcept |
R(&)(Args...) const & noexcept |
R(&&)(Args...) const & noexcept |
R(*)(Args...) const & noexcept |
31 | R(Args...) const && noexcept |
R(&)(Args...) const && noexcept |
R(&&)(Args...) const && noexcept |
R(*)(Args...) const && noexcept |
32 | R(Args...) volatile noexcept |
R(&)(Args...) volatile noexcept |
R(&&)(Args...) volatile noexcept |
R(*)(Args...) volatile noexcept |
33 | R(Args...) volatile & noexcept |
R(&)(Args...) volatile & noexcept |
R(&&)(Args...) volatile & noexcept |
R(*)(Args...) volatile & noexcept |
34 | R(Args...) volatile && noexcept |
R(&)(Args...) volatile && noexcept |
R(&&)(Args...) volatile && noexcept |
R(*)(Args...) volatile && noexcept |
35 | R(Args...) const volatile noexcept |
R(&)(Args...) const volatile noexcept |
R(&&)(Args...) const volatile noexcept |
R(*)(Args...) const volatile noexcept |
36 | R(Args...) const volatile & noexcept |
R(&)(Args...) const volatile & noexcept |
R(&&)(Args...) const volatile & noexcept |
R(*)(Args...) const volatile & noexcept |
37 | R(Args...) const volatile && noexcept |
R(&)(Args...) const volatile && noexcept |
R(&&)(Args...) const volatile && noexcept |
R(*)(Args...) const volatile && noexcept |
38 | R(Args..., ...) & noexcept |
R(&)(Args..., ...) & noexcept |
R(&&)(Args..., ...) & noexcept |
R(*)(Args..., ...) & noexcept |
39 | R(Args..., ...) && noexcept |
R(&)(Args..., ...) && noexcept |
R(&&)(Args..., ...) && noexcept |
R(*)(Args..., ...) && noexcept |
40 | R(Args..., ...) const noexcept |
R(&)(Args..., ...) const noexcept |
R(&&)(Args..., ...) const noexcept |
R(*)(Args..., ...) const noexcept |
41 | R(Args..., ...) const & noexcept |
R(&)(Args..., ...) const & noexcept |
R(&&)(Args..., ...) const & noexcept |
R(*)(Args..., ...) const & noexcept |
42 | R(Args..., ...) const && noexcept |
R(&)(Args..., ...) const && noexcept |
R(&&)(Args..., ...) const && noexcept |
R(*)(Args..., ...) const && noexcept |
43 | R(Args..., ...) volatile noexcept |
R(&)(Args..., ...) volatile noexcept |
R(&&)(Args..., ...) volatile noexcept |
R(*)(Args..., ...) volatile noexcept |
44 | R(Args..., ...) volatile & noexcept |
R(&)(Args..., ...) volatile & noexcept |
R(&&)(Args..., ...) volatile & noexcept |
R(*)(Args..., ...) volatile & noexcept |
45 | R(Args..., ...) volatile && noexcept |
R(&)(Args..., ...) volatile && noexcept |
R(&&)(Args..., ...) volatile && noexcept |
R(*)(Args..., ...) volatile && noexcept |
46 | R(Args..., ...) const volatile noexcept |
R(&)(Args..., ...) const volatile noexcept |
R(&&)(Args..., ...) const volatile noexcept |
R(*)(Args..., ...) const volatile noexcept |
47 | R(Args..., ...) const volatile & noexcept |
R(&)(Args..., ...) const volatile & noexcept |
R(&&)(Args..., ...) const volatile & noexcept |
R(*)(Args..., ...) const volatile & noexcept |
48 | R(Args..., ...) const volatile && noexcept |
R(&)(Args..., ...) const volatile && noexcept |
R(&&)(Args..., ...) const volatile && noexcept |
R(*)(Args..., ...) const volatile && noexcept |
版本 | 含义说明 |
---|---|
R(Args...) |
普通函数,接受参数 Args,返回类型 R |
R(Args..., ...) |
支持可变参数列表(C风格的可变参数,如printf) |
R(Args...) noexcept |
声明函数不会抛出异常,提高编译器优化可能和异常安全 |
R(Args..., ...) noexcept |
支持可变参数且不会抛异常 |
版本 | 说明 |
---|---|
R(Args...) & |
只能通过左值(lvalue)对象调用的成员函数(限定调用对象为左值) |
R(Args...) && |
只能通过右值(rvalue)对象调用的成员函数(限定调用对象为右值) |
R(Args...) const |
成员函数是const ,不会修改对象成员,允许对const 对象调用 |
R(Args...) const & |
const 成员函数,且限定只能由左值调用 |
R(Args...) const && |
const 成员函数,且限定只能由右值调用 |
R(Args...) volatile |
成员函数是volatile ,多用于硬件寄存器或多线程环境,保证内存可变 |
R(Args...) volatile & |
volatile 成员函数,只能由左值调用 |
R(Args...) volatile && |
volatile 成员函数,只能由右值调用 |
R(Args...) const volatile |
同时是const volatile 成员函数 |
R(Args...) const volatile & |
const volatile 成员函数,只能由左值调用 |
R(Args...) const volatile && |
const volatile 成员函数,只能由右值调用 |
总结: |
const
限定:保证不修改对象,可用于读操作&
限定:限制只能在左值(具名变量)调用,防止错误修改临时对象&&
限定:限定只能在右值(临时对象或移动语义)调用,方便实现移动优化volatile
限定:表示对象可能被外部环境修改,通常用于底层编程变参版本就是带C风格可变参数的版本,但同样支持上面那11种限定符组合,比如:
版本 | 说明 |
---|---|
R(Args..., ...) & |
带可变参数,左值限定函数 |
R(Args..., ...) const & |
带可变参数,const限定,左值限定函数 |
… | 其余依次类推 |
noexcept
表明函数不会抛出异常,跟上面限定符组合,如:
版本 | 说明 |
---|---|
R(Args...) & noexcept |
左值限定且不抛异常的成员函数 |
R(Args...) const & noexcept |
const限定,左值限定且不抛异常的成员函数 |
… | 其他组合同理 |
结合变参和noexcept
的限定符版本。
比如R(&)(Args...)
表示函数类型是一个左值引用(reference to function),其他限定符同上,只不过这里是函数引用。一般用得较少,主要用于函数引用传递。
R(&&)(Args...)
是函数的右值引用版本,主要用于模板和完美转发场景。
R(*)(Args...)
是最常见的函数指针类型,函数指针也可以带上这些限定符(const
、volatile
、&
、&&
、noexcept
、变参等)。
限定符类别 | 作用与应用场景 |
---|---|
const |
保护函数不会修改对象状态,安全访问对象 |
volatile |
表示对象可能被外部修改,多用于底层或硬件相关编程 |
& |
限定函数只能由左值调用,避免临时对象误调用 |
&& |
限定函数只能由右值调用,方便实现移动语义和优化 |
noexcept |
函数保证不抛异常,提升优化和异常安全 |
... |
支持C风格可变参数的函数 |
operator()
的类(如lambda和函数对象)。R(Args...)
及其各种cv/ref修饰和noexcept
、变参等(共48种组合)R(&)(Args...)
R(&&)(Args...)
R(*)(Args...)
R(* const)(Args...)
、R(* volatile)(Args...)
等R(*&)(Args...)
、R(*&&)(Args...)
等每个变体下面都有48种不同的组合(不同的
const
、volatile
、&
、&&
、noexcept
等修饰符)
R(C::*)(Args...)
,带各种const
、volatile
、引用限定符等组合const
、volatile
限定,及其引用版本同样是48种组合,且统计了所有这些类型总数。
T C::*
,同样带const
、volatile
、引用限定符等operator()
成员函数的类(如Lambda,函数对象):
operator()
成员的类、struct、unionconst
、volatile
、引用限定符版本的operator()
成员函数这份统计展示了C++函数调用机制的复杂性,尤其是考虑了:
const
/ volatile
限定符&
/ &&
)noexcept
operator()
)的多种形态std::invoke
来统一调用它们,同时说明了一些转换和调用规则。下面我帮你总结和解释它的重点:double add(double x, double y) { return x + y; }
// 直接调用函数指针或函数引用都用 std::invoke:
std::invoke(add, 4, 2); // 结果:6
using add_lref_t = double(&)(double, double);
add_lref_t add_lref = add;
std::invoke(add_lref, 4, 2);
using add_rref_t = double(&&)(double, double);
add_rref_t add_rref = add;
std::invoke(add_rref, 4, 2);
using add_ptr_t = double(*)(double, double);
add_ptr_t add_ptr = add;
std::invoke(add_ptr, 4, 2);
using add_misc_t = double(* const volatile &&)(double, double);
add_misc_t add_misc = add;
std::invoke(add_misc, 4, 2);
作用:
std::invoke
支持传入普通函数、函数引用、函数指针,以及带各种修饰符的指针和引用,统一调用接口。std::invoke
调用。auto x = add; // x 是函数指针类型 double(*)(double,double)
// decltype(auto) y = add;
// 报错,因为函数名不能直接赋值给 decltype(auto) ——函数类型不能被变量持有,只能是指针或引用
struct adder_t {
double operator()(double x, double y) const { return x + y; }
};
adder_t adder;
std::invoke(adder, 4, 2); // 调用operator(),结果6
// 成员函数指针:
using adder_ptr_t = double(adder_t::*)(double, double) const;
adder_ptr_t adder_ptr = &adder_t::operator();
std::invoke(adder_ptr, adder, 4, 2);
using adder_misc_t = double(adder_t::* const volatile &&)(double, double) const;
adder_misc_t adder_misc = &adder_t::operator();
std::invoke(adder_misc, adder, 4, 2);
作用:
operator()
的类对象直接调用。const volatile &&
)的成员函数指针。template <class T>
struct wrapper {
using type = T;
type value;
template <class U, class = std::enable_if_t<std::is_constructible_v<T, U&&>>>
constexpr wrapper(U&& f) : value(std::forward<U>(f)) {}
constexpr operator T() { return value; }
template <class... Args>
constexpr decltype(auto) call(Args&&... args) {
return std::invoke(value, std::forward<Args>(args)...);
}
};
wrapper
包装了任意类型 T
,支持隐式转换为 T
,并且实现了调用接口 call
,内部用 std::invoke
调用封装的可调用对象。std::is_invocable_v
检测包装的各种类型是否可调用:std::is_invocable_v<wrapper<double(*)(double, double)>, double, double>; // true
std::is_invocable_v<wrapper<double(&)(double, double)>, double, double>; // true
std::is_invocable_v<wrapper<double(* const volatile &&)(double, double)>, double, double>; // true
std::is_invocable_v<wrapper<adder_t>, double, double>; // false, 因为wrapper本身不是可调用对象
std::is_invocable_v<decltype(&wrapper<adder_t>::value), wrapper<adder_t>>; // true,访问value成员是可调用的
std::is_invocable_v<decltype(&wrapper<adder_t>::call<double, double>), wrapper<adder_t>, double, double>; // true
operator()
的类、结构体、联合体实例(闭包类型lambda等)std::invoke
统一了对这些类型的调用方式。overload_set
能兼容所有这些情况几乎是“噩梦”。overload_set
,需要构造一组类型特征(type traits),来判断和操作各种可调用类型。remove_all_pointers
)copy_const
,copy_volatile
,copy_cv
,copy_reference
)empty_base
,is_inheritable
等)is_closure
,is_functor
,is_function_object
,is_callable
)index_constant
,false_v
,true_v
)overload_set
类,兼容所有可调用类型,但:主题 | 说明 |
---|---|
重载集合类实现难点 | 各种可调用类型的多样性和复杂修饰符导致包装和继承复杂,特别是final类和union |
需要的工具 | 复杂的类型特征(type traits)用来移除指针、复制限定符、判断可调用性、继承判断等 |
已有工作 | P1016R0标准提案和开源type-utilities库提供了辅助工具 |
目前无法解决的限制 | final类和union因不可继承无法包装 |
未来展望 | C++反射特性有望解决这些问题 |
如果你想,我可以帮你: |
overload_set
如何实现和工作原理,用一行代码拿到:
using type = decltype(
some_tool(
some_other_tool<int>(/*...*/),
some_other_tool<std::vector<int>>(/*...*/),
some_other_tool<double>(/*...*/)
)
);
这个 type
会是满足某个条件的第一个有效类型,比如第一个可调用lambda且能访问 .size()
的类型。
invalid_option
结构体作为失败标记struct invalid_option {};
表示“该类型不满足要求”,类似错误标志。
type_option
模板函数template <class T, class... F>
constexpr std::conditional_t<(std::is_invocable_v<F, T> && ...), T, invalid_option> type_option(F&&...);
T
和一个或多个函数对象 F
。F
是否都可以被 T
调用(std::is_invocable_v
)。type_option
的返回类型就是 T
,否则是 invalid_option
。type_selector
模板函数template <class... T>
constexpr std::tuple_element_t<
tuple_index_v<
std::true_type,
std::tuple<
std::conditional_t<
std::is_same_v<T, invalid_option>,
std::false_type,
std::true_type
>...
>
>,
std::tuple<T...>
> type_selector(T&&...);
T...
,这些类型可能是有效的或者是 invalid_option
。invalid_option
的类型(第一个有效类型)。tuple_index_v
是一个辅助模板,用来找出第一个 std::true_type
出现的位置。int main() {
auto lambda = [](auto x) -> decltype(x.size()) {};
using type = decltype(type_selector(
type_option<int>(lambda), // int 不满足 lambda(x).size()
type_option<std::vector<int>>(lambda), // std::vector 满足
type_option<double>(lambda) // double 不满足
));
print_type<type>(); // 输出 std::vector
return 0;
}
type_option(lambda)
-> invalid_option
type_option>(lambda)
-> std::vector
type_option(lambda)
-> invalid_option
type_selector
选择第一个不是 invalid_option
的类型,即 std::vector
。if constexpr
或者模板偏特化。工具类别 | 代表工具 | 作用说明 |
---|---|---|
修饰符复制 | copy_const , copy_volatile , copy_reference 等 |
复制某个类型的 const 、volatile 、引用等修饰符到另一个类型,方便保持类型属性一致。 |
修饰符克隆 | clone_const , clone_volatile , clone_reference 等 |
克隆类型的修饰符,类似复制但更全面,处理多维数组、指针等复杂修饰符。 |
SFINAE相关 | validator , validate , is_valid |
实现编译期检查代码有效性(SFINAE),判断模板实例化是否成功。 |
重载序列 | overload_sequence , overload_sequence_r , overload_sequence_nothrow 等 |
构造重载序列用于函数重载决策,支持异常安全和右递归形式。 |
转发重载序列 | forwarding_overload_sequence , forwarding_overload_sequence_r 等 |
能够转发参数的重载序列,支持完美转发。 |
重载集合 | overload_set , forwarding_overload_set , overload_set_selector |
将多个重载函数或可调用对象封装成一个集合,实现重载决策。 |
类型选择 | type_selector , type_option , invalid_option |
在多个候选类型中选出满足条件的第一个类型,invalid_option 作为失败标记。 |
指针移除 | remove_all_pointers |
递归去除类型中的所有指针修饰。 |
继承相关 | empty_base , is_instantiable , is_inheritable , inherit_if |
处理继承关系和条件继承,支持空基类优化和可继承检测。 |
可调用分类 | is_closure , is_functor , is_function_object , is_callable |
判断类型是否是闭包、函数对象或可调用对象。 |
杂项辅助 | index_constant , type_t , false_v , true_v , tuple_index , remove_cvref , print_type , reporter |
常用模板工具,简化编程和调试,print_type 便于调试类型,tuple_index 查找类型索引等。 |
这些工具组合使用,可以极大简化复杂模板元编程工作,尤其是: