CppCon 2018 学习:From Metaprogramming Tricks to Elegance

#这段内容是 Vincent Reverdy 在 CppCon 2018 演讲的一部分,核心主题是:

这场演讲的核心问题是:

如何在 C++17 中提高代码的通用性(Genericity),而不牺牲代码的可读性与简洁性?

具体要解决的问题是:

你希望写出既通用可维护的模板代码。换句话说,你希望能像这样思考和写代码:

如果满足某个条件,就执行这个函数:

if (some_condition) {
    do_this();
}

让编译器自动选中可编译的那个函数:

  • 你有一个函数列表
  • 你有一组参数
  • 编译器会选择第一个与这些参数兼容的函数来调用

或更进一步:

  • 你有一个表达式列表类型列表
  • 编译器会尝试将这些表达式或类型“替换”进代码中
  • 只要能成功编译,那就选择那个版本

所以,这是关于模板元编程中的 SFINAE(替换失败不是错误)技巧的泛化与简化

你可以理解为:

在不引入代码爆炸的情况下,使用 C++17 中的 constexpr, if constexpr, decltype, void_t 等工具,实现“在一堆模板可能性中,选择那个能编译通过的方案”。

演讲进一步探讨了:

  • 如何组织模板元函数(例如以函数列表形式)
  • 如何用 “第一个能编译的表达式” 原则来选择重载
  • 如何让这些逻辑更清晰、更可读,而不是成为“只有模板大师能看懂”的魔法代码

总结:

这是一次关于“在现代 C++(特别是 C++17)中,如何用更优雅、更可维护的方式实现强大泛型功能”的演讲。
如果你想深入理解这套机制(比如如何自己实现函数列表选择、类型推导、模板条件判断等),我可以给你演示一些小例子,比如:

  • constexpr ifstd::enable_if 的替代
  • 如何写一个“按参数类型选择最佳函数”的框架
  • 如何用 std::void_tdecltype 判断某个表达式是否有效

关于 C++ 中的一个核心模板编程机制:SFINAE(Substitution Failure Is Not An Error),也叫“替换失败不是错误”。

我来帮你翻译并解释清楚它的原理、用途和限制

什么是 SFINAE?

SFINAE(Substitution Failure Is Not An Error) 是 C++ 模板系统中的一个规则,用来在模板实例化过程中优雅地处理失败

中文解释:

当我们在使用模板时,将某些类型代入模板参数中,如果这会导致代码不合法(比如调用了不存在的成员函数),编译器不会报错,而是简单地忽略这一个重载或特化版本,继续尝试其他可能的重载。

来自不同来源的定义与说明:

维基百科(Wikipedia)说:

替换失败不是错误(SFINAE)是指在 C++ 中,如果模板参数的某个替换导致无效,这本身不被认为是错误

cppreference 解释:

这个规则应用在函数模板重载解析过程中。

如果某个候选模板,在用实际类型替换模板参数时失败了,该模板会被从重载集合中排除,而不是立即引发编译错误。

这个特性是 模板元编程(template metaprogramming) 的核心技术之一。

C++ 标准草案 [temp.deduct]/8 原文精要:

这是标准中对 SFINAE 机制的技术性描述:

  • 当模板替换导致无效类型或表达式时,类型推导失败
  • “无效”表示:如果用这个类型或表达式写代码,会违反语法规则,并需要诊断(也就是报错)
  • 只有函数**类型签名的“直接上下文”**中的无效替换才会触发推导失败
    • 比如函数返回类型、参数类型、explicit 声明等
  • 不包括间接触发的错误(如类模板特化、隐式生成函数等),这类情况仍然会让程序非法(ill-formed)

示例讲解

来看一个典型的 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)所需的工具。
模板结构体部分(SFINAE 检测逻辑)
// 默认模板:假设没有 foo(),所以继承自 false_type
template <typename, typename = std::void_t<>>
struct has_foo : std::false_type {};
  • 这是主模板,也叫 fallback,当后面偏特化无法匹配时就使用这个。
  • 第二个模板参数是默认参数 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()) 是合法表达式时才会匹配。
  • 如果 Tfoo() 成员函数,那么这个表达式是合法的,就会选择这个特化版本,结果为 true_type
decltype(std::declval().foo()) 是什么?
  • std::declval() 返回一个 “假的” T 实例(仅用于推导,不会真的调用)。
  • .foo() 尝试调用 Tfoo() 方法。
  • decltype(...) 用于检测是否编译通过。
  • 如果编译能通过,就说明 Tfoo(),否则 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()"); //  成功编译
}

拓展理解

你也可以检测特定函数签名,比如:

// 检测是否有 `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 表达式合法就说明存在该函数

你提供的代码是一个经典的 SFINAE 示例(来自 cppreference),演示了如何通过 SFINAE(Substitution Failure Is Not An Error)机制,根据传入参数的类型,选择不同的函数重载

下面是这段代码的纠正版本,并附上逐行中文注释与理解解释

修正后的代码 + 中文注释

#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)() 才是合法的语法。

SFINAE 原理小结

  • 当模板函数中使用 decltype(...) 推导时,如果表达式非法,不会报错,只是该重载不参与重载决议
  • 这就允许我们通过合法性来筛选合适的函数重载

每行理解总结:

行号 代码 中文解释
4-5 void test(...) 通配版本,匹配任何参数,优先级最低
8-9 template auto test(C&& c, F f) 如果 c.*f 合法,说明是对象+成员函数指针,匹配该版本
12-13 template auto test(C* c, F f) 如果 c->*f 合法,说明是指针+成员函数指针,匹配该版本
16 struct X { void f(); }; 测试结构体,有一个成员函数 f()
19-24 main() 调用 test() 测试三个不同情况的匹配情况

这是 C++ 中一个非常巧妙的技巧,用于类型萃取(type trait)和 SFINAE 判断

表达式:

decltype(..., void())

关键词解释:

符号 含义
decltype(...) 编译期获取表达式的类型
,(逗号运算符) C++ 中的逗号表达式运算符:(a, b) 的结果是 b,但会先计算 a
void() 一个合法的返回 void 的值(空函数调用的意思)

逐步解释:

1⃣ decltype(..., void()) 是一个使用 逗号表达式decltype

decltype(expr1, expr2)

这会返回 expr2 的类型,但会在 编译时先验证 expr1 是否合法

2⃣ 所以:

decltype((void)((c->*f)()), void())

这个表达式的行为就是:

如果 (c->*f)() 是一个合法的成员函数调用,

然后忽略它的返回值(用 (void) 转换),

最终整个 decltype(...) 返回的是 void 类型。

3⃣ 如果 (c->*f)() 不合法会怎样?

  • 编译器尝试解析 (c->*f)()
  • 如果不合法(比如 c 不是指针,或 f 不是成员函数指针),
  • 替代失败(Substitution Failure);
  • SFINAE 启动:这个函数模板就会被排除掉,不进入重载集。

举个例子(更通俗一点)

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
    • 如果不合法,就触发 SFINAE

总结(一句话)

decltype(expr1, void())

的作用是:

用逗号表达式忽略 expr1 的具体类型,只要它能编译,就让 decltype(...) 返回 void,否则 SFINAE。

这段代码是一个非常经典的 std::enable_if 与类型萃取(std::is_integral / std::is_floating_point)结合使用的 C++11 示例,展示了如何在函数模板类模板特化中根据类型做分支(即编译时选择行为)。

我将为你做三件事:

1. 修正代码格式(特别是中文字符和特殊符号)

2. 逐行添加中文注释解释逻辑

3. 总结它的核心思想和运行原理

修正后代码(带详细中文注释)

#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

代码核心理解要点

1⃣ std::enable_if_t<...> 是什么?

  • 它是 C++11 的一种 编译期类型选择工具
  • 如果里面的条件为 true,返回 typename = void,可以参与模板匹配;
  • 如果条件为 false,替代失败(SFINAE),该模板将被忽略。

2⃣ 函数模板重载逻辑

function(5);     // T = int → is_integral_v = true → 匹配整数版本
function(0.5);   // T = double → is_floating_point_v = true → 匹配浮点版本

注意这两个模板参数必须略有不同(如参数引用 vs 值),否则会因“模板重定义”报错。

3⃣ 结构体模板特化逻辑

structure<double>{}();  // double 不是 integral → 匹配默认模板
structure<int>{}();     // int 是 integral → 匹配偏特化模板

这种方式常用于为不同类型提供编译期行为定制。

总结(一句话)

本例展示了如何使用 std::enable_if 结合类型 traits,根据类型在编译期选择不同的函数或结构体行为,是 C++ 模板元编程中的重要工具。

这段代码展示了如何用 C++17 的 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&>()
    • 用来模拟一个 T 的左值引用,解决了数组类型(如 int[4])传递时的匹配问题。
    • 保证传递给 std::beginstd::end 的参数是左值引用,因为 std::begin 对数组的重载接受左值引用。
  • is_iterable 结构体
    • 默认继承 false_type 表示不可迭代。
    • 偏特化版本继承 true_type 表示可迭代。
      这段代码能够检测包括标准容器、C 风格数组是否可用于范围 for 循环。

SFINAE(Substitution Failure Is Not An Error)、std::enable_ifstd::void_t 在 C++ 元编程中的优缺点,意思是:

主要观点和理解

  • SFINAE、std::enable_ifstd::void_t 是非常强大的元编程工具
    它们可以用来实现各种复杂的模板技巧和泛型编程,使代码更加通用和灵活。
  • 但这些工具通常需要很多技巧和“套路”才能发挥作用
    这意味着学习和写出正确的 SFINAE 代码不简单,需要深刻理解模板元编程。
  • 表达意图不够清晰,代码的可读性和可维护性较差
    使用 SFINAE 往往需要定义很多辅助函数或类模板特化,代码显得冗长且难懂。
  • 经常会导致大量模板样板代码(boilerplate)
    这会影响代码局部性(locality),降低代码整洁度。
  • 虽然 C++20 的 concepts(概念)在一定程度上解决了这些问题
    concepts 使得表达模板约束更加直观和简洁,但 SFINAE 仍然有其用武之地,不能完全替代。
  • 理想状态是能写出“内联”的 SFINAE,避免额外的模板样板代码
    也就是说,希望能直接在代码里简洁地使用 SFINAE 而不用写大量额外的模板辅助结构。

总结一句话

SFINAE 等模板技巧功能强大但用起来繁琐,代码不易读写,未来更简洁、更直接的约束表达方式(如 concepts)会更好,但 SFINAE 依然是重要工具。
如果想,我可以帮你举例对比一下 SFINAE 和 concepts,或者帮你写更简洁的模板约束示例!

详细注释、分析及对“immediate context”和“non-immediate context”区别的理解:

#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
}

重点分析

1. Immediate Context (直接上下文) — immediate_context

  • SFINAE 的判断写成 typename T::type,这是对模板参数 T 的直接依赖(immediate context),
  • 只有当 T 本身有嵌套类型 type 时,模板才能实例化成功,否则会 SFINAE(失败但不报错)。
  • 由于 int 是基本类型,没有成员 type,所以尝试实例化 immediate_context 的模板失败,退化调用到 immediate_context(...)
    但这段代码 immediate_context(0) 能“工作”,说明调用的是 catch-all 版本,不是模板版本。

2. Non-Immediate Context (非直接上下文) — nonimmediate_context

  • SFINAE 判断写成 typename trait::type,这里依赖 trait,也就是依赖 T 的包装类型,属于 非立即上下文
  • trait 试图访问 T::type,如果 T 没有嵌套 type,那么 trait::type 会报错(不是 SFINAE),因为 trait 不存在 type,而模板参数默认值依赖于该 trait::type,导致编译错误。
  • 因此,nonimmediate_context(0) 直接导致错误,而不是降级到 catch-all。

为什么?

  • SFINAE 只适用于模板参数的“直接上下文”(immediate context)中的替换错误。
  • typename T::type 是直接上下文。
  • typename trait::type 是非直接上下文(因为访问是通过 trait,而不是直接在模板参数上)。

总结

调用 是否有效 原因
immediate_context(0) 有效,调用 catch-all int 没有 type,模板失败,使用非模板 catch-all 重载
immediate_context(structure{}) 有效,调用模板版本 structuretype,模板成功实例化
nonimmediate_context(0) 编译错误 traittype,非立即上下文错误导致编译失败
nonimmediate_context(structure{}) 有效,调用模板版本 trait>type,模板成功实例化

结论

这段代码演示了:

  • SFINAE 只对直接上下文(immediate context)有效,模板替换失败时会降级为其他重载或不报错。
  • 如果错误在非直接上下文发生,会变成编译错误,而不是 SFINAE 替换失败。
  • 这是 C++ 模板元编程中经常遇到的坑,理解“immediate context”是掌握 SFINAE 的关键。

这个问题围绕的是 如何构造一个带“优先级”的函数模板重载集,实现行为如下:

目标:实现 element_count(T&&),根据类型 T 自动选择最合适的实现

按照优先级:

  1. 如果 T.size() 成员函数 → 调用它;
  2. 否则,如果 T 可以用 std::begin()std::end() → 用 std::distance(begin, end) 计算数量;
  3. 否则 → 返回 1,假设只有一个元素(例如标量类型)。

使用的技巧:

“排序重载集(ranked overload set)”+ SFINAE:

  • 用递增优先级的“标签类型” rank 来引导重载匹配;
  • 使用 SFINAE 检测条件是否成立;
  • 非可变参数模板优先于可变参数模板(模板偏特化优先级)来形成“最合适的匹配”。

示例实现

#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,表示一个默认元素。

二、使用的关键技术

1. rank:控制函数模板的匹配优先级

template <int N> struct rank : rank<N - 1> {};
template <> struct rank<0> {};
  • 它是一个继承链,rank<2>rank<1> 的子类,以此类推;
  • 编译器会优先匹配参数类型更“具体”的函数版本(rank<2> > rank<1> > rank<0>);
  • 实现手动排序重载集,从而实现“更高级匹配优先”。

2. 三个版本的 element_count 函数模板(按优先级降序)

优先级 2:使用 .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();
}
  • 如果 Tt.size() 成员函数,这个重载成立;
  • 返回类型 decltype(t.size(), size_t{}) 使用逗号表达式确保结果是 size_t
优先级 1:使用 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() 计算元素数量。
优先级 0:兜底版本
template <typename T>
size_t element_count(T&&, rank<0>) {
    std::cout << "Called default fallback (return 1)" << std::endl;
    return 1;
}
  • 万一以上两个版本都失败,这个默认版本始终可用;
  • 假设输入是标量、结构体或不可迭代类型,返回 1

3. 接口函数

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 策略 保证任何类型都能有返回结果

展示的是一个相当高阶的 C++ 模板技巧,它使用了:

#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
}
  • SFINAE(Substitution Failure Is Not An Error)
  • 可变参数模板(variadic templates)
  • 重载分派(通过非可变/可变参数模板区别优先级)
  • std::enable_ifdecltype 来做条件启用
    目标是实现一个通用的 element_count() 函数,根据类型不同行为如下:

功能目标总结:

条件 使用方式
如果有 .size() 成员函数 调用 .size()
否则如果支持 begin()/end() 使用 std::distance() 计算
否则如果是 std::initializer_list 特化版本
否则 返回 1,假设是一个单元素
下面我们逐段标注注释与理解:

Part 1: container_element_count(内部实现)

1. 有 .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()
}

2. 可迭代类型 — 使用 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() 不存在时才被考虑

Part 2: element_count(外部接口)

3. 对 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} 这类临时初始化列表;
  • 没有用 SFINAE,只是函数重载。

4. 正常类型 → 调用 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() 不适用,就替代失败,转向下面的变参模板。

5. 兜底 fallback:不可迭代、无 .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

下面我将:

  1. 给出完整代码(含注释)
  2. 分析 overload_rank 技巧和 SFINAE 如何协同工作
  3. 总结优缺点
  4. 提出 C++20 concepts 的改进方向

一、完整代码 + 中文注释

1. 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>
  • 编译器在重载解析时优先尝试更“深层”的重载(即优先级更高的版本)。

2. 各种重载版本的 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;
}

3. 通用接口(从最高优先级开始匹配)

template <class T>
std::size_t element_count(T&& x) {
    return element_count(overload_rank<2>{}, std::forward<T>(x));
}

4. 示例用法

#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()) 用于判断类型是否有 .size() 成员函数
SFINAE + 默认模板参数 条件启用重载模板(当表达式合法时才匹配)
变参与非变参版本结合 形成 fallback 机制

三、优缺点总结

优点:

  • 能用在 C++11 中,无需 C++20 支持;
  • 非常灵活,支持分层逻辑;
  • 使用 overload_rank 比递归模板更清晰、更具可控性。

缺点:

  • 样板代码多(每层都需要写一份函数模板);
  • 阅读困难,需要理解 SFINAE、decltype、重载优先级;
  • 不直观,表达意图(比如“类型是否可迭代”)不够清晰;
  • 和实际业务逻辑混杂,容易造成代码局部性差(scattered)。

四、提升方向:使用 C++20 Concepts(推荐)

如果你可以用 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;
}

这段代码:

  • 表达意图非常清晰;
  • 没有样板代码;
  • 易读易维护。

最后总结:关于 SFINAE 的评价

你最后提到的总结:

  • 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 是未来的方向,但两者仍然可以共存。

记录一下常用的汇编指令很多,不同架构有差异,下面以x86/x86-64架构为例,列出一些基础且常见的汇编指令,帮助你入门:

1. 数据传送指令(Data Transfer)

指令 作用 说明
mov 传送数据 将数据从源操作数复制到目的操作数
lea 加载有效地址 将内存地址计算结果加载到寄存器
push 压栈 把数据压入栈
pop 弹栈 把数据从栈中弹出

2. 算术运算指令(Arithmetic)

指令 作用 说明
add 加法 将两个操作数相加
sub 减法 两个操作数相减
inc 加一 操作数加1
dec 减一 操作数减1
imul 有符号乘法 有符号整数乘法
idiv 有符号除法 有符号整数除法

3. 逻辑运算指令(Logical)

指令 作用 说明
and 按位与 两个操作数按位与
or 按位或 两个操作数按位或
xor 按位异或 两个操作数按位异或
not 按位取反 对操作数按位取反
shl 左移 将操作数左移若干位
shr 右移 将操作数右移若干位

4. 控制流指令(Control Flow)

指令 作用 说明
jmp 无条件跳转 跳转到指定地址
je/jz 条件跳转(等于/零) 如果ZF=1,则跳转
jne/jnz 条件跳转(不等于/非零) 如果ZF=0,则跳转
jg/jnle 条件跳转(大于) 如果满足有符号大于条件跳转
jl/jnge 条件跳转(小于) 如果满足有符号小于条件跳转
call 调用子程序 调用指定地址的函数
ret 返回 从函数返回

5. 比较指令(Compare)

指令 作用 说明
cmp 比较两个操作数 通过设置标志位(ZF、CF等)反映结果

6. 其他常用指令

指令 作用 说明
nop 空操作 什么都不做,常用于对齐
int 软中断 触发中断,用于系统调用
cli 清除中断标志 禁止中断
sti 设置中断标志 允许中断

小结

  • mov 是最基础的指令,做数据传输
  • add、sub、inc、dec 处理算术运算
  • jmp 和条件跳转指令 控制程序流程
  • cmp 和条件跳转配合使用,完成条件判断
    如果你需要我帮你写一段示例代码,或者介绍某条指令的详细用法,告诉我哈!
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                      
; 返回到调用函数

这段代码的核心思想是用 C++17 的 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 的类型特性,并返回其“元素数量”:

  1. 优先使用 .size() 方法(如 std::vector
  2. 如果不可用 .size(),则判断是否可迭代(std::begin + std::end
  3. 否则默认返回 1(如基本类型 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& 保持引用,防止数组退化为指针!

核心函数:element_count
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;
}

提供的例子是 使用 lambda 表达式 + SFINAE + 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... 形式被调用。

错误例子一:lambda 中直接访问 .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

这段代码展示了 如何使用 Lambda 和 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;
}

核心点:

  1. decltype(x.size())
    • 这个返回类型声明会触发 SFINAE。如果传入的类型 x 没有 .size() 成员函数,编译器会认为这个 Lambda 的调用无效(substitution failure),不会报错,但该重载会被剔除。
  2. std::is_invocable
    • 标准库类型特征,用于判断传入的可调用对象 F 是否能被参数类型 Args... 调用。
    • 结合上面 lambda 的返回类型 SFINAE,就能判断传入参数是否拥有 .size() 方法。
  3. check(lambda, 42) 返回 false
    • 因为 int 没有 .size(),Lambda 不可调用。
  4. check(lambda, std::vector{}) 返回 true
    • 因为 std::vector.size(),Lambda 可调用。

总结:

  • 这是一个非常优雅的“内联”SFINAE检测方法。
  • 利用 Lambda 的返回类型 decltype 特性,结合 std::is_invocable 实现编译期表达式有效性判断。
  • 不用写复杂的模板特化和启发式检测,只需一个简单的 Lambda 和 std::is_invocable

你给的代码片段是一个非常通用且灵活的“表达式有效性验证器”实现,利用 C++17 的 std::is_invocable 和 SFINAE 机制,将“表达式是否有效”检测封装为几个工具类(validatorvalidateis_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 表达式)**检测给定类型或变量是否支持某些操作(表达式)。它的主要用途是写出模板友好、类型安全的代码,避免无效表达式导致的编译错误。

主要组件及其作用

1. validator 类模板

  • 接收若干个可调用对象(Callables),通常是lambda表达式,这些lambda表达式定义了需要验证的表达式(比如访问 .size())。
  • 提供静态成员模板 is_invocable_v,判断传入的参数类型 Args... 是否都能被所有可调用对象成功调用。
  • 通过调用运算符 operator()(Args&&...),直接返回是否所有表达式对给定参数都有效。
    总结validator 是“表达式有效性检查器”的封装体,用户定义lambda表达式,封装后即可复用。

2. validate 函数模板

  • 普通版本:接受一组 lambda(非 validator),判断它们对给定类型 Args... 是否都能调用成功。
  • 重载版本:接受一个 validator 对象,直接使用它的成员模板返回结果。
    总结validate 提供了两种灵活用法,方便直接验证lambda表达式或直接验证封装好的 validator

3. is_valid 类模板

  • 结合了 validatorvalidate 的思想,封装了表达式是否有效的结果。
  • 可以通过构造函数传入多个lambda,检测是否所有表达式对类型 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 和类型别名方便静态布尔值封装。
  • 结合 SFINAE 与 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;
}

关键点解析

1. if constexpr

  • 这是 C++17 的编译期条件语句,只会编译满足条件的分支,不满足的分支会被丢弃。
  • 避免了无效代码的编译错误,实现“编译期条件选择”。

2. is_valid(...)

  • 使用前面实现的 is_valid 机制,检测类型 T 是否满足某些表达式的有效性。
  • 这里检测 T 是否支持 .size(),或者是否有 std::begin()std::end()

3. 检测的表达式是通过 lambda 来实现

  • [](auto x) -> decltype(x.size()) {} 用来检测传入对象是否有 .size() 成员。
  • [](auto x) -> decltype(std::begin(x)) {}, [](auto x) -> decltype(std::end(x)) {} 用来检测对象是否可以用 beginend 迭代。

这段代码实现的功能和优势

  • 通过 内联表达式检测(inline SFINAE)element_count 能智能地选择:
    • 如果对象有 size(),用 size() 返回元素数量;
    • 否则如果支持迭代器,则用 distance(begin, end) 计算;
    • 否则默认元素数量为1。
  • 无须写多个模板特化或重载,只用一个函数体就完成了多态行为。
  • 编译期判断,运行时没有额外开销

总结

这段代码是利用之前封装的表达式检测框架,实现了一个智能元素计数器,用到了:

  • 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),然后对调用时传入的参数,从头到尾按顺序尝试调用这些函数,直到找到第一个能调用成功的函数并返回结果。

详细理解:

1. 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 判断该函数能否接受传入参数调用。
    • 能调用就直接调用并返回结果。
    • 不能调用就递归尝试下一个函数。
2. 辅助推导函数 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 限定符,避免传入引用导致的问题。

3. 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。

总结

  • 这个模板实现了类似重载解析的功能,但更灵活,允许把一组任意lambda组合成一个“多重重载”对象。
  • 通过递归模板和 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

1. Lambda 表达式

最常见的用法,完全支持,因为 lambda 是匿名类,内部实现了 operator()

2. 拥有 operator() 的普通类(functor)

struct TimesTen {
    int operator()(int x) const { return x * 10; }
};
auto f = overload_set(TimesTen{});

什么情况会出现问题?

1. 函数指针不能继承

int foo(int);
auto f = overload_set(foo); //  错误,函数不是类,不能继承

正确方式:用 lambda 包裹函数指针:

auto f = overload_set([](int x) { return foo(x); });

2. 成员函数指针也不能继承

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); });

3. final 类的对象不能被继承

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); });

4. std::function、std::reference_wrapper 也不能继承

这些都是封装器类型,不是普通类,也不能被继承。

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 会变成“噩梦”?

因为你希望它支持“所有”可调用对象:

  • 普通函数
  • 成员函数
  • lambda
  • functor 类
  • std::function
  • final 类
  • 成员变量
  • std::bind 表达式……
    但 C++ 并没有提供统一的语法来继承/使用所有这些可调用对象。而我们写的 overload_set 基于的是“继承 + operator()”,这注定无法适配所有类型。

总结

overload_set 是一个优雅且强大的技巧,但:

  • 适合 lambda 和普通 functor 类
  • 不适合函数指针、成员函数指针、final 类等情况
  • 解决方案是使用 lambda 包裹器,统一为可继承的 functor 类型。

关于“转发函数(forwarding function)与转发类(forwarding class)”的入门问题

什么是 Forwarding Function?

转发函数的目标:
透明地调用一个可调用对象 f 并完美转发参数 args... 给它。

C++11 实现(需要指定返回类型)

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 保持参数的左/右值性质

C++14 实现(支持 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)

C++17 实现(改进:使用 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 的好处:

  • 它不仅可以调用函数或 lambda,还可以处理:
    • 成员函数指针(&Class::func
    • 成员对象指针(&Class::member
    • 函数对象(functor)

问题来了:如何用一个 来实现相同的逻辑?

你希望写出这样的语法:

caller(f)(42, "Hello", "World");

也就是让 caller(f) 返回一个对象,这个对象可调用,就像函数一样自动转发参数。

如何实现 caller 类?

我们需要一个类包装器,它:

  • 存储传入的函数或可调用对象
  • 实现 operator() 转发参数
  • 使用 std::invoke 调用目标函数(支持最多种可调用类型)

示例实现(C++17):

#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
}

这段代码的目的是实现一个 通用的函数转发器(forwarding class),将任意可调用对象(如 lambda、函数指针、仿函数)封装在类中,并通过 operator() 转发调用。

下面逐步解释你这段代码的关键点和工作原理:

核心目标

构造一个 caller 类:

  • 能接收任意 可调用对象(包括 lambda、函数指针、仿函数等)
  • 能正确地 转发参数保持 noexcept 推导
  • 能使用 模板推导规则 自动推导类型
  • 支持 临时对象调用 以及 左值/右值 的处理

代码分析

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)))。
调用转发 operator()
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>
  • 会移除引用和 cv 修饰,确保封装对象内部是值存储

main 函数测试

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 类的优秀示例实现。如果你进一步希望它支持成员函数指针或捕获状态也没问题,它已经支持了。

正在探索 C++ 中一个经典又复杂的问题:“转发(forwarding)和类型推导中与 cv-ref 限定符(const, volatile, &, &&)相关的困扰”。

#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&& 构造时才参与重载。

operator()

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

使用举例解析

1. structure{42}() 的类型推导

using A1 = decltype(structure{42}());

过程:

  • 42int → 推导出 structure
  • 调用 operator()() → 返回 value,类型为 int
  • 所以 A1 = int

2. 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(value) 保留 T=int 的值类别为右值

Tip:如何打印推导出的类型?

你可以用这个工具打印:

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>();

即可输出 intint&& 类型名。需要 #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>>;

表达式 1:structure{42}()

using A1 = decltype(structure{42}());

推导过程

  • 42 是一个 int,值是右值
  • std::decay_tint,所以推导为 structure
  • 调用的是:operator()()不转发
  • 该函数返回 value,是 int 类型的变量
    返回类型: int
    结果: A1 = int

表达式 2:structure{42}(0)

using A2 = decltype(structure{42}(0));

推导过程

  • 同样推导为 structure
  • 调用的是:operator()(int)转发版本
  • 这个版本返回 std::forward(value)
  • Tintvalue 是左值,但通过 std::forward 被转发为 int&&右值引用
    返回类型: int&&
    结果: A2 = int&&

关键点对比表

表达式 调用函数 是否转发 T 类型 返回表达式 返回类型
structure{42}() operator()() int value int
structure{42}(0) operator()(int) int std::forward(value) int&&

理解重点

  • std::forward(value) 的行为取决于 T 本身:
    • 如果 T 是普通类型(如 int),则结果是 T&&,即 右值引用
    • 如果 TT&,则 std::forward(...) 是左值引用
  • 所以,这种行为能模拟完美转发语义,也解释了为什么 A2 = int&&

延伸:如果我们写 structure{x}(0) 呢?

int x = 10;
structure<int&> s{x};
auto&& r = s(0);  // 返回 std::forward(x)

此时 rint&,因为:

  • T = int&
  • std::forward(x) 展开为 static_cast(x),结果是 左值引用

提供的内容涉及模板参数推导、引用折叠、完美转发,以及不同情况下类型的推导结果,特别关注于如何根据传入的参数类型 T 推导出函数返回类型或类模板成员类型。

我帮你详细解析和总结这两大部分(The unqualified typeTemplate argument deduction and forwarding references),并结合表格对理解做辅助说明。

1. The unqualified type — 模板参数推导与返回类型推导(无引用折叠)

代码示意

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 xconst X xconst X& x等)或构造,结果都会退化成不带引用、不带cv修饰的裸类型 type
即使传入引用或const类型,模板参数 X 都会被剥离引用和cv,推导成纯净类型。

2. Template argument deduction and forwarding references — 模板参数推导和转发引用(引用折叠)

代码示意

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),传入任何引用类型时,引用会被剥掉,因为实参绑定时会解引用到对象本身。
  • 转发引用(又叫完美转发)可保持参数引用性质,非常关键用于泛型编程和移动语义。
  • 类模板参数推导(CTAD)和构造函数模板结合时,推导规则稍微复杂,有时会保持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;
}

你这段代码是系统化的 C++ 类型推导实验平台,通过 decltypestd::forward、结构体模板推导指南 (CTAD) 等机制,全方位分析模板参数在不同上下文中的类型推导行为。

下面是对该代码的详细理解与解析:

核心功能:

  1. 打印类型推导结果
    • 使用 __PRETTY_FUNCTION__(GCC/Clang)来获取推导后的类型。
    • print_type 显示函数/模板调用后的类型信息。
  2. 模板函数类型推导实验
    • f0 ~ f6: 测试值传递、const 修饰、引用、转发引用(完美转发)、lambda 等。
    • s7 ~ s9: 三种构造函数模板,分别演示如何通过不同构造签名影响类模板参数推导。
  3. 两组测试宏
    • 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 用不同构造函数风格推导类模板参数

forwarding-specific 测试(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 的返回值是 推导结果 X
  • ss 通过构造函数模板推导出 T,并定义了 using type = T;
    这样你可以对照看出:
    | T 是什么 | 推导出的 X 是什么 |
    | -------------- | --------------- |
    | type | type |
    | type& | type& |
    | type&& | type(因右值实参折叠) |
    | const type& | const type& |
    | const type&& | const type |

总结你能学到什么:

  1. 完美转发与引用折叠的细节
  2. 类模板推导指南如何结合构造函数模板影响类型
  3. decltype(std::declval()) 在实际模板分析中的用途
  4. lambda 参数与函数模板在类型推导上的差异
  5. 如何打印并分析类型推导结果

decltype(std::declval())C++ 类型萃取中最常用的技巧之一,它的作用是:

std::declval() 是 C++ 中的一个工具函数模板,它本身从不运行,只用于在编译期类型推导中“虚构”一个类型为 T 的值,特别是在 decltype 等不需要实际值、只需要“假设值类型”的上下文中使用。

先说作用:

std::declval() 是为了:

构造一个 “类型为 T 的右值”,但不要求 T 有默认构造函数,也不需要实际构造出值

举个例子:你写这句代码:

decltype(std::declval<std::vector<int>>().size())

你想让编译器帮你推导 std::vector::size() 的返回类型。但你又不想构造出 vector 实例(因为也许你根本不能),这时你就用:

std::declval<std::vector<int>>()

这“假装”生成了一个 std::vector 的右值,然后你可以继续调用 .size() 来让 decltype 推导其返回类型。

为啥不能直接 T()

你可能想问:

decltype(T().foo())

也可以推导呀?
但——如果 T抽象类 / 没有默认构造函数 / 是右值引用类型,你写 T() 会出错!而 std::declval() 是纯编译期假值,不构造、不实例化,对任何类型都安全

示例:对比有无 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
}

核心话题拆解:

Forwarding referencesdecltype(std::forward(x)) 的结果区别

在模板中,T&& 被称为转发引用(forwarding reference)。它的行为取决于实参是左值还是右值:

template<typename T>
void foo(T&& x) {
  auto y = std::forward<T>(x);  // 精确保留原始值的引用类型(左值还是右值)
}
传入类型 T 推导结果 T&& 实际类型 std::forward(x) 结果
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,成员 valueint
这样你后续:

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)时:

  • reporter 被移动进来
  • 调用 operator()()&& 版本 → 说明临时值被转发了!
    structure(Option 2)时:
  • reporter 绑定到 U&& → 但如果是左值传入,就变成 U = reporter&,最终变成悬垂引用的可能。

最终结论:

推导方式 解释 推荐?
structure(U&&) -> structure 将 U 解引用并衰减为普通类型,例如 int, std::string 推荐
structure(U&&) -> structure 原样保留左值/右值引用,可能产生引用悬垂等生命周期问题 不推荐

你可以记住一句话:

对于模板类中的构造函数模板推导(CTAD),推导成 T 而不是 T&& 更安全,除非你确定需要引用语义。

> “What is a callable?”

以及 std::invoke 到底可以调用哪些类型?有哪些 callable 的种类?

这是 C++ 高级泛型编程和元编程中的一个核心问题,尤其是在设计 invokeforwarding、函数封装器等时极其重要。

什么是 Callable?

广义的 Callable 指的是可以像函数一样“调用”的东西。

在 C++ 中,std::invoke(f, args...) 可以调用的 f,就称为 Callable。

std::invoke 能调用哪些类型?

1. 普通函数(函数指针)

int func(int x, int y) { return x + y; }
std::invoke(func, 1, 2); // OK

2. Lambda 表达式

auto lam = [](int x, int y) { return x + y; };
std::invoke(lam, 1, 2); // OK

3. 函数对象(实现了 operator() 的类)

struct Functor {
    int operator()(int x, int y) const { return x + y; }
};
std::invoke(Functor{}, 1, 2); // OK

4. 成员函数指针

struct S {
    int method(int x) { return x * 2; }
};
S s;
std::invoke(&S::method, s, 5); // OK,等价于 s.method(5)

5. 成员变量指针

struct S {
    int value = 42;
};
S s;
std::invoke(&S::value, s); // OK,等价于 s.value

“Callable” 的分类总结:

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)

C++ 标准对 Callable 的定义:

从标准来看,如果 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?

以下这些不符合“callable”要求:

  • 原始对象类型(没有 operator() 的普通对象)
  • 静态变量
  • 类型本身(std::invoke(S{}, ...) 会尝试调用 S::operator(),如果没有就错)

小技巧:如何检测是否 Callable?

可以使用 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 种。

函数类型限定符版本表(共 4 类 × 48 种)

序号 普通函数类型 函数左值引用类型 函数右值引用类型 函数指针类型
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

逐类解释每一种限定符的含义和作用。由于数量多,我会分模块来讲,先从普通函数版本开始,然后讲带引用的函数版本,最后讲函数指针。这样你能更清楚每种限定符的语义。

1. 基本版本(4种)

版本 含义说明
R(Args...) 普通函数,接受参数 Args,返回类型 R
R(Args..., ...) 支持可变参数列表(C风格的可变参数,如printf)
R(Args...) noexcept 声明函数不会抛出异常,提高编译器优化可能和异常安全
R(Args..., ...) noexcept 支持可变参数且不会抛异常

2. 限定符版本(11种)

版本 说明
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限定:表示对象可能被外部环境修改,通常用于底层编程

3. 变参版本(11种)

变参版本就是带C风格可变参数的版本,但同样支持上面那11种限定符组合,比如:

版本 说明
R(Args..., ...) & 带可变参数,左值限定函数
R(Args..., ...) const & 带可变参数,const限定,左值限定函数
其余依次类推

4. noexcept版本(11种)

noexcept表明函数不会抛出异常,跟上面限定符组合,如:

版本 说明
R(Args...) & noexcept 左值限定且不抛异常的成员函数
R(Args...) const & noexcept const限定,左值限定且不抛异常的成员函数
其他组合同理

5. 变参 + noexcept版本(11种)

结合变参和noexcept的限定符版本。

6. 函数左值引用版本

比如R(&)(Args...)表示函数类型是一个左值引用(reference to function),其他限定符同上,只不过这里是函数引用。一般用得较少,主要用于函数引用传递。

7. 函数右值引用版本

R(&&)(Args...)是函数的右值引用版本,主要用于模板和完美转发场景。

8. 函数指针版本

R(*)(Args...)是最常见的函数指针类型,函数指针也可以带上这些限定符(constvolatile&&&noexcept、变参等)。

总结:

限定符类别 作用与应用场景
const 保护函数不会修改对象状态,安全访问对象
volatile 表示对象可能被外部修改,多用于底层或硬件相关编程
& 限定函数只能由左值调用,避免临时对象误调用
&& 限定函数只能由右值调用,方便实现移动语义和优化
noexcept 函数保证不抛异常,提升优化和异常安全
... 支持C风格可变参数的函数

这段内容是对C++中各种“可调用类型”(callable types)全面的分类和统计,特别聚焦函数、成员函数指针、成员对象指针,以及带有 operator() 的类(如lambda和函数对象)。

1. 函数类型 (Functions)

  • 这里的函数类型包括:
    • 普通函数:R(Args...) 及其各种cv/ref修饰和noexcept、变参等(共48种组合)
    • 函数左值引用:R(&)(Args...)
    • 函数右值引用:R(&&)(Args...)
    • 函数指针:R(*)(Args...)
    • 带各种限定的函数指针,如 R(* const)(Args...)R(* volatile)(Args...)
    • 函数指针的引用版本,如 R(*&)(Args...)R(*&&)(Args...)

每个变体下面都有48种不同的组合(不同的constvolatile&&&noexcept等修饰符)

2. 成员函数指针 (Member function pointers)

  • 类似函数指针,但指向类的成员函数:
    • 例如:R(C::*)(Args...),带各种constvolatile、引用限定符等组合
    • 也有指向成员函数指针的constvolatile限定,及其引用版本

同样是48种组合,且统计了所有这些类型总数。

3. 成员对象指针 (Member object pointers)

  • 指向类成员变量的指针类型:
    • T C::*,同样带constvolatile、引用限定符等
    • 统计组合数是12种类型(少于函数相关的,因为成员变量没有参数列表)

4. 类类型可调用对象

  • operator()成员函数的类(如Lambda,函数对象):
    • 包括lambda、具有可能继承的operator()成员的类、struct、union
    • 也包括constvolatile、引用限定符版本的operator()成员函数
    • 数量在12到48之间,取决于具体计数方法

5. 统计总结

  • 所有函数相关类型总计约1500种不同的callable类型:
    • 函数类型约720种
    • 成员函数指针类型约576种
    • 成员对象指针类型约144种
    • 其它可调用类类型约48种

总结理解

这份统计展示了C++函数调用机制的复杂性,尤其是考虑了:

  • const / volatile 限定符
  • 引用限定符 (& / &&)
  • noexcept
  • 变参
  • 指针 vs 引用 vs 普通函数
  • 成员函数 vs 成员对象
  • 以及函数调用操作符(operator())的多种形态
    这反映了C++对可调用对象的强大支持和丰富的类型系统。理解这能帮助你写更泛化、更健壮的模板代码,或是理解编译器如何区分各种函数重载和调用。
    如果你想,我可以帮你:
  • 进一步分解某一类(比如函数指针)的48种具体类型
  • 给出它们的具体C++写法示例
  • 或者帮你理解这些限定符组合的实际用法与意义

这段内容演示了如何在 C++ 中调用各种可调用类型(callables),并用 std::invoke 来统一调用它们,同时说明了一些转换和调用规则。下面我帮你总结和解释它的重点:

1. 函数(Function)的调用示例

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) ——函数类型不能被变量持有,只能是指针或引用

2. 类(Class)调用示例

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 &&)的成员函数指针。

3. 转换规则及模板包装示例

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

4. 总结:C++中可调用类型的分类与调用

  • 可调用类型 包括:
    • 各种cv/ref限定的函数、函数引用、函数指针
    • 各种cv/ref限定的成员函数指针
    • 各种cv/ref限定的成员对象指针
    • 含有 operator() 的类、结构体、联合体实例(闭包类型lambda等)
    • 带有合适转换操作符的类型
  • C++ 标准提供的 std::invoke 统一了对这些类型的调用方式。
  • 这种统一调用接口对模板泛型编程非常重要,可以让模板代码适配各种类型的“可调用对象”。

这段话主要讨论了在C++中实现通用的overload_set(重载集合类)时遇到的复杂性和难点,尤其是针对各种**可调用类型(callables)**的处理问题,以及实现时的思路和已知的局限。

1. overload_set 实现的挑战

  • 非lambda的类有 operator() 的情况:应该没问题,可以通过继承或包装实现重载集合。
  • 函数类型本身不能继承:因为函数不能被继承,所以必须用类来包装函数,使它们能被放入重载集合。
  • 成员函数指针成员对象指针(带限定符的)operator() 的类的引用final类(不可继承)带 operator()带 operator() 的union:这些情况都极大增加了实现的复杂度。
  • 最终导致:想要写一个通用的 overload_set 能兼容所有这些情况几乎是“噩梦”。

2. 关键点——转发类(forwarding classes)和可调用类型

  • 要实现 overload_set,需要构造一组类型特征(type traits),来判断和操作各种可调用类型。
  • 例如:
    • 移除所有指针(remove_all_pointers
    • 复制所有限定符(copy_constcopy_volatilecopy_cvcopy_reference
    • 继承辅助(empty_baseis_inheritable 等)
    • 可调用类型分类(is_closureis_functoris_function_objectis_callable
    • 其他辅助工具(index_constantfalse_vtrue_v
  • 这套工具帮助包装分类各种可调用类型,实现转发和调用。

3. P1016R0 和相关实现

  • 作者参考了P1016R0(C++标准提案)中的“额外的类型操作工具”,以及GitHub上的type-utilities库,用于实现这些复杂的类型变换。
  • 有了这些工具,理论上可以实现一个overload_set类,兼容所有可调用类型,但:

4. 已知的限制和“坏消息”

  • final类union是不可继承的,导致无法用继承方式包装它们的operator(),难以实现统一重载集合。
  • 作者留给开发者“练习题”:给出除了final类和union外,其它大多数1500多种可调用类型都能兼容的实现。
  • 也暗示未来C++的**反射(reflection)**机制可能会简化这类问题。

5. 总结

主题 说明
重载集合类实现难点 各种可调用类型的多样性和复杂修饰符导致包装和继承复杂,特别是final类和union
需要的工具 复杂的类型特征(type traits)用来移除指针、复制限定符、判断可调用性、继承判断等
已有工作 P1016R0标准提案和开源type-utilities库提供了辅助工具
目前无法解决的限制 final类和union因不可继承无法包装
未来展望 C++反射特性有望解决这些问题
如果你想,我可以帮你:
  • 具体解释这些类型特征如何设计和使用,
  • 讲解overload_set如何实现和工作原理,
  • 给你看相关代码示例,或者
  • 简要介绍C++反射对这类问题的潜在帮助。

这段内容讲的是如何利用模板技巧在编译期选出第一个满足某种条件的类型,从而解决复杂的类型推断和重载选择问题,避免写冗长的模板代码(boilerplate)。

核心目标

用一行代码拿到:

using type = decltype(
  some_tool(
    some_other_tool<int>(/*...*/),
    some_other_tool<std::vector<int>>(/*...*/),
    some_other_tool<double>(/*...*/)
  )
);

这个 type 会是满足某个条件的第一个有效类型,比如第一个可调用lambda且能访问 .size() 的类型。

核心实现思路

1. invalid_option 结构体作为失败标记

struct invalid_option {};

表示“该类型不满足要求”,类似错误标志。

2. 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

3. 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

额外说明

  • 这种技术利用了SFINAE和模板条件编译,可以在编译期间选出合适类型,避免运行时开销。
  • 有限制:不能直接用lambda表达式在“未求值上下文(unevaluated contexts)”中声明,这会违反C++标准,目前不支持。
  • 该方案可视为“编译时策略选择”,避免写一大堆if constexpr或者模板偏特化。
    如果你想,我可以帮你写一个完整的示例代码,或者进一步解释其中的模板元编程细节。你有兴趣吗?

你列出的这些工具,是用于C++模板元编程的各种辅助工具集合,帮助你处理类型修饰符复制、SFINAE检测、重载序列和集合、类型选择、指针和继承操作,以及可调用对象的分类等问题。简单归纳如下:

工具类别 代表工具 作用说明
修饰符复制 copy_const, copy_volatile, copy_reference 复制某个类型的 constvolatile、引用等修饰符到另一个类型,方便保持类型属性一致。
修饰符克隆 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 查找类型索引等。

总结

这些工具组合使用,可以极大简化复杂模板元编程工作,尤其是:

  • 实现通用转发和重载集合
  • 在编译期选择最合适的类型
  • 灵活复制类型修饰符,保证类型传递正确
  • 检测可调用对象的特性并区分不同类型的可调用形式

你可能感兴趣的:(CppCon 2018 学习:From Metaprogramming Tricks to Elegance)