标准来源:ISO/IEC C++ 标准 §7.5.6.2 [expr.prim.lambda.closure] / §7.5.6.3 [expr.prim.lambda.capture]

一、闭包类型(Closure Type)§7.5.6.2

1.1 什么是闭包类型?

每一个 lambda 表达式都对应一个独一无二的、匿名的、非联合体的类类型,叫做闭包类型(closure type)

[捕获列表](参数列表) -> 返回类型 { 函数体 }
        |
        v
编译器自动生成一个匿名类(闭包类型)
其中包含一个 operator() 成员函数
auto lam = [](int x) { return x * 2; };
// 等价于编译器生成:
// struct __Lambda_unique {
//     int operator()(int x) const { return x * 2; }
// };
// auto lam = __Lambda_unique{};

关键性质:

  • 唯一性:每个 lambda 表达式的闭包类型都不同,即使写法一模一样
  • 匿名性:无法直接写出其类型名,只能用 auto 或模板推导
  • 非联合体:不是 union
  • 不完整性:在 lambda 的 {} 结束之前,闭包类型是不完整类型

1.2 闭包类型的声明位置

闭包类型被声明在包含该 lambda 的最小块作用域、类作用域或命名空间作用域中。

namespace N {           ← lambda 在这里,闭包类型声明于命名空间 N
    void f() {          ← lambda 在这里,闭包类型声明于函数块作用域
        auto lam = []{};
    }
}

1.3 闭包类型的特殊性质


性质 说明
非聚合类型 不能用聚合初始化
结构化类型 当且仅当无 lambda-capture 时,是结构化类型(可做模板非类型参数)
实现可调整 编译器可以改变大小/对齐、是否可平凡复制、是否标准布局,但不能添加右值引用成员

1.4 函数调用运算符(operator())

闭包类型的核心是一个函数调用运算符

非泛型 lambda → public inline operator()(普通成员函数)
泛型 lambda   → public inline operator() 模板(函数调用运算符模板)
修饰符规则
lambda 声明后跟 static    → operator() 是静态成员函数
lambda 声明后跟 mutable   → operator() 不加 const
lambda 有显式对象参数     → operator() 不加 const
其他情况                  → operator() 加 const(默认)
lambda 声明后跟 constexpr → operator() 是 constexpr
lambda 声明后跟 consteval → operator() 是立即函数(immediate function)
noexcept/合约说明符       → 应用于 operator()
#include <iostream>
int main() {
    // 默认:operator() 是 const
    auto f1 = [](int x) { return x; };
    // mutable:operator() 不加 const,可修改捕获的副本
    int n = 0;
    auto f2 = [n]() mutable { return ++n; };  // 捕获副本可修改
    // constexpr:编译期可调用
    auto f3 = [](int x) constexpr { return x * x; };
    static_assert(f3(5) == 25);  // 编译期验证
    // static:operator() 是静态成员函数
    auto f4 = [](int x) static { return x + 1; };
    std::cout << f1(10) << "\n";  // 10
    std::cout << f2() << "\n";    // 1
    std::cout << f3(4) << "\n";   // 16
    std::cout << f4(7) << "\n";   // 8
    return 0;
}

https://godbolt.org/z/ThjnvYqj7

1.5 泛型 Lambda 与函数调用运算符模板

泛型 lambda(参数含 auto 或模板参数列表)生成函数调用运算符模板

#include <iostream>
int main() {
    // auto 参数 → 泛型 lambda
    auto glambda = [](auto a, auto&& b) { return a < b; };
    bool r = glambda(3, 3.14);   // 实例化 operator()<int, double>
    std::cout << r << "\n";      // 1(true)
    // 显式模板参数列表 + requires 约束
    auto f = []<typename T>(T x) requires std::is_integral_v<T> {
        return x * 2;
    };
    std::cout << f(5) << "\n";   // 10
    // f(3.14);  // 错误:double 不满足 is_integral
    return 0;
}

https://godbolt.org/z/xsMaMe5b8

1.6 递归 Lambda(显式对象参数,Explicit Object Parameter)

C++23 允许 lambda 通过显式对象参数 this auto self 实现递归:

#include <iostream>
int main() {
    // this auto self:显式对象参数,self 就是 lambda 自身
    auto fact = [](this auto self, int n) -> int {
        return (n <= 1) ? 1 : n * self(n - 1);
    };
    std::cout << fact(5) << "\n";  // 120
    // 显式对象参数的类型限制(有捕获时):
    // 必须是闭包类型本身、其派生类、或对它们的引用
    int x = 10;
    auto lam = [x](this auto&& self) { return x; };  // OK
    std::cout << lam() << "\n";  // 10
    return 0;
}

https://godbolt.org/z/Tr7jcTaxn
显式对象参数的类型要求(当 lambda 有捕获时):

合法的显式对象参数类型:
  1. 闭包类型本身
  2. 公开且无歧义地派生自闭包类型的类
  3. 上述两种类型的(可 cv 限定的)引用

1.7 转换为函数指针

无捕获、无显式对象参数的 lambda,其闭包类型有一个隐式转换函数,可以转换为对应的函数指针:

无捕获非泛型 lambda → 可转换为 函数指针
无捕获泛型 lambda   → 可转换为 函数指针模板(通过转换函数模板)
#include <iostream>
void call_with_3(int (*fp)(int)) {
    std::cout << fp(3) << "\n";
}
int main() {
    // 无捕获 lambda 可隐式转为函数指针
    auto lam = [](int x) { return x * 2; };
    int (*fp)(int) = lam;      // 隐式转换
    std::cout << fp(5) << "\n"; // 10
    call_with_3(lam);           // 传给函数指针参数:输出 6
    // 泛型 lambda 转换为函数指针(需要能推导类型)
    auto glam = [](auto a) { return a; };
    int (*gfp)(int) = glam;    // OK:推导出 T=int
    std::cout << gfp(7) << "\n"; // 7
    // 转换函数本身是:public constexpr non-virtual non-explicit const noexcept
    return 0;
}

https://godbolt.org/z/nYbx4M5bn
转换歧义示例:

#include <iostream>
void g(int (*)(int))  {}   // 重载 #1
void g(char (*)(char)){}   // 重载 #2
void h(int (*)(int))  {}   // 重载 #3
void h(char (*)(int)) {}   // 重载 #4
int main() {
    auto glambda = [](auto a) { return a; };
    // g(glambda);   // 错误:歧义,#1 和 #2 都可以匹配
    h(glambda);      // OK:选择 #3,int(*)(int) 可以从 glambda 转换
    return 0;
}

https://godbolt.org/z/hqdvWMKEe

1.8 闭包类型的特殊成员函数


特殊成员函数 有捕获时 无捕获时
默认构造函数 删除(无) 默认生成
拷贝构造函数 默认生成 默认生成
移动构造函数 默认生成 默认生成
拷贝赋值运算符 删除 默认生成
移动赋值运算符 删除 默认生成
析构函数 隐式声明 隐式声明

#include <iostream>
int main() {
    int x = 5;
    // 有捕获的 lambda:不能默认构造、不能赋值
    auto lam1 = [x]() { return x; };
    auto lam2 = lam1;        // OK:拷贝构造
    auto lam3 = std::move(lam1); // OK:移动构造
    // lam2 = lam3;          // 错误:拷贝赋值被删除
    // 无捕获的 lambda:可以默认构造(C++20 起)
    auto lam4 = [](int n) { return n * 2; };
    decltype(lam4) lam5{};   // OK:默认构造(C++20)
    std::cout << lam5(3) << "\n"; // 6
    return 0;
}

https://godbolt.org/z/dE5xYnG7r

1.9 __func__ 变量

在 lambda 的函数体内,编译器会隐式定义 __func__ 变量(与普通函数一样),其值是实现定义的(通常是某种 lambda 名称)。

二、捕获机制(Captures)§7.5.6.3

2.1 捕获语法总览

lambda-capture(捕获列表):
  ├── capture-default        默认捕获
  │     ├── &                按引用捕获所有
  │     └── =                按值捕获所有
  ├── capture-list           显式捕获列表
  └── capture-default , capture-list  默认捕获 + 显式捕获
capture(单个捕获):
  ├── simple-capture(简单捕获)
  │     ├── identifier [...]      按值捕获变量(可展开包)
  │     ├── & identifier [...]    按引用捕获变量
  │     ├── this                  捕获 this 指针(按引用)
  │     └── * this                捕获 *this(按值,拷贝整个对象)
  └── init-capture(初始化捕获)
        ├── [...] identifier = expr    按值初始化捕获
        └── & [...] identifier = expr  按引用初始化捕获

2.2 捕获规则与限制

规则1:默认捕获与显式捕获不能冲突
[&, i]   → OK:默认按引用,i 显式按值
[&, &i]  → 错误:默认已经是 &,不能再显式加 &
[=, *this] → OK:默认按值,*this 显式按值拷贝对象
[=, this]  → OK:等价于 [=](this 在 = 默认中被允许)
[i, i]   → 错误:同一变量捕获两次
[this, *this] → 错误:this 出现两次
规则2:捕获只能在特定作用域使用

lambda 只有在以下三种情况下才能有捕获列表:

  1. 其最内层封闭作用域是块作用域(函数体内)
  2. 出现在默认成员初始化器中,最内层作用域是对应类作用域
  3. 出现在合约断言中,最内层作用域是对应合约断言作用域
规则3:捕获名不能与参数名重名
void f() {
    int x = 0;
    // auto g = [x](int x) { return 0; };  // 错误:参数名和捕获名相同
    auto h = [x](int y) { return x + y; }; // OK
}

2.3 按值捕获(Capture by Copy)

触发条件

满足以下任一条件时,实体按值捕获:

  1. 默认捕获是 =,且被捕获的不是 *this
  2. 显式捕获形式不是 this& identifier& ... identifier initializer
机制

按值捕获时,闭包类型中会声明一个匿名非静态数据成员来存储副本:

原类型为 T(对象类型)  → 成员类型为 T
原类型为 T&(对象引用)  → 成员类型为 T(存储引用指向的对象的副本)
原类型为 F&(函数引用)  → 成员类型为 F&(lvalue 引用)

lambda 非 mutable 时,该成员视为 const 限定。

#include <iostream>
int main() {
    int a = 10;
    int b = 20;
    // 按值捕获 a:闭包类型有一个 int 成员存储 a 的副本
    auto lam = [a]() {
        // a 在 lambda 内是 const int(非 mutable)
        // a = 999; // 错误!
        return a;
    };
    a = 999;           // 修改外部 a
    std::cout << lam() << "\n";  // 10(捕获的是旧值)
    // mutable:可以修改捕获的副本(不影响外部)
    auto lam2 = [b]() mutable {
        b += 100;      // 修改的是闭包内的副本
        return b;
    };
    std::cout << lam2() << "\n"; // 120
    std::cout << b << "\n";      // 20(外部 b 未变)
    return 0;
}

https://godbolt.org/z/En3n1dTjE

2.4 按引用捕获(Capture by Reference)

按引用捕获时,闭包类型中可能(由实现决定)声明一个引用成员,但也可能不声明(直接使用被捕获变量的地址)。
关键点:按引用捕获捕获的是引用绑定的对象本身,不是引用变量。

#include <iostream>
int main() {
    int x = 10;
    // 按引用捕获 x
    auto lam = [&x]() {
        x += 5;   // 修改的是外部的 x
    };
    lam();
    std::cout << x << "\n";  // 15(外部 x 被修改)
    // 危险:引用捕获的悬空引用
    auto make_lam = [](int& r) {
        return [&r]() { return r; };  // 捕获的是 r 绑定的对象
    };
    // auto bad = make_lam(local_var);
    // bad() 在 local_var 销毁后调用 → 未定义行为!
    return 0;
}

https://godbolt.org/z/Wzb6rd5sM
位字段和匿名联合体成员不能按引用捕获。

2.5 初始化捕获(Init-Capture)

C++14 引入,允许在捕获时创建一个新变量并初始化:

#include <iostream>
#include <memory>
#include <utility>
int main() {
    // 按值初始化捕获:捕获一个移动后的 unique_ptr
    auto ptr = std::make_unique<int>(42);
    auto lam = [p = std::move(ptr)]() {
        return *p;
    };
    std::cout << lam() << "\n";  // 42
    // ptr 已经被移动,不能再用
    // 按引用初始化捕获
    int x = 10;
    auto lam2 = [&r = x]() {
        r += 1;  // r 是 x 的引用
    };
    lam2();
    std::cout << x << "\n";  // 11
    // 计数器:init-capture + mutable
    auto counter = [i = 0]() mutable -> int {
        return i++;
    };
    std::cout << counter() << "\n"; // 0
    std::cout << counter() << "\n"; // 1
    std::cout << counter() << "\n"; // 2
    return 0;
}

https://godbolt.org/z/96cdThj79
初始化捕获的本质:

[x = expr] 等价于 编译器在 lambda 作用域内声明:
    auto x = expr;
    然后按值捕获这个 x
[&x = expr] 等价于:
    auto& x = expr;
    然后按引用捕获这个 x

2.6 嵌套 Lambda 的捕获传递

当 lambda m2 嵌套在 lambda m1 内,m2 捕获 m1 已捕获的实体时:

m1 按值捕获了 a → m2 捕获的是 m1 闭包类型的对应成员
                    若 m1 非 mutable,则该成员是 const 的
m1 按引用捕获了 b → m2 捕获的是 b 原来绑定的实体(同一个对象)
#include <iostream>
int main() {
    int a = 1, b = 1, c = 1;
    // m1: a 按值捕获,b 和 c 按引用捕获,mutable
    auto m1 = [a, &b, &c]() mutable {
        // m2: a 按值捕获(m1 的副本),b 按值捕获,c 按引用
        auto m2 = [a, b, &c]() mutable {
            std::cout << a << b << c;  // 输出 m2 调用时的值
            a = 4; b = 4; c = 4;       // 修改:a/b 是 m2 自己的副本,c 影响外部
        };
        a = 3; b = 3; c = 3;           // 修改 m1 的副本 a,外部 b 和 c
        m2();                           // 输出: a=1(m2捕获时), b=3, c=3 → "133"
    };
    a = 2; b = 2; c = 2;
    m1();                               // 触发上面 m2 输出 "133"
    std::cout << a << b << c;          // a=2(未变), b=4(c改的), c=4 → "244"
    // 标准说输出是 123234,对应不同时刻的值(见标准示例)
    return 0;
}

https://godbolt.org/z/a7PMGPab3
捕获传递路径图:

外部变量 a(=1) ──[按值]──> m1 副本 a(=1) ──[按值]──> m2 副本 a(=1)
                                                          修改不影响任何人
外部变量 b(=1) ──[按引用]──> m1 看到 b ──[按值]──> m2 副本 b(=m1修改后的值)
                              m1 修改 b 影响外部
外部变量 c(=1) ──[按引用]──> m1 看到 c ──[按引用]──> m2 直接看到 c
                              任何修改都影响外部

2.7 隐式捕获与 ODR 使用(odr-use)

当 lambda 使用了 [=][&] 默认捕获,编译器会自动捕获被 odr-使用的局部实体
"潜在引用局部实体"的三种情形

  1. 命名局部实体的 id-expression
  2. this 表达式
  3. lambda 的 simple-capture 列出的实体
#include <iostream>
void f(int, const int (&)[2] = {}) { std::cout << "f(int, [2])\n"; }
void f(const int&, const int (&)[1]) { std::cout << "f(int&, [1])\n"; }
int main() {
    const int x = 17;
    // x 是编译期常量,不需要 odr-use,g 不捕获 x
    auto g = [](auto a) { f(x); };
    // [=]:x 被捕获(即使可能被优化掉)
    auto g1 = [=](auto a) { f(x); };
    // 捕获是**语法上确定的**,即使在 if constexpr 的 discarded 分支里也会捕获
    int n = 0;
    auto g2 = [=](auto a) {
        if constexpr (sizeof(a) > 100) {
            (void)n;  // 即使这个分支永远不执行,n 也被捕获了!
        }
    };
    g(0);   // f(int, [2])
    g1(0);  // f(int, [2])
    return 0;
}

https://godbolt.org/z/9d4754T1E
重要:捕获集合是语法上静态确定的,不受运行时条件影响,也不受 if constexpr 的 discarded 分支影响。

2.8 *this 捕获(按值拷贝整个对象)

#include <iostream>
struct S {
    double value = 0.007;
    // 返回一个捕获了 *this 副本的 lambda
    auto f() {
        return [this] {             // 按引用捕获(通过 this 指针)
            return [*this] {        // 按值捕获(拷贝整个 S 对象)
                return value;
            };
        }();
    }
    // 错误示例:外层没捕获,内层无法捕获 *this
    // auto g() {
    //     return [] {
    //         return [*this] { };  // 错误!外层 lambda 没有捕获 *this
    //     }();
    // }
};
int main() {
    S s;
    auto lam = s.f();
    s.value = 999.0;             // 修改原对象
    std::cout << lam() << "\n"; // 0.007(访问的是拷贝,不受影响)
    return 0;
}

https://godbolt.org/z/8v6z1oeov

2.9 包展开捕获(Pack Expansion Capture)

#include <iostream>
#include <utility>
void g(int a, char b, double c) {
    std::cout << a << b << c << "\n";
}
template<class... Args>
void f(Args... args) {
    // args... 按值捕获所有包元素
    auto lm = [&, args...] { return g(args...); };
    lm();
    // 初始化捕获 + 包展开(C++20):移动捕获所有参数
    auto lm2 = [...xs = std::move(args)] { return g(xs...); };
    lm2();
}
int main() {
    f(1, 'a', 3.14);
    return 0;
}

https://godbolt.org/z/hzeYxvrM9

三、完整综合示例

编译:g++ -std=c++20 -Wall lambda_demo.cpp(显式对象参数 this auto 需 GCC14+ 或 Clang16+,此处用仿函数等价替代)

#include <iostream>
#include <memory>
#include <utility>
// 递归仿函数:等价于 [](this auto self, int n)->int{...} 的语义
struct Fib {
    int operator()(int n) const {
        return n <= 1 ? n : (*this)(n-1) + (*this)(n-2);
    }
};
int main() {
    // ===== 1. 无捕获 lambda 隐式转换为函数指针 =====
    auto square = [](int x) { return x * x; };
    int (*fp)(int) = square;            // 隐式转换:无捕获才可以
    std::cout << "[函数指针] " << fp(5) << "\n";   // 25
    // ===== 2. 按值捕获 vs 按引用捕获 =====
    int val = 10;
    auto byVal = [val]()  { return val; };  // 捕获时拷贝副本
    auto byRef = [&val]() { return val; };  // 捕获引用,指向原变量
    val = 99;
    std::cout << "[按值] " << byVal() << "\n";      // 10(旧值)
    std::cout << "[按引用] " << byRef() << "\n";    // 99(新值)
    // ===== 3. mutable:修改按值捕获的副本,不影响外部 =====
    int counter = 0;
    auto inc = [counter]() mutable { return ++counter; };
    std::cout << "[mutable] " << inc() << " " << inc() << "\n"; // 1 2
    std::cout << "[外部counter] " << counter << "\n";           // 0(未变)
    // ===== 4. 初始化捕获 + 移动语义 =====
    auto uptr = std::make_unique<int>(42);
    auto lam  = [p = std::move(uptr)]() { return *p; }; // 移动进闭包
    std::cout << "[init-capture] " << lam() << "\n";    // 42
    // ===== 5. 泛型 lambda =====
    auto identity = [](auto x) { return x; };
    std::cout << "[泛型] " << identity(100) << "\n";    // 100
    // ===== 6. constexpr lambda:编译期可调用 =====
    auto sq = [](int n) constexpr { return n * n; };
    static_assert(sq(7) == 49);  // 编译期验证
    std::cout << "[constexpr] " << sq(7) << "\n";       // 49
    // ===== 7. 递归(仿函数等价演示) =====
    Fib fib;
    std::cout << "[递归] fib(10)=" << fib(10) << "\n"; // 55
    // ===== 8. *this 按值捕获:拷贝整个对象 =====
    struct Box {
        int data = 100;
        auto getGetter() const {
            return [*this]() { return data; }; // 拷贝整个 Box 进闭包
        }
    };
    Box box;
    auto getter = box.getGetter();
    box.data = 999;                           // 修改原对象
    std::cout << "[*this] " << getter() << "\n"; // 100(访问副本)
    // ===== 9. 无捕获闭包:C++20 起可默认构造 =====
    auto lam2 = [](int x) { return x + 1; };
    decltype(lam2) lam3{};   // 默认构造(C++20)
    std::cout << "[默认构造] " << lam3(41) << "\n"; // 42
    // ===== 10. 嵌套 lambda 捕获传递 =====
    int a = 1, b = 1, c = 1;
    auto m1 = [a, &b, &c]() mutable {
        // a:m1 的副本(=1),m2 按值再拷贝一份
        // b:m1 通过引用看到外部 b,m2 按值捕获 m1 修改后的 b
        // c:两层都按引用,任何修改直达外部
        auto m2 = [a, b, &c]() mutable {
            std::cout << "[嵌套] m2: a=" << a
                      << " b=" << b << " c=" << c << "\n";
            a = 4; b = 4; c = 4; // a/b 修改的是 m2 自己的副本,c 影响外部
        };
        a = 3; b = 3; c = 3; // 修改 m1 的 a 副本、外部 b、外部 c
        m2();
    };
    a = 2; b = 2; c = 2;    // 外部修改
    m1();
    // m2 调用时:a=1(m1副本创建时),b=2(m1捕获时外部b的引用,m1改前),c=3(m1刚改的)
    std::cout << "[嵌套] 外部: a=" << a
              << " b=" << b << " c=" << c << "\n";
    return 0;
}

https://godbolt.org/z/1aMGsh3K1
实测输出:

[函数指针] 25
[按值] 10
[按引用] 99
[mutable] 1 2
[外部counter] 0
[init-capture] 42
[泛型] 100
[constexpr] 49
[递归] fib(10)=55
[*this] 100
[默认构造] 42
[嵌套] m2: a=1 b=2 c=3
[嵌套] 外部: a=2 b=3 c=4

四、整体结构总览

lambda 表达式

闭包类型
唯一匿名非联合体类

operator 成员函数

是否泛型?

普通 inline operator

operator 模板

修饰符

static → 静态成员函数

mutable → 不加 const

默认 → const 成员函数

constexpr/consteval

特殊成员函数

有捕获?

删除默认构造和赋值
保留拷贝和移动构造

全部默认生成

无捕获且无显式对象参数?

可转换为函数指针

非泛型 → 函数指针

泛型 → 函数指针模板

捕获列表

捕获方式

按值捕获
闭包内存副本
非mutable时为const

按引用捕获
指向原对象
注意悬空引用

init-capture
创建新变量并初始化
支持移动语义

this/星号this
this指针or对象副本

包展开捕获
args... 或 ...xs=move-args

五、关键对比:按值捕获 vs 按引用捕获

特性              按值捕获 [x]          按引用捕获 [&x]
──────────────────────────────────────────────────────
存储方式          闭包内存一份副本      存储引用(或指针)
捕获时机          lambda 创建时         访问时(运行时)
修改外部变量      不能(除非 mutable)  可以直接修改
生命期问题        安全(有副本)        危险(可能悬空)
内存开销          增加(存副本)        小(只存引用)
const 性          非mutable时为const    不加 const

六、记忆口诀

  1. 闭包类型唯一匿名,每个 lambda 类型都不同
  2. 无捕获才能转函数指针,有捕获就不行
  3. 有捕获删除默认构造,无捕获 C++20 可以默认构造
  4. 按值捕获存副本,lambda 创建时拷贝,非 mutable 时是 const
  5. 按引用捕获存引用,捕获的是对象本身,注意悬空
  6. init-capture 创建新变量,可以 [p = std::move(ptr)] 捕获移动
  7. *this 拷贝整个对象this 只拷贝指针
  8. 捕获集合语法静态确定,不受 if constexpr 或运行时条件影响
  9. 捕获名不能与参数名相同,也不能重复捕获同一实体
  10. 嵌套 lambda 捕获传递:外层按值 → 内层得到 const 副本;外层按引用 → 内层直接共享原对象

C++ 折叠表达式、requires 表达式与拼接表达式详解

标准来源:ISO/IEC C++ §7.5.7 [expr.prim.fold] / §7.5.8 [expr.prim.req] / §7.5.9 [expr.prim.splice]

一、折叠表达式(Fold Expressions)§7.5.7

1.1 什么是折叠表达式?

折叠表达式用于对**参数包(parameter pack)**进行累积运算,是 C++17 引入的特性。
直觉理解:就是把一个列表里所有元素用同一个运算符"折叠"(累积)成一个值。

参数包 args = {1, 2, 3, 4}
(args + ...) → 1 + 2 + 3 + 4 = 10  (一元右折叠)
(... + args) → ((1 + 2) + 3) + 4 = 10 (一元左折叠)
(0 + ... + args) → 0 + 1 + 2 + 3 + 4 = 10 (二元左折叠)

1.2 四种折叠形式

语法
( 包 op ... )           → 一元右折叠(unary right fold)
( ... op 包 )           → 一元左折叠(unary left fold)
( 包 op ... op 初值 )   → 二元左折叠(binary left fold,初值在右)
( 初值 op ... op 包 )   → 二元右折叠(binary right fold,初值在左)

注意:二元折叠中 op1op2 必须是同一个运算符,且只有一侧含有未展开的包。

展开规则

设参数包 E 1 , E 2 , … , E n E_1, E_2, \ldots, E_n E1,E2,,En,运算符为 ⊕ \oplus ,初值为 I I I
一元右折叠  ( E ⊕ … ) ⇒ E 1 ⊕ ( E 2 ⊕ ( … ⊕ E n ) ) \text{一元右折叠 } (E \oplus \ldots) \Rightarrow E_1 \oplus (E_2 \oplus (\ldots \oplus E_n)) 一元右折叠 (E)E1(E2(En))
一元左折叠  ( … ⊕ E ) ⇒ ( ( E 1 ⊕ E 2 ) ⊕ … ) ⊕ E n \text{一元左折叠 } (\ldots \oplus E) \Rightarrow ((E_1 \oplus E_2) \oplus \ldots) \oplus E_n 一元左折叠 (E)((E1E2))En
二元右折叠  ( E ⊕ … ⊕ I ) ⇒ E 1 ⊕ ( E 2 ⊕ ( … ⊕ ( E n ⊕ I ) ) ) \text{二元右折叠 } (E \oplus \ldots \oplus I) \Rightarrow E_1 \oplus (E_2 \oplus (\ldots \oplus (E_n \oplus I))) 二元右折叠 (EI)E1(E2((EnI)))
二元左折叠  ( I ⊕ … ⊕ E ) ⇒ ( ( I ⊕ E 1 ) ⊕ E 2 ) ⊕ … ) ⊕ E n \text{二元左折叠 } (I \oplus \ldots \oplus E) \Rightarrow ((I \oplus E_1) \oplus E_2) \oplus \ldots) \oplus E_n 二元左折叠 (IE)((IE1)E2))En

1.3 支持的运算符

算术:  +  -  *  /  %
位运算:^  &  |  <<  >>
赋值:  +=  -=  *=  /=  %=  ^=  &=  |=  <<=  >>=  =
比较:  ==  !=  <  >  <=  >=
逻辑:  &&  ||
其他:  ,  .*  ->*

1.4 空包时的默认值

一元折叠在空包时的行为:

运算符 空包时的值
&& true
|| false
, void()
其他 程序非良构(ill-formed)

使用二元折叠可以为空包提供自定义默认值,避免非良构。

1.5 完整代码示例

#include <iostream>
#include <string>
// ===== 一元右折叠:求和 =====
template<typename... Args>
auto sum_right(Args... args) {
    // (args + ...) 展开为 arg1 + (arg2 + (arg3 + ...))
    return (args + ...);
}
// ===== 一元左折叠:求和(整数加法结果相同,但字符串拼接顺序不同)=====
template<typename... Args>
auto sum_left(Args... args) {
    // (... + args) 展开为 ((arg1 + arg2) + arg3) + ...
    return (... + args);
}
// ===== 二元左折叠:带初值的求和(支持空包)=====
template<typename... Args>
auto sum_with_init(Args... args) {
    // (0 + ... + args) → 初值在左侧,二元左折叠
    return (0 + ... + args);
}
// ===== 逻辑 AND 折叠:判断所有参数都为 true =====
template<typename... Args>
bool all_true(Args... args) {
    // (... && args):空包时结果为 true(标准规定)
    return (... && args);
}
// ===== 逻辑 OR 折叠:判断至少一个为 true =====
template<typename... Args>
bool any_true(Args... args) {
    // (args || ...):空包时结果为 false
    return (args || ...);
}
// ===== 逗号折叠:对每个元素调用函数 =====
template<typename... Args>
void print_all(Args... args) {
    // (表达式, ...) 依次对每个元素求值,结果丢弃
    ((std::cout << args << " "), ...);
    std::cout << "\n";
}
// ===== 二元右折叠:字符串拼接(初值在右)=====
template<typename... Args>
std::string concat_right(Args... args) {
    // (args + ... + std::string{}) 展开为
    // args1 + (args2 + (... + (argsN + "")))
    return (args + ... + std::string{});
}
// ===== 类型约束:判断所有参数都是同一类型(编译期检查)=====
template<typename T, typename... Args>
bool all_same_as_first(T first, Args... rest) {
    // 展开为 (std::is_same_v<T,Args> && ...)
    return (std::is_same_v<T, Args> && ...);
}
int main() {
    // 一元右折叠
    std::cout << "[右折叠] 1+2+3+4 = " << sum_right(1, 2, 3, 4) << "\n"; // 10
    // 一元左折叠(整数加法顺序不影响结果)
    std::cout << "[左折叠] 1+2+3+4 = " << sum_left(1, 2, 3, 4) << "\n";  // 10
    // 二元左折叠(带初值 0,支持空包)
    std::cout << "[带初值] sum() = " << sum_with_init() << "\n";           // 0
    std::cout << "[带初值] sum(1,2,3) = " << sum_with_init(1, 2, 3) << "\n"; // 6
    // 逻辑折叠
    std::cout << "[AND] all_true(1,1,1) = " << all_true(true, true, true) << "\n"; // 1
    std::cout << "[AND] all_true(1,0,1) = " << all_true(true, false, true) << "\n";// 0
    std::cout << "[AND] all_true() = "      << all_true() << "\n";                 // 1(空包)
    std::cout << "[OR]  any_true(0,0,1) = " << any_true(false, false, true) << "\n";// 1
    std::cout << "[OR]  any_true() = "      << any_true() << "\n";                  // 0(空包)
    // 逗号折叠:打印所有元素
    std::cout << "[逗号折叠] ";
    print_all(1, 2.5, "hello", 'A');   // 1 2.5 hello A
    // 字符串拼接(右折叠)
    std::cout << "[字符串] " << concat_right(std::string("Hello"), std::string(", "), std::string("World")) << "\n";
    // 类型检查
    std::cout << "[类型] all int? " << all_same_as_first(1, 2, 3) << "\n";     // 1
    std::cout << "[类型] mixed?   " << all_same_as_first(1, 2.0, 3) << "\n";   // 0
    return 0;
}

https://godbolt.org/z/srqjzcbz3

1.6 折叠展开示意(ASCII)

一元右折叠 (args + ...),args = {a, b, c, d}:
  a + (b + (c + d))
      ^───右结合────^
一元左折叠 (... + args),args = {a, b, c, d}:
  ((a + b) + c) + d
  ^───左结合────^
二元右折叠 (args + ... + 0),args = {a, b, c}:
  a + (b + (c + 0))
                ^初值在最右
二元左折叠 (0 + ... + args),args = {a, b, c}:
  ((0 + a) + b) + c
  ^初值在最左

二、Requires 表达式 §7.5.8

2.1 什么是 requires 表达式?

requires 表达式是 C++20 引入的约束检查机制,用来在编译期检查某些操作是否合法,结果是一个 bool 类型的纯右值(prvalue)
核心思想:

requires (...) { 检查内容; }
→ 如果检查全部通过,结果为 true
→ 如果任意一项失败(类型替换失败/约束不满足),结果为 false
→ 不会导致编译错误(SFINAE 友好)
// 注意:requires requires 是合法的!
// 第一个 requires 引入约束子句,第二个引入 requires 表达式
template<typename T>
requires requires(T x) { x + x; }  // 检查 T 支持 +
T add(T a, T b) { return a + b; }

2.2 四种需求(Requirements)

requires-expression 包含四种需求:
  ├── simple-requirement      简单需求:检查表达式是否合法
  ├── type-requirement        类型需求:检查类型是否存在
  ├── compound-requirement    复合需求:检查表达式 + noexcept + 返回类型
  └── nested-requirement      嵌套需求:检查额外的约束表达式

2.3 简单需求(Simple Requirement)§7.5.8.2

语法expression ;
只检查表达式是否合法,不对表达式求值(不求值语境)。

#include <iostream>
template<typename T>
concept Addable = requires(T a, T b) {
    a + b;       // 简单需求:检查 a+b 是否是合法表达式
    a - b;       // 简单需求:检查 a-b 是否是合法表达式
};
template<typename T>
concept HasPrint = requires(T x) {
    x.print();   // 简单需求:检查 x.print() 是否合法
};
struct Vec2 {
    float x, y;
    Vec2 operator+(const Vec2& o) const { return {x+o.x, y+o.y}; }
    Vec2 operator-(const Vec2& o) const { return {x-o.x, y-o.y}; }
    void print() const { std::cout << "(" << x << "," << y << ")"; }
};
int main() {
    std::cout << std::boolalpha;
    std::cout << "[Addable<int>]   " << Addable<int>   << "\n"; // true
    std::cout << "[Addable<Vec2>]  " << Addable<Vec2>  << "\n"; // true
    std::cout << "[HasPrint<Vec2>] " << HasPrint<Vec2> << "\n"; // true
    std::cout << "[HasPrint<int>]  " << HasPrint<int>  << "\n"; // false(int 没有 print())
    return 0;
}

https://godbolt.org/z/YaxzhT6aP
注意:以 requires 开头的需求永远不会被解析为简单需求,而是嵌套需求。

2.4 类型需求(Type Requirement)§7.5.8.3

语法typename [嵌套名]::type_name ;
检查类型是否存在(嵌套类型、模板特化、别名模板实例化是否合法)。

#include <iostream>
#include <vector>
// 检查 T 是否有 value_type 嵌套类型
template<typename T>
concept HasValueType = requires {
    typename T::value_type;  // 类型需求
};
// 检查 T 是否有 iterator 和 const_iterator
template<typename T>
concept Container = requires {
    typename T::iterator;
    typename T::const_iterator;
    typename T::value_type;
};
int main() {
    std::cout << std::boolalpha;
    std::cout << "[HasValueType<vector<int>>] "
              << HasValueType<std::vector<int>> << "\n"; // true
    std::cout << "[HasValueType<int>]         "
              << HasValueType<int>              << "\n"; // false
    std::cout << "[Container<vector<int>>]    "
              << Container<std::vector<int>>    << "\n"; // true
    return 0;
}

https://godbolt.org/z/qqajjaWMW
注意:类型需求中类模板特化不要求类型完整(incomplete type 也算合法)。

2.5 复合需求(Compound Requirement)§7.5.8.4

语法{ expression } [noexcept] [-> type-constraint] ;
复合需求可以同时检查三件事:

  1. 表达式合法性(必须)
  2. 是否不抛异常noexcept,可选)
  3. 返回类型满足约束-> type-constraint,可选)
{ E } noexcept -> C;
等价于:
  E;                        // 简单需求:E 合法
  requires noexcept(E);     // E 不能是潜在抛出的
  requires C<decltype((E))>; // decltype((E)) 满足约束 C
#include <iostream>
#include <concepts>
// C1:检查 x++ 是否合法(等价于简单需求)
template<typename T>
concept C1 = requires(T x) {
    { x++ };   // 花括号使之成为复合需求,但这里没有 noexcept 和返回类型约束
};
// C2:检查 *x 合法,且结果可转换为 int
template<typename T>
concept C2 = requires(T x) {
    { *x } -> std::convertible_to<int>;
    // 等价于:*x; requires std::convertible_to<decltype((*x)), int>;
};
// C3:检查某函数调用合法且不抛异常
template<typename T>
concept C3 = requires(T x) {
    { x.size() } noexcept -> std::convertible_to<std::size_t>;
    // 1. x.size() 必须合法
    // 2. x.size() 必须标记为 noexcept
    // 3. 返回值必须可转换为 size_t
};
// 综合 concept:迭代器要求
template<typename T>
concept SimpleIterator = requires(T it) {
    { *it };               // 可解引用
    { ++it } -> std::same_as<T&>;  // 前置++,返回 T&
    { it++ };              // 后置++
};
// 测试用的简单指针包装
struct IntPtr {
    int* p;
    int& operator*()  const { return *p; }
    IntPtr& operator++()    { ++p; return *this; }
    IntPtr  operator++(int) { IntPtr t=*this; ++p; return t; }
    std::size_t size() const noexcept { return 1; }
};
int main() {
    std::cout << std::boolalpha;
    std::cout << "[C1<int>]          " << C1<int>     << "\n"; // true(int++ 合法)
    std::cout << "[C1<void*>]        " << C1<void*>   << "\n"; // false(void*++ 不合法)
    std::cout << "[C2<int*>]         " << C2<int*>    << "\n"; // true(*p 是 int,可转 int)
    std::cout << "[C3<IntPtr>]       " << C3<IntPtr>  << "\n"; // true
    std::cout << "[SimpleIter<int*>] " << SimpleIterator<int*> << "\n"; // true
    return 0;
}

https://godbolt.org/z/qTWdPEcWz
三个检查的顺序(lexical order,按词法顺序逐步验证,遇到失败立即停止):

{ E } noexcept -> C

替换模板参数到 E

E 合法?

结果 = false

有 noexcept?

E 不抛异常?

有返回类型约束?

替换参数到约束

C满足 decltype E?

结果 = true

2.6 嵌套需求(Nested Requirement)§7.5.8.5

语法requires constraint-expression ;
用来在 requires 表达式内部加入额外的约束条件,可以利用局部参数进行更复杂的检查。

#include <iostream>
#include <concepts>
// 嵌套需求:检查 +t 的类型大小为 1
template<typename U>
concept Size1 = sizeof(U) == 1;
template<typename T>
concept D = requires(T t) {
    requires Size1<decltype(+t)>;
    // 嵌套需求:decltype(+t) 必须满足 Size1 约束
    // 即 sizeof(decltype(+t)) == 1
};
// 更实际的例子:检查容器支持 push_back 且 value_type 是整数
template<typename T>
concept IntContainer = requires(T c, typename T::value_type v) {
    typename T::value_type;              // 类型需求
    { c.push_back(v) };                 // 复合需求
    requires std::integral<typename T::value_type>; // 嵌套需求
};
int main() {
    std::cout << std::boolalpha;
    // char 加正号后仍是 char(大小为1)或 int(大小>1)
    // 实际 +char → int(整型提升),sizeof(int)=4,不满足 Size1
    std::cout << "[D<char>]   " << D<char>   << "\n"; // false(+char→int,size=4)
    std::cout << "[D<signed char>] " << D<signed char> << "\n"; // false(同上)
    // IntContainer 测试
    // std::vector<int>:有 value_type=int(满足 integral),有 push_back
    // 这里用手动判断演示(避免引入 vector)
    std::cout << "[D概念演示完成]\n";
    return 0;
}

https://godbolt.org/z/9jvYjjzr7

2.7 requires 表达式的求值行为

关键规则:
1. 按词法顺序逐步检查,遇到失败立即停止(短路)
2. 模板参数替换失败 → 结果为 false,不导致编译错误
3. 如果"替换永远失败"(如 new decltype((void)T{})),程序非良构(不需要诊断)
4. 不在模板实体声明中 → 检查失败直接是编译错误
requires 表达式的两种典型用法:
1. 定义 concept(约束名字化):
   template<typename T>
   concept MyConcept = requires(T x) { x.foo(); };
2. 内联约束(直接写在 requires 子句里):
   template<typename T>
   requires requires(T x) { x.foo(); }  // 注意双 requires
   void func(T x) { x.foo(); }

三、拼接表达式(Splice Expressions)§7.5.9

3.1 什么是拼接表达式?(C++26 反射特性)

拼接表达式是 C++26 **静态反射(static reflection)机制的一部分,允许把一个反射(reflection)**值"拼接"(splice)回表达式、类型或模板。

^^T          → 获取 T 的反射值(reflect)
[:r:]        → 把反射值 r 拼接回原来的实体(splice)
类比理解:
  反射就像把代码"装进变量",拼接就是把"变量里的代码"取出来用。
  constexpr auto r = ^^int;      // 反射:把 int 这个类型装进 r
  typename [:r:] x = 42;         // 拼接:把 r 里的类型取出来,等价于 int x = 42

3.2 三种拼接表达式形式

splice-expression 的三种形式:
  ├── [:splice-specifier:]
  │     最基本形式,拼接一个反射值
  ├── template [:splice-specifier:]
  │     拼接一个函数模板(触发重载决议)
  └── template [:splice-specialization-specifier:]
        拼接一个模板特化(函数模板或变量模板)

3.3 拼接不是 ::typename 后面跟的东西

// 以下是"类型拼接"或"作用域说明符",不是拼接表达式:
constexpr int c = [:^^S:]::a;           // [:^^S:] 是作用域说明符,不是表达式
constexpr int d = template [:^^TCls:]<int>::b; // 同上
// 以下是拼接表达式:
template<auto V>
constexpr int e = [:V:];   // [:V:] 是拼接表达式(变量模板里)

3.4 拼接表达式的值类别与类型

根据被拼接的实体 S S S 的种类,结果不同:

S S S 的种类 结果值类别 类型
函数 lvalue(重载决议后) 函数类型
对象或非静态数据成员 lvalue S S S 的类型
直接基类关系 ( D , B ) (D, B) (D,B) lvalue B B B
变量或结构化绑定 lvalue S S S 的类型
值或枚举器 prvalue S S S 的类型
构造函数/析构函数/匿名位字段/局部实体(特定条件) 非良构

3.5 概念性代码示意(C++26 反射,GCC/Clang 尚未完全支持)

#include <meta>
import std;
struct S {
    static constexpr int a = 1;
    int b = 2;
    int c = 3;
};
constexpr int val = [:^^S::a:];
int S::* mptr = &[:^^S::b:];
template <typename T>
consteval auto members_of_T() {
    return std::define_static_array(          // ✅ std:: 而非 std::meta::
        std::meta::nonstatic_data_members_of(
            ^^T,
            std::meta::access_context::unchecked()
        )
    );
}
template<typename T>
void print_members() {
    template for (constexpr auto mem : members_of_T<T>()) {
        std::cout << std::meta::identifier_of(mem) << "\n";
    }
}
int main() {
    static_assert(val == 1);
    S s{};
    s.*mptr = 42;
    std::cout << "b = " << s.b << "\n";
    print_members<S>();  // 输出: b \n c
}

https://www.godbolt.org/z/oqhaWh5Gn

四、三者总体关系

基本表达式 Primary Expression

折叠表达式
fold-expression

requires 表达式
requires-expression

拼接表达式
splice-expression

一元左折叠
...op pack

一元右折叠
pack op ...

二元左折叠
init op ... op pack

二元右折叠
pack op ... op init

简单需求
expression;

类型需求
typename T::type;

复合需求
E noexcept -> C;

嵌套需求
requires expr;

splice-specifier
拼接单个实体

template splice-specifier
拼接函数模板

template splice-specialization
拼接模板特化

pack expansion
参数包展开

prvalue bool
编译期约束检查

C++26 反射
静态元编程

五、折叠表达式 vs requires 表达式对比

折叠表达式(fold expression):
  用途:对参数包做累积运算
  结果:运算累积的值(类型取决于运算)
  时机:运行时(但结果可以是 constexpr)
  引入版本:C++17
requires 表达式:
  用途:检查模板参数是否满足某些要求
  结果:bool 类型的纯右值(编译期)
  时机:编译期
  引入版本:C++20
拼接表达式(splice expression):
  用途:把反射值"还原"为代码实体
  结果:取决于被拼接的实体
  时机:编译期(静态反射)
  引入版本:C++26(尚未正式发布)

六、记忆口诀

折叠表达式

  1. 圆括号包裹,省略号是关键(pack op ...)(... op pack)
  2. 右折叠从右边开始,左折叠从左边开始
  3. 二元折叠提供初值,解决空包问题
  4. && 空包为 true,|| 空包为 false,其余空包非良构
    requires 表达式
  5. 简单需求:只检查表达式合法性,一行一个
  6. 类型需求typename 开头,检查类型存在
  7. 复合需求{E} 花括号,可选 noexcept-> 类型约束
  8. 嵌套需求requires 开头,检查更复杂的约束
  9. 失败不报错,只返回 false(在模板上下文中)
    拼接表达式(C++26)
  10. ^^ 取反射,[::] 做拼接
  11. 前面跟 ::typename 的不是拼接表达式
  12. 构造/析构/匿名位字段不能拼接

C++26 标准草案 §7.6 复合表达式 —— 详细中文解析

原文来源:N5032,§7.6.1 后缀表达式(Postfix expressions)

目录

  1. 后缀表达式总览
  2. 下标操作 []
  3. 函数调用 ()
  4. 显式类型转换(函数式写法)
  5. 类成员访问 .->
  6. 自增自减 ++ --
  7. 综合示例代码

1. 后缀表达式总览

什么是后缀表达式?

后缀表达式就是"操作符写在操作数后面"的表达式,比如 a++obj.memberfunc() 等。
核心规则:后缀表达式从左向右结合。
也就是说 a.b.c 等价于 (a.b).c,先算左边,再算右边。

语法结构(简化版)

后缀表达式 :=
    主表达式
  | 后缀表达式 [ 下标 ]          // 数组/下标
  | 后缀表达式 ( 参数列表 )       // 函数调用
  | 后缀表达式 . 成员名           // 成员访问(点)
  | 后缀表达式 -> 成员名          // 成员访问(箭头)
  | 后缀表达式 ++                // 后置自增
  | 后缀表达式 --                // 后置自减
  | dynamic_cast<类型>(表达式)
  | static_cast<类型>(表达式)
  | reinterpret_cast<类型>(表达式)
  | const_cast<类型>(表达式)
  | typeid(表达式)

2. 下标操作

基本含义

E1[E2] 在语义上完全等价于 *((E1)+(E2))
用数学来表示,对于指针 p p p 和整数 i i i
p [ i ] ≡ ∗ ( p + i ) p[i] \equiv *(p + i) p[i](p+i)
因为加法满足交换律,所以下标访问也是可交换的(忽略求值顺序):
E 1 [ E 2 ] ≡ E 2 [ E 1 ] E1[E2] \equiv E2[E1] E1[E2]E2[E1]
这就是为什么 5[arr] 在 C++ 里合法,虽然没人这么写。

求值顺序规则

后缀表达式(即数组名/指针)
       先求值
          |
          v
expression-list 中各参数求值(顺序不定)

即:arr[i] 中,arr 一定先于 i 被求值。但如果是自定义的下标运算符,多个参数之间的求值顺序不确定

值类别


操作数类型 结果值类别
数组操作数是左值 结果是左值
数组操作数是右值(xvalue) 结果是 xvalue

3. 函数调用

调用机制

函数调用 = 后缀表达式 + ( 参数列表 )
虚函数 vs 非虚函数
调用 obj.method()
        |
        +-- method 是非虚函数  -->  直接调用,编译期确定
        |
        +-- method 是虚函数    -->  运行时查 vtable,调用动态类型的版本
                                    (这就是多态)

参数初始化顺序

每个参数在进入函数前都要被初始化,但多个参数之间的初始化顺序不确定

// 危险写法:i 被修改两次,结果未定义
void f(int a, int b);
int i = 0;
f(i++, i++);  // 未定义行为!两个 i++ 的顺序不确定

标准用一个巧妙的例子说明这不总是问题:

// 这个是合法且结果确定的(来自标准 Example 3)
std::string s = "but I have heard it works even if you don't believe in it";
s.replace(0, 4, "")
 .replace(s.find("even"), 4, "only")
 .replace(s.find(" don't"), 6, "");
// 链式调用,每次调用是顺序的(左到右)
assert(s == "I have heard it works only if you believe in it");

https://godbolt.org/z/o3Y75Gs6q
但下面这个就不确定了(来自标准 Example 4):

struct S { S(int); };
int operator<<(S, int);
int i, j;
int x = S(i=1) << (i=2);
// i 最终是 2(因为 << 是内置运算符,左先于右)
int y = operator<<(S(j=1), j=2);
// j 的值不确定!因为这是函数调用,两个参数初始化顺序不定

参数销毁顺序

参数总是以构造的逆序销毁(后构造的先销毁),这与局部变量的销毁规则一致。
销毁时机是实现定义的,可以是:

  • 函数退出时
  • 包含这次函数调用的完整表达式结束时

前置/后置条件(C++26 新增)

C++26 引入了合约(Contracts),函数调用时:
求值顺序 = 参数初始化 → 前置条件断言 → 函数体 → 后置条件断言 → 参数销毁 \text{求值顺序} = \text{参数初始化} \to \text{前置条件断言} \to \text{函数体} \to \text{后置条件断言} \to \text{参数销毁} 求值顺序=参数初始化前置条件断言函数体后置条件断言参数销毁

函数调用结果的值类别


返回类型 调用表达式的值类别
左值引用 T& 左值
右值引用到函数类型 F&& 左值
右值引用到对象类型 T&& xvalue
其他(包括值类型) prvalue

可变参数(...

当没有对应形参时,实参通过默认参数提升传递,规则为:

  • 整型 → 整型提升(如 charint char → promote int \text{char} \xrightarrow{\text{promote}} \text{int} charpromote int
  • floatdouble
  • std::nullptr_tvoid*

4. 显式类型转换(函数式写法)

基本形式

T(表达式)      // 等价于对应的 cast 表达式
T{初始化列表}  // 直接初始化

规则树

T(expr) 或 T{...}
    |
    +-- T 是 cv void          --> 什么都不做,表达式类型为 void
    |
    +-- T 是引用类型           --> 等价于直接初始化一个 T 类型临时变量
    |                               值类别:左值引用 -> 左值,否则 xvalue
    |
    +-- T 是单个括号内表达式   --> 等价于 cast 表达式
    |
    +-- 其他                   --> 创建 T 类型的 prvalue,用初始化列表直接初始化

示例说明

struct A {};
void f(A&);    // #1 接受左值引用
void f(A&&);   // #2 接受右值引用
A& g();        // g() 返回左值引用
f(g());        // 调用 #1:g() 是左值
f(A(g()));     // 调用 #2:A(g()) 创建临时对象(prvalue),匹配右值引用
f(auto(g()));  // 调用 #2:auto(g()) 也是 prvalue

auto(x) 是 C++23 引入的"衰变复制"语法,强制创建一个脱离原对象的副本。

5. 类成员访问 .->

箭头转换

E1->E2 在语义上完全等价于 (*(E1)).E2,即:
E 1 → E 2 ≡ ( ∗ ( E 1 ) ) . E 2 E1 \to E2 \equiv (*(E1)).E2 E1E2((E1)).E2
后续规则只讨论点(.)的形式,箭头形式先做这个转换即可。

结果值类别推导

E1.E2 中,E1 的类型为 c v 1 X cv_{1} X cv1XE2 的类型为 c v 2 T cv_{2} T cv2T,则:

非静态数据成员

c v 12 = c v 1 ∪ c v 2 cv_{12} = cv_1 \cup cv_2 cv12=cv1cv2
即 const/volatile 取"并集"(只要任一为 const,结果就是 const)。

E1 的值类别 E1.E2 的值类别
左值 左值
prvalue 或 xvalue xvalue


E2 是否 mutable E1.E2 的类型
v q 12   T vq_{12}\ T vq12 T(去掉 const)
c q 12   v q 12   T cq_{12}\ vq_{12}\ T cq12 vq12 T

静态数据成员

E1.E2左值E1 是被丢弃的值(discarded-value expression),即 E1 被求值但结果不用。

成员函数

成员函数类型 E1.E2 的值类别
静态成员函数 左值
非静态成员函数 prvalue(不能直接取地址)

C++26 反射拼接成员访问

C++26 支持用反射拼接表达式作为成员访问的右侧:

struct B { int b; };
struct C : B { int get() const { return b; } };
struct D : B, C {};
constexpr int f() {
    D d = {1, {}};
    // bases_of 获取 D 的所有直接基类反射列表
    // [0] 取第一个,即直接基类 B(不是通过 C 继承的 B)
    B& b = d.[: std::meta::bases_of(
                   ^^D,
                   std::meta::access_context::current()
               )[0] :];
    b.b += 10;
    return 10 * b.b + d.get();
    // d 的直接 B 子对象 b.b = 1 + 10 = 11
    // d.get() 调用 C::get(),返回 C 的 B 子对象的 b,仍为 1
    // 结果 = 10 * 11 + 1 = 111... 
    // 等等,标准说结果是 110,说明 d.get() 拿的也是同一个 B
}
static_assert(f() == 110);

这里的关键是:反射 bases_of(^^D)[0] 精确指定了"D 的直接基类 B",而不是"通过 C 继承的 B",解决了菱形继承中的歧义问题。

求值顺序

点操作符左侧的 E1 总是先被求值,即使最终结果不需要它(比如访问静态成员时)。

未定义行为陷阱

struct A { int i; };
struct B { int j; };
struct D : A, B {};
void f() {
    D d;
    static_cast<B&>(d).j;      // OK:正确转换到 B 子对象
    reinterpret_cast<B&>(d).j; // 未定义行为!类型不相似
}

规则:若 E1 的动态类型与 E1 表达式的静态类型不相似(similar),访问非静态成员则是未定义行为

6. 自增自减

后置 ++

后置 ++ 的语义:

  1. 读取当前值(lvalue-to-rvalue 转换),得到旧值副本
  2. 修改操作数(加 1)
  3. 返回旧值副本(prvalue
    result = old_value , operand = old_value + 1 \text{result} = \text{old\_value}, \quad \text{operand} = \text{old\_value} + 1 result=old_value,operand=old_value+1
    关键性质:读取旧值与修改操作数不会被函数调用打断(原子性保证在单线程语义下)。
    返回结果的类型是操作数类型去掉 cv 限定符后的版本。

后置 --

++ 完全对称,只是方向相反(减 1)。

操作数要求


条件 要求
值类别 必须是可修改的左值
类型 算术类型(非 bool)或指向完整对象的指针
volatile 类型操作数 已弃用(deprecated)

7. 综合示例代码

下面是一个完整可运行的示例,覆盖本文档所有要点:

#include <iostream>
#include <string>
#include <cassert>
// ============================================================
// §7.6.1.2 下标操作:E1[E2] 等价于 *((E1)+(E2))
// ============================================================
void demo_subscript() {
    int arr[5] = {10, 20, 30, 40, 50};
    // 正常写法
    std::cout << arr[2] << "\n";    // 30
    // 交换律:合法但不推荐
    std::cout << 2[arr] << "\n";    // 同样是 30,因为 *(arr+2) == *(2+arr)
    // 指针写法验证等价性
    int* p = arr;
    assert(p[3] == *(p + 3));       // 恒成立
}
// ============================================================
// §7.6.1.3 函数调用:参数顺序问题演示
// ============================================================
// 用于追踪构造顺序的辅助类
struct Tracked {
    int id;
    explicit Tracked(int i) : id(i) {
        std::cout << "构造 Tracked(" << i << ")\n";
    }
    ~Tracked() {
        std::cout << "析构 Tracked(" << id << ")\n";
    }
};
// 接受两个 Tracked 对象的函数
void take_two(Tracked a, Tracked b) {
    std::cout << "函数体:a=" << a.id << " b=" << b.id << "\n";
    // 参数销毁顺序:b 先销毁,然后 a(构造的逆序)
}
void demo_call_order() {
    std::cout << "\n--- 参数构造/析构顺序演示 ---\n";
    // 注意:Tracked(1) 和 Tracked(2) 哪个先构造,标准不保证
    take_two(Tracked{1}, Tracked{2});
    // 销毁顺序保证:后构造的先销毁
}
// ============================================================
// §7.6.1.4 显式类型转换(函数式)
// ============================================================
struct A {};
void f_ref(A&)  { std::cout << "f(A&) 左值重载\n"; }
void f_ref(A&&) { std::cout << "f(A&&) 右值重载\n"; }
A& get_A() {
    static A a;
    return a;   // 返回左值引用
}
void demo_type_conv() {
    std::cout << "\n--- 类型转换与值类别 ---\n";
    f_ref(get_A());        // 调用 f(A&),get_A() 是左值
    f_ref(A(get_A()));     // 调用 f(A&&),A(...) 创建 prvalue 临时对象
    f_ref(auto(get_A())); // 调用 f(A&&),auto(...) 强制创建副本
}
// ============================================================
// §7.6.1.5 类成员访问:cv 限定符传播
// ============================================================
struct Widget {
    int value = 42;
    mutable int cache = 0;  // mutable 成员不继承 const
    void show() const {
        // 在 const 成员函数中:
        // this 的类型是 const Widget*
        // value 类型变为 const int(不可修改)
        // cache 类型仍为 int(mutable,可修改)
        cache = value * 2;  // OK:cache 是 mutable
        // value = 99;      // 错误:value 在 const 上下文中
        std::cout << "value=" << value << " cache=" << cache << "\n";
    }
};
// ============================================================
// §7.6.1.6 后置自增:读旧值,然后修改
// ============================================================
void demo_postfix_inc() {
    std::cout << "\n--- 后置自增 ---\n";
    int x = 5;
    int old = x++;   // old = 5(旧值),x 变为 6
    std::cout << "old=" << old << " x=" << x << "\n";
    // 指针的后置自增
    int arr[3] = {1, 2, 3};
    int* p = arr;
    int val = *p++;  // val = arr[0] = 1,p 移到 arr[1]
    std::cout << "val=" << val << " *p=" << *p << "\n";
    // 后置--
    int y = 10;
    int old_y = y--;  // old_y = 10,y = 9
    std::cout << "old_y=" << old_y << " y=" << y << "\n";
}
// ============================================================
// §7.6.1.3 标准 Example 3:链式 replace 顺序确定
// ============================================================
void demo_chain_replace() {
    std::cout << "\n--- 链式调用顺序 ---\n";
    std::string s = "but I have heard it works even if you don't believe in it";
    // 链式调用:左到右顺序执行
    s.replace(0, 4, "")                      // 删除 "but "
     .replace(s.find("even"), 4, "only")     // "even" -> "only"
     .replace(s.find(" don't"), 6, "");      // " don't" -> ""
    assert(s == "I have heard it works only if you believe in it");
    std::cout << s << "\n";
}
int main() {
    demo_subscript();
    demo_call_order();
    demo_type_conv();
    Widget w;
    w.show();
    demo_postfix_inc();
    demo_chain_replace();
    return 0;
}

https://godbolt.org/z/7dGoWE8o7

总结速查表


操作 形式 等价形式 结果值类别
下标 E1[E2] *((E1)+(E2)) T 的左值或 xvalue
箭头成员访问 E1->E2 (*(E1)).E2 取决于成员类型
后置自增 x++ 返回旧值,x 加 1 prvalue
后置自减 x-- 返回旧值,x 减 1 prvalue
函数调用 f(args) 取决于返回类型
函数式转换 T(expr) 对应 cast prvalue(T 非引用时)

值类别快速参考

函数调用结果 = { 左值 返回类型为  T &  或 函数类型的  T & & xvalue 返回类型为对象类型的  T & & prvalue 其他情况 \text{函数调用结果} = \begin{cases} \text{左值} & \text{返回类型为 } T\& \text{ 或 函数类型的 } T\&\& \\ \text{xvalue} & \text{返回类型为对象类型的 } T\&\& \\ \text{prvalue} & \text{其他情况} \end{cases} 函数调用结果= 左值xvalueprvalue返回类型为 T&  函数类型的 T&&返回类型为对象类型的 T&&其他情况

参数求值顺序规则

后缀表达式 ⏟ 第一 ≺ 各参数 ⏟ 顺序不定,互相之间 ≺ 前置条件断言 ⏟ 最后,顺序执行 \underbrace{\text{后缀表达式}}_{\text{第一}} \prec \underbrace{\text{各参数}}_{\text{顺序不定,互相之间}} \prec \underbrace{\text{前置条件断言}}_{\text{最后,顺序执行}} 第一 后缀表达式顺序不定,互相之间 各参数最后,顺序执行 前置条件断言
其中 ≺ \prec 表示"先于(sequenced before)"。

C++26 标准草案 §7.6.1.7 —— dynamic_cast 详细中文解析

原文来源:N5032,§7.6.1.7 Dynamic cast

目录

  1. 基本语法与约束
  2. 三种目标类型的规则
  3. 向上转型(Upcast)
  4. 运行时检查逻辑
  5. 转型失败的结果
  6. 标准示例深度解析
  7. 完整可运行代码

1. 基本语法与约束

dynamic_cast<T>(v)
  • T:目标类型,必须是以下之一:
    • 指向完整类类型的指针
    • 完整类类型的引用(左值引用或右值引用)
    • void*(转换到最终派生对象)
  • v:源表达式
  • 不能去掉 constness(与 const_cast 不同,dynamic_cast 不能丢弃 const

"完整类类型"意思是:该类的定义在当前翻译单元中可见,不能是前向声明的不完整类型。

2. 三种目标类型的规则


目标类型 T v 的要求 结果值类别
指针类型 C* v 是指向完整类的prvalue 指针 prvalue
左值引用 C& v 是完整类的左值 左值
右值引用 C&& v 是完整类的glvalue xvalue

3. 向上转型(Upcast)

向上转型(子类转父类)是静态确定的,不需要运行时检查。

条件:T 是 "pointer/reference to cv1 B"
      v 的类型是 "pointer/reference to cv2 D"
      B 是 D 的基类

此时 dynamic_cast<B*>(pd) 等价于隐式转换,编译器直接计算偏移量。
前提B 必须是 D可访问(public)且无歧义的基类,否则程序非良构(ill-formed,编译错误)。

D 对象内存布局示意(D 继承 B):
+------------------+
|   B 子对象        |  <-- dynamic_cast<B*>(dp) 直接指向这里
|   (B的成员)       |
+------------------+
|   D 自己的成员    |
+------------------+

4. 运行时检查逻辑

前提:多态类型

除向上转型和 void* 外,v 必须指向/引用多态类型(即含有虚函数的类)。
这是因为 dynamic_cast 需要在运行时读取对象的 RTTI(Run-Time Type Information),而 RTTI 存储在虚函数表(vtable)中。
dynamic_cast 可用    ⟺    v 的类型含有虚函数(直接或继承) \text{dynamic\_cast 可用} \iff \text{v 的类型含有虚函数(直接或继承)} dynamic_cast 可用的类型含有虚函数(直接或继承)

转到 void*

dynamic_cast<void*>(v)

直接返回指向**最终派生对象(most derived object)**起始地址的指针。无论 v 指向哪个基类子对象,结果都是整个对象的开头。

最终派生对象(most derived object)
+-----------------------------+
|  ^                          |
|  | dynamic_cast<void*> 指这里|
|  基类 A 子对象               |
|  基类 B 子对象               |
|  D 自己的成员                |
+-----------------------------+

向下/横向转型的运行时检查步骤

设目标类型为 C C C,运行时执行以下逻辑(按顺序):

dynamic_cast<C*>(v) 运行时检查流程:
step 1: 找到 v 所在的"最终派生对象"
            |
            v
step 2: 检查条件 9.1
        在最终派生对象中,v 是否指向某个 C 对象的 public 基类子对象?
        且这样的 C 对象唯一?
            |
        是 --> 成功,返回那个 C 对象的指针
        否 --> 继续
            |
            v
step 3: 检查条件 9.2
        v 是否指向最终派生对象的 public 基类子对象?
        且最终派生对象有唯一、public 的 C 类型基类?
            |
        是 --> 成功,返回该 C 子对象的指针
        否 --> 失败

用数学表达成功条件:
成功    ⟺    ∃ !   C -对象  c  满足: v  指向  c  的 public 基类子对象,且路径唯一且可访问 \text{成功} \iff \exists!\ C\text{-对象}\ c \text{ 满足:} v \text{ 指向 } c \text{ 的 public 基类子对象,且路径唯一且可访问} 成功! C-对象 c 满足:v 指向 c  public 基类子对象,且路径唯一且可访问
其中 ∃ ! \exists! ! 表示"存在唯一"。

5. 转型失败的结果


目标类型 失败结果
指针 C* 返回空指针nullptr
引用 C&C&& 抛出 std::bad_cast 异常

这是一个重要的使用模式差异:

// 指针版本:检查 nullptr
Base* bp = ...;
Derived* dp = dynamic_cast<Derived*>(bp);
if (dp != nullptr) {
    // 转型成功
}
// 引用版本:捕获异常
Base& br = ...;
try {
    Derived& dr = dynamic_cast<Derived&>(br);
    // 转型成功
} catch (const std::bad_cast& e) {
    // 转型失败
}

6. 标准示例深度解析

示例一:虚继承与私有继承

类继承关系:
        A (虚基类,有虚函数 f)
        ^
        | (virtual public)
        D
        ^ (private 继承 B)
        |
        B (有虚函数 g)

用 mermaid 表示:

virtual public

private

D

A
(有虚函数 f)

B
(有虚函数 g)

D 对象内存布局(概念示意):
+----------------------+
| B 子对象             |  <-- bp = (B*)&d  (强制转型,绕过访问控制)
|   vptr -> B::vtable  |
+----------------------+
| D 自己成员           |
+----------------------+
| A 子对象(虚基类)   |  <-- ap = &d
|   vptr -> A::vtable  |
+----------------------+

各转型结果分析:

表达式 结果 原因
(B*)&d 编译通过(强制) C 风格强转,绕过访问控制检查
dynamic_cast<D&>(*bp) 失败,抛异常 B 是 D 的 private 基类,路径不可访问
dynamic_cast<A*>(bp) 失败,返回 null 从 B* 横向转到 A*,B 与 A 之间无直接 public 路径
dynamic_cast<B*>(ap) 失败,返回 null 同上,A* 到 B* 也无 public 路径
dynamic_cast<A*>(&d) 成功 D 虚继承 A(public),路径唯一且可访问
dynamic_cast<B*>(&d) 非良构(编译错误) B 是 D 的 private 基类,静态检查即失败

示例二:菱形继承与歧义

public

public

public

public

virtual public

E

D

B
(有虚函数 g)

F

A
(有虚函数 f)

F 对象内存布局(概念示意):
+---------------------------+
| E 子对象                  |
|   +-------------------+   |
|   | D 子对象(来自E)  |   |  <-- F 有两个 D 子对象!
|   +-------------------+   |
|   | B 子对象          |   |
+---------------------------+
| D 子对象(F直接继承的)   |  <-- 另一个 D
+---------------------------+
| A 子对象(虚基类,唯一)  |  <-- ap = &f 指向这里
+---------------------------+

表达式 结果 原因
A* ap = &f 成功(隐式) F 中 A 是虚基类,唯一
dynamic_cast<D*>(ap) 失败,返回 null F 中有两个 D 子对象,歧义,不满足"唯一"条件
(E*)ap 编译错误 不能从虚基类指针做 C 风格向下转型
dynamic_cast<E*>(ap) 成功 F 中只有一个 E 子对象,路径唯一且 public

关键结论:
dynamic_cast 成功    ⟺    目标类型在最终派生对象中唯一且路径全为 public \text{dynamic\_cast 成功} \iff \text{目标类型在最终派生对象中唯一且路径全为 public} dynamic_cast 成功目标类型在最终派生对象中唯一且路径全为 public

7. 完整可运行代码

#include <iostream>
#include <stdexcept>
// ============================================================
// 基础演示:向上转型(不需要运行时检查)
// ============================================================
struct Base {
    // 必须有虚函数,dynamic_cast 才能向下/横向转型
    virtual ~Base() = default;
    virtual void identify() const { std::cout << "Base\n"; }
};
struct Derived : public Base {
    void identify() const override { std::cout << "Derived\n"; }
    void derived_only() const { std::cout << "Derived 专有方法\n"; }
};
void demo_basic() {
    std::cout << "=== 基础向上/向下转型 ===\n";
    Derived d;
    Base* bp = &d;  // 向上转型:隐式,不需要 dynamic_cast
    // 向下转型:指针版本
    Derived* dp = dynamic_cast<Derived*>(bp);
    if (dp != nullptr) {
        std::cout << "向下转型成功:";
        dp->derived_only();
    }
    // 向下转型失败:指针版本返回 nullptr
    Base b;
    Base* bp2 = &b;
    Derived* dp2 = dynamic_cast<Derived*>(bp2);
    if (dp2 == nullptr) {
        std::cout << "向下转型失败:返回 nullptr\n";
    }
    // 向下转型失败:引用版本抛出 bad_cast
    try {
        Derived& dr = dynamic_cast<Derived&>(b);
        (void)dr;  // 不会执行到这里
    } catch (const std::bad_cast& e) {
        std::cout << "引用转型失败:捕获 bad_cast: " << e.what() << "\n";
    }
}
// ============================================================
// void* 转型:获取最终派生对象的起始地址
// ============================================================
void demo_void_cast() {
    std::cout << "\n=== void* 转型(获取 most derived object 地址)===\n";
    Derived d;
    Base* bp = &d;
    // bp 指向 d 的 Base 子对象部分
    // dynamic_cast<void*>(bp) 返回整个 d 对象的起始地址
    void* vp = dynamic_cast<void*>(bp);
    // 验证:void* 和 &d 应该相同(单继承下 Base 子对象就在开头)
    std::cout << "bp     地址: " << static_cast<void*>(bp) << "\n";
    std::cout << "void*  地址: " << vp << "\n";
    std::cout << "&d     地址: " << static_cast<void*>(&d) << "\n";
}
// ============================================================
// 虚继承:演示标准示例一
// ============================================================
class A {
public:
    virtual ~A() = default;
    virtual void fa() { std::cout << "A::fa\n"; }
};
class B {
public:
    virtual ~B() = default;
    virtual void gb() { std::cout << "B::gb\n"; }
};
// D 虚继承 A,私有继承 B
class D : public virtual A, private B {
public:
    ~D() override = default;
};
void demo_virtual_private() {
    std::cout << "\n=== 虚继承 + 私有继承 ===\n";
    D d;
    // C 风格强转绕过访问控制(不推荐,仅演示)
    B* bp = (B*)&d;
    A* ap = &d;  // D 虚继承 A(public),隐式转型
    // dynamic_cast<D&>(*bp):B 是 D 的 private 基类,失败
    try {
        D& dr = dynamic_cast<D&>(*bp);
        (void)dr;
    } catch (const std::bad_cast&) {
        std::cout << "dynamic_cast<D&>(*bp)  失败:B 是 D 的 private 基类\n";
    }
    // dynamic_cast<A*>(bp):从 B* 横向到 A*,无 public 路径,失败
    A* ap2 = dynamic_cast<A*>(bp);
    std::cout << "dynamic_cast<A*>(bp)   结果: "
              << (ap2 ? "成功" : "失败(nullptr)") << "\n";
    // dynamic_cast<B*>(ap):从 A* 横向到 B*,B 是 private 的,失败
    B* bp2 = dynamic_cast<B*>(ap);
    std::cout << "dynamic_cast<B*>(ap)   结果: "
              << (bp2 ? "成功" : "失败(nullptr)") << "\n";
    // dynamic_cast<A*>(&d):D 虚继承 A(public),唯一,成功
    A* ap3 = dynamic_cast<A*>(&d);
    std::cout << "dynamic_cast<A*>(&d)   结果: "
              << (ap3 ? "成功" : "失败") << "\n";
    // dynamic_cast<B*>(&d):B 是 private 基类,静态检查失败,注释掉
    // B* bp3 = dynamic_cast<B*>(&d);  // 编译错误!
}
// ============================================================
// 菱形继承:演示标准示例二
// ============================================================
class E : public D, public B {
public:
    ~E() override = default;
};
class F : public E, public D {
public:
    ~F() override = default;
};
void demo_diamond() {
    std::cout << "\n=== 菱形继承(歧义导致失败)===\n";
    F f;
    // F 虚继承 A(通过 D),A 唯一
    A* ap = &f;
    std::cout << "A* ap = &f  成功(A 是虚基类,唯一)\n";
    // F 有两个 D 子对象(一个来自 E,一个直接继承),歧义,失败
    D* dp = dynamic_cast<D*>(ap);
    std::cout << "dynamic_cast<D*>(ap)   结果: "
              << (dp ? "成功" : "失败(F 有两个 D 子对象,歧义)") << "\n";
    // F 只有一个 E 子对象,成功
    E* ep = dynamic_cast<E*>(ap);
    std::cout << "dynamic_cast<E*>(ap)   结果: "
              << (ep ? "成功" : "失败") << "\n";
}
// ============================================================
// 使用模式对比:指针版 vs 引用版
// ============================================================
void demo_usage_pattern() {
    std::cout << "\n=== 使用模式:指针 vs 引用 ===\n";
    Derived d;
    Base& br = d;
    // 模式1:指针版——用 if 检查
    if (Derived* dp = dynamic_cast<Derived*>(&br)) {
        std::cout << "指针版:转型成功\n";
        dp->derived_only();
    }
    // 模式2:引用版——用 try/catch(适合"必须成功"的场景)
    try {
        Derived& dr = dynamic_cast<Derived&>(br);
        std::cout << "引用版:转型成功\n";
        dr.derived_only();
    } catch (const std::bad_cast&) {
        std::cout << "引用版:转型失败\n";
    }
}
int main() {
    demo_basic();
    demo_void_cast();
    demo_virtual_private();
    demo_diamond();
    demo_usage_pattern();
    return 0;
}

https://godbolt.org/z/Mczd46Mfo

附:dynamic_cast vs 其他转型对比


特性 dynamic_cast static_cast reinterpret_cast C 风格 (T)
运行时检查
需要多态类型 是(向下/横向)
失败处理 nullptr / bad_cast 未定义行为 未定义行为 未定义行为
能去掉 const
性能开销 高(查 RTTI) 极低
推荐场景 安全向下/横向转型 已知安全的转型 底层内存操作 不推荐

选择策略

需要转型?
    |
    +-- 是指针/引用转换到基类? --> 直接隐式转换(无需 cast)
    |
    +-- 需要在运行时确认类型?  --> dynamic_cast
    |       |
    |       +-- 指针版:检查 nullptr
    |       +-- 引用版:捕获 bad_cast
    |
    +-- 确定类型安全,追求性能? --> static_cast
    |
    +-- 底层内存/ABI操作?      --> reinterpret_cast(小心使用)
    |
    +-- 去掉 const?            --> const_cast(几乎不应该用)

核心公式总结

dynamic_cast 成功条件 = { 无需检查 向上转型(子类 → 父类) 目标类型唯一且路径全 public 向下/横向转型 始终成功 目标为 void* \text{dynamic\_cast 成功条件} = \begin{cases} \text{无需检查} & \text{向上转型(子类} \to \text{父类)} \\ \text{目标类型唯一且路径全 public} & \text{向下/横向转型} \\ \text{始终成功} & \text{目标为 void*} \end{cases} dynamic_cast 成功条件= 无需检查目标类型唯一且路径全 public始终成功向上转型(子类父类)向下/横向转型目标为 void*
失败结果 = { nullptr T  是指针类型 抛出 std::bad_cast T  是引用类型 \text{失败结果} = \begin{cases} \text{nullptr} & T \text{ 是指针类型} \\ \text{抛出 std::bad\_cast} & T \text{ 是引用类型} \end{cases} 失败结果={nullptr抛出 std::bad_castT 是指针类型T 是引用类型

C++26 标准草案 §7.6.1.8–7.6.1.9 —— typeidstatic_cast 详细中文解析

原文来源:N5032,§7.6.1.8 Type identification / §7.6.1.9 Static cast

目录

  1. typeid —— 类型识别
  2. static_cast —— 静态转型
  3. 综合对比速查

1. typeid —— 类型识别

1.1 基本语义与返回值

typeid 用于在运行时(或编译期)查询一个表达式或类型的类型信息。

typeid(表达式)
typeid(类型名)

返回值:const std::type_info&(左值引用)

  • 静态类型永远是 const std::type_info
  • 动态类型可以是实现定义的派生类(标准建议命名为 extended_type_info
  • 返回的对象生命周期延续到程序结束,可以安全地保存其引用

使用 typeid 前必须包含 <typeinfo>,否则程序非良构(编译错误)。

1.2 多态类型 vs 非多态类型

这是 typeid 最核心的规则,两种情况行为完全不同:

typeid(表达式)

表达式是多态类型的 glvalue?
即类含有虚函数

运行时查询
返回动态类型
(most derived)
的 type_info

编译期确定
返回静态类型
的 type_info

注意:不做左值到右值转换
不做数组到指针转换
表达式本身不被求值

用内存布局说明动态类型查询:

Base* bp = new Derived();   // bp 静态类型是 Base*,动态类型是 Derived
          bp
          |
          v
    +------------------+
    | vptr ------------|----> Derived 的 vtable
    | Base 的成员      |      (包含 RTTI 指针)
    | Derived 的成员   |           |
    +------------------+           v
                             type_info for Derived
typeid(*bp) 沿着 vptr 找到 Derived 的 type_info
结果:typeid(*bp).name() 返回 "Derived",而不是 "Base"

typeid ( 多态 glvalue ) ⇒ 动态类型的 type_info(运行时决定) \text{typeid}(\text{多态 glvalue}) \Rightarrow \text{动态类型的 type\_info(运行时决定)} typeid(多态 glvalue)动态类型的 type_info(运行时决定)
typeid ( 其他 ) ⇒ 静态类型的 type_info(编译期决定) \text{typeid}(\text{其他}) \Rightarrow \text{静态类型的 type\_info(编译期决定)} typeid(其他)静态类型的 type_info(编译期决定)

1.3 cv 限定符的处理

typeid 忽略所有 cv 限定符(const/volatile)和引用,以下所有比较结果均为 true

typeid(D)         == typeid(const D)      // 忽略 const
typeid(d1)        == typeid(d2)           // d1: D,  d2: const D
typeid(D)         == typeid(d2)           // 忽略变量的 const
typeid(D)         == typeid(const D&)     // 忽略引用和 const

typeid ( c v   T ) ≡ typeid ( T ) , typeid ( T & ) ≡ typeid ( T ) \text{typeid}(cv\ T) \equiv \text{typeid}(T), \quad \text{typeid}(T\&) \equiv \text{typeid}(T) typeid(cv T)typeid(T),typeid(T&)typeid(T)

1.4 空指针解引用的异常

Base* p = nullptr;
typeid(*p);   // 抛出 std::bad_typeid

这是 typeid 独有的保护机制:当操作数是 *空指针 时,抛出 std::bad_typeid 而不是产生未定义行为。
注意:只有 typeid 有这个保护,其他情况下解引用空指针仍是未定义行为。

1.5 typeid 完整代码示例

#include <iostream>
#include <typeinfo>   // 必须包含,否则程序非良构
// ============================================================
// 多态类层次
// ============================================================
struct Animal {
    virtual ~Animal() = default;        // 有虚函数 => 多态类型
    virtual void speak() const = 0;
};
struct Dog : Animal {
    void speak() const override { std::cout << "Woof\n"; }
};
struct GoldenRetriever : Dog {
    void speak() const override { std::cout << "Woof woof!\n"; }
};
// ============================================================
// 非多态类(无虚函数)
// ============================================================
struct Point {
    int x, y;     // 无虚函数 => 非多态类型
};
struct Point3D : Point {
    int z;
};
// ============================================================
// 演示1:多态类型 —— 查询动态类型
// ============================================================
void demo_polymorphic() {
    std::cout << "=== 多态类型:返回动态类型 ===\n";
    GoldenRetriever gr;
    Dog& dog_ref = gr;              // 引用绑定到 GoldenRetriever
    Animal* ap = &gr;               // 指针指向 GoldenRetriever
    // typeid 通过 vptr 查询实际对象类型
    std::cout << "typeid(gr)       : " << typeid(gr).name()       << "\n";
    std::cout << "typeid(dog_ref)  : " << typeid(dog_ref).name()  << "\n"; // 动态类型
    std::cout << "typeid(*ap)      : " << typeid(*ap).name()      << "\n"; // 动态类型
    // 比较:同一对象,不同静态类型,typeid 结果相同
    bool same = (typeid(dog_ref) == typeid(*ap));
    std::cout << "typeid(dog_ref) == typeid(*ap) : " << std::boolalpha << same << "\n";
    // 与静态类型 Dog 不同(因为实际是 GoldenRetriever)
    bool diff = (typeid(dog_ref) == typeid(Dog));
    std::cout << "typeid(dog_ref) == typeid(Dog): " << diff << "\n";
}
// ============================================================
// 演示2:非多态类型 —— 返回静态类型,表达式不求值
// ============================================================
void demo_non_polymorphic() {
    std::cout << "\n=== 非多态类型:返回静态类型 ===\n";
    Point3D p3d{1, 2, 3};
    Point& ref = p3d;               // 引用绑定到 Point3D
    // 没有虚函数,typeid 只看静态类型
    std::cout << "typeid(ref)    : " << typeid(ref).name()    << "\n"; // Point,不是 Point3D
    std::cout << "typeid(p3d)    : " << typeid(p3d).name()    << "\n"; // Point3D
    // 表达式不被求值(unevaluated operand)
    int counter = 0;
    // 下面的 ++counter 不会被执行!
    const std::type_info& ti = typeid(++counter);
    std::cout << "counter 仍为 0: " << counter << "\n";   // 输出 0
    std::cout << "类型: " << ti.name() << "\n";            // int
}
// ============================================================
// 演示3:cv 限定符被忽略
// ============================================================
void demo_cv_ignored() {
    std::cout << "\n=== cv 限定符被忽略 ===\n";
    Point p1;
    const Point p2{};
    // 所有结果都是 true
    std::cout << (typeid(p1)        == typeid(p2))         << " (p1 vs const p2)\n";
    std::cout << (typeid(Point)     == typeid(const Point)) << " (Point vs const Point)\n";
    std::cout << (typeid(Point)     == typeid(p2))          << " (Point vs const p2)\n";
    std::cout << (typeid(Point)     == typeid(const Point&))<< " (Point vs const Point&)\n";
}
// ============================================================
// 演示4:解引用空指针抛出 bad_typeid
// ============================================================
void demo_null_ptr() {
    std::cout << "\n=== 解引用空指针 => std::bad_typeid ===\n";
    Animal* p = nullptr;
    try {
        // *p 是空指针解引用,typeid 特殊处理,抛 bad_typeid
        const std::type_info& ti = typeid(*p);
        (void)ti;
    } catch (const std::bad_typeid& e) {
        std::cout << "捕获 bad_typeid: " << e.what() << "\n";
    }
}
// ============================================================
// 演示5:type_info 对象生命周期到程序结束,可安全保存
// ============================================================
const std::type_info* saved_ti = nullptr;
void save_type() {
    Dog d;
    // type_info 对象生命期到程序结束,指针在函数返回后仍有效
    saved_ti = &typeid(d);
}
void demo_lifetime() {
    std::cout << "\n=== type_info 生命周期 ===\n";
    save_type();
    // saved_ti 仍然有效
    std::cout << "保存的类型: " << saved_ti->name() << "\n";
}
int main() {
    demo_polymorphic();
    demo_non_polymorphic();
    demo_cv_ignored();
    demo_null_ptr();
    demo_lifetime();
    return 0;
}

https://godbolt.org/z/G8x58519q

2. static_cast —— 静态转型

2.1 基本语义与结果值类别

static_cast<T>(v)

结果值类别由目标类型 T 决定:

目标类型 T 结果值类别
左值引用 T& 左值
右值引用到函数类型 F&& 左值
右值引用到对象类型 T&& xvalue
其他(值类型) prvalue

static_cast编译期静态检查,不产生运行时开销(与 dynamic_cast 的运行时查询不同)。

2.2 类层次间的引用/指针转换

向下转型(基类 -> 派生类)
条件:
  - 目标类型 D 是 B 的完整派生类
  - cv 限定只能增加不能减少(cv2 >= cv1)
  - B 不是虚基类,也不是虚基类的基类
  - 存在合法的 D* -> B* 标准转换

c v 2 ≥ c v 1 ( 即  c v 2  的限定不少于  c v 1 ) cv_2 \geq cv_1 \quad (\text{即 } cv_2 \text{ 的限定不少于 } cv_1) cv2cv1( cv2 的限定不少于 cv1)

类层次示意:
    B
    ^
    |(public 非虚继承)
    D
B& br = d;                    // 向上(隐式)
static_cast<D&>(br);          // 向下(静态,无运行时检查)
                              // 若 br 实际不引用 D 对象 => 未定义行为!

关键差异对比:

static_cast<D*>(bp)   -- 不检查,若 bp 实际不指向 D 对象 => 未定义行为
dynamic_cast<D*>(bp)  -- 运行时检查,失败返回 nullptr,安全
虚基类的限制

virtual public

public

D

B

C

// B 是 D 的虚基类
// static_cast<D*>(bp) 非良构(编译错误)
// 必须用 dynamic_cast

原因:虚基类子对象的偏移量在编译期不确定,只能运行时查询。

成员指针的逆变换

成员指针转换方向与类指针相反(逆变):

类指针:  Base* -> Derived*   (向下,static_cast)
成员指针:Derived::* -> Base::*(向上,static_cast)

成员指针转换方向 = 类继承方向的反向 \text{成员指针转换方向} = \text{类继承方向的反向} 成员指针转换方向=类继承方向的反向

struct Base { int x; };
struct Derived : Base { int y; };
// 类指针:派生类指针转基类指针(向上,隐式)
Derived d;
Base* bp = &d;
// 成员指针:基类成员指针转派生类(向下,隐式)
int Base::* mp_base = &Base::x;
int Derived::* mp_derived = mp_base;    // 隐式转换
// 成员指针:派生类成员指针转基类(向上,需要 static_cast)
// int Base::* mp2 = &Derived::y;       // 错误:y 不是 Base 的成员

2.3 枚举类型转换

有作用域枚举(enum class)转整型/浮点
enum class Color { Red = 1, Green = 2, Blue = 3 };
int i = static_cast<int>(Color::Green);           // 2
double d = static_cast<double>(Color::Blue);       // 3.0

过程(两步走):
enum class → 第一步 underlying type(如 int) → 第二步 目标类型 \text{enum class} \xrightarrow{\text{第一步}} \text{underlying type(如 int)} \xrightarrow{\text{第二步}} \text{目标类型} enum class第一步 underlying type(如 int第二步 目标类型

整型/浮点转枚举
有固定底层类型的枚举:
  整数值 -> 先整型提升到底层类型 -> 再转换为枚举值
  若值不在枚举范围内:标准允许(结果是该枚举的对应值)
没有固定底层类型的枚举:
  若值在枚举范围内 -> 值不变
  若值超出范围    -> 未定义行为!

枚举范围 = [ 0 ,   2 ⌈ log ⁡ 2 ( max ⁡ _ e n u m e r a t o r + 1 ) ⌉ − 1 ] \text{枚举范围} = [0,\ 2^{\lceil \log_2(\max\_enumerator+1) \rceil} - 1] 枚举范围=[0, 2log2(max_enumerator+1)⌉1]
(对于非负枚举值)

2.4 浮点类型转换

float  ->  double  -> long double   (精度提升,精确)
long double -> double -> float      (精度降低,可能丢失)

转换规则:
结果 = { 精确值 若源值能在目标类型中精确表示 两邻近值之一(实现定义) 若源值在两个目标值之间 未定义行为 若源值超出目标类型范围 \text{结果} = \begin{cases} \text{精确值} & \text{若源值能在目标类型中精确表示} \\ \text{两邻近值之一(实现定义)} & \text{若源值在两个目标值之间} \\ \text{未定义行为} & \text{若源值超出目标类型范围} \end{cases} 结果= 精确值两邻近值之一(实现定义)未定义行为若源值能在目标类型中精确表示若源值在两个目标值之间若源值超出目标类型范围

2.5 void* 与对象指针互转

这是 C++ 中类型擦除的基础机制:

T*  -->  void*  -->  T*    (合法的往返转换)
T*  -->  void*  -->  U*    (若 T != U,通常未定义行为)
内存模型示意:
地址 A: [ T 对象 ]
         ^
         | T* p1 指向这里
         |
static_cast<void*>(p1) = 地址 A(仅记录地址,丢失类型信息)
         |
         v
static_cast<const T*>(void_ptr) = 还原回 T*(合法)
static_cast<U*>(void_ptr)       = 若地址对齐不满足 U 的要求 => 未指定
                                  若对象不是 U 类型 => 未定义行为

对齐要求:
合法    ⟺    A   m o d   alignof ( T ) = 0 \text{合法} \iff A \bmod \text{alignof}(T) = 0 合法Amodalignof(T)=0
若地址 A A A 不满足类型 T T T 的对齐要求,结果未指定(unspecified)

2.6 成员指针转换

struct Base   { int x; };
struct Derived : Base { int y; };
// 基类成员指针 -> 派生类成员指针(方向:逆变)
int Base::*    mp_b = &Base::x;
int Derived::* mp_d = static_cast<int Derived::*>(mp_b);
// 使用:
Derived obj{10, 20};
std::cout << obj.*mp_d << "\n";  // 10(访问的是继承来的 x)

转换有效的前提:Base 必须实际包含该成员,或者是包含该成员的类的基类。

2.7 static_cast 完整代码示例

#include <iostream>
#include <typeinfo>
// ============================================================
// 演示1:类层次向下转型
// ============================================================
struct Base {
    int base_val = 10;
    virtual ~Base() = default;  // 虚析构(方便 dynamic_cast 对比)
};
struct Derived : public Base {
    int derived_val = 20;
};
void demo_downcast() {
    std::cout << "=== 类层次向下转型 ===\n";
    Derived d;
    Base& br = d;   // 向上转型(隐式)
    // static_cast 向下:已知 br 实际引用 Derived,安全
    Derived& dr = static_cast<Derived&>(br);
    std::cout << "derived_val = " << dr.derived_val << "\n";  // 20
    // 指针版本
    Base* bp = &d;
    Derived* dp = static_cast<Derived*>(bp);
    std::cout << "dp->derived_val = " << dp->derived_val << "\n";  // 20
    // 危险示例(注释掉,不实际运行未定义行为)
    // Base b_only;
    // Derived* bad = static_cast<Derived*>(&b_only);  // UB:b_only 不是 Derived!
}
// ============================================================
// 演示2:枚举转换
// ============================================================
enum class Direction { North = 0, East = 1, South = 2, West = 3 };
// 没有固定底层类型的老式枚举
enum OldEnum { A = 1, B = 4 };
void demo_enum_cast() {
    std::cout << "\n=== 枚举类型转换 ===\n";
    // enum class -> 整型(必须显式转换)
    Direction dir = Direction::South;
    int i = static_cast<int>(dir);
    std::cout << "Direction::South -> int: " << i << "\n";   // 2
    // enum class -> 浮点
    double d = static_cast<double>(Direction::West);
    std::cout << "Direction::West -> double: " << d << "\n"; // 3.0
    // 整型 -> enum class
    Direction d2 = static_cast<Direction>(1);
    std::cout << "1 -> Direction: " << (d2 == Direction::East ? "East" : "other") << "\n";
    // 整型 -> 老式枚举(有作用域外的值,行为取决于范围)
    // OldEnum 的范围是 [0, 7](因为最大值 4,需要 3 位)
    OldEnum e = static_cast<OldEnum>(3);  // 3 在范围内,合法
    std::cout << "3 -> OldEnum: " << static_cast<int>(e) << "\n";
}
// ============================================================
// 演示3:浮点类型转换
// ============================================================
void demo_float_cast() {
    std::cout << "\n=== 浮点类型转换 ===\n";
    double d = 3.14159265358979;
    // double -> float(精度降低,结果是邻近两值之一)
    float f = static_cast<float>(d);
    std::cout << "double -> float: " << f << "\n";
    // float -> double(精度提升,精确)
    double d2 = static_cast<double>(f);
    std::cout << "float -> double: " << d2 << "\n";
    // 注意 d != d2,因为中间经历了精度损失
    std::cout << "原始 double: " << d << "\n";
    std::cout << "往返后 double: " << d2 << "\n";
    std::cout << "是否相等: " << std::boolalpha << (d == d2) << "\n";  // false
}
// ============================================================
// 演示4:void* 往返转换(类型擦除)
// ============================================================
void demo_void_ptr() {
    std::cout << "\n=== void* 类型擦除往返转换 ===\n";
    int x = 42;
    int* p1 = &x;
    // T* -> void*(类型擦除)
    void* vp = static_cast<void*>(p1);
    // void* -> const T*(还原,增加了 const,合法)
    const int* p2 = static_cast<const int*>(vp);
    std::cout << "原始值: " << *p1 << "\n";        // 42
    std::cout << "还原后: " << *p2 << "\n";        // 42
    std::cout << "地址相同: " << (p1 == p2) << "\n"; // true(const int* 可与 int* 比较)
}
// ============================================================
// 演示5:成员指针的逆变转换
// ============================================================
struct Shape {
    double area = 0.0;
};
struct Circle : Shape {
    double radius = 1.0;
};
void demo_member_ptr() {
    std::cout << "\n=== 成员指针转换(逆变)===\n";
    // Shape 的成员指针
    double Shape::* mp_shape = &Shape::area;
    // 转换到 Circle 的成员指针(类指针是向下,成员指针是向上)
    double Circle::* mp_circle = static_cast<double Circle::*>(mp_shape);
    Circle c;
    c.area = 3.14;
    std::cout << "通过成员指针访问 area: " << c.*mp_circle << "\n";  // 3.14
}
// ============================================================
// 演示6:转换到 void(丢弃值)
// ============================================================
int side_effect_func() {
    std::cout << "side_effect_func 被调用\n";
    return 42;
}
void demo_void_cast() {
    std::cout << "\n=== 转换到 void(保留副作用,丢弃值)===\n";
    // 调用函数但忽略返回值,明确表达"故意忽略"
    static_cast<void>(side_effect_func());
    // 函数仍被调用(有副作用),但返回值被丢弃
}
int main() {
    demo_downcast();
    demo_enum_cast();
    demo_float_cast();
    demo_void_ptr();
    demo_member_ptr();
    demo_void_cast();
    return 0;
}

https://godbolt.org/z/5ndKohGP4

3. 综合对比速查

typeid 行为速查


操作数 有虚函数? 返回类型 求值时机
多态类的 glvalue 动态类型(运行时) 运行时
非多态类的 glvalue 静态类型(编译期) 不求值
prvalue 静态类型 实质化后不求值
类型名 typeid(T) T 的静态类型 编译期
*空指针 抛出 std::bad_typeid 运行时

static_cast 能做什么(总览)

static_cast<T>(v) 合法场景:
1. 存在隐式转换序列(隐式能做的 static_cast 都能做)
2. 类层次向下转型(非虚基类,cv 只增不减)
3. lvalue -> rvalue reference(T2 与 T1 引用兼容)
4. 任意表达式 -> void(丢弃值)
5. scoped enum <-> 整型 / 浮点
6. 整型 / 浮点 -> 任意枚举
7. 浮点 <-> 浮点
8. 类层次指针向下(非虚基类)
9. void* <-> 对象指针
10. 成员指针协变/逆变转换

四种转型对比

需要转型 ->
    |
    +-- 涉及类层次,需安全检查?
    |       yes --> dynamic_cast(运行时,有开销)
    |
    +-- 涉及类层次,确认安全/非虚继承?
    |       yes --> static_cast(编译期,无开销)
    |
    +-- 需要去掉 const/volatile?
    |       yes --> const_cast
    |
    +-- 底层内存/不相关指针类型之间?
            yes --> reinterpret_cast(极度小心)

核心公式汇总

typeid 的 cv 忽略规则: typeid ( c v   T ) ≡ typeid ( T & ) ≡ typeid ( T ) \text{typeid 的 cv 忽略规则:}\quad \text{typeid}(cv\ T) \equiv \text{typeid}(T\&) \equiv \text{typeid}(T) typeid  cv 忽略规则:typeid(cv T)typeid(T&)typeid(T)
static_cast 向下合法条件: c v 2 ≥ c v 1    ∧    B  非虚基类    ∧    ∃   D ∗ → B ∗  标准转换 \text{static\_cast 向下合法条件:}\quad cv_2 \geq cv_1 \;\land\; B \text{ 非虚基类} \;\land\; \exists\, D^* \to B^* \text{ 标准转换} static_cast 向下合法条件:cv2cv1B 非虚基类DB 标准转换
void* 还原合法条件: 地址 A   m o d   alignof ( T ) = 0    ∧    原始对象类型与 T 相似 \text{void* 还原合法条件:}\quad \text{地址} A \bmod \text{alignof}(T) = 0 \;\land\; \text{原始对象类型与 T 相似} void* 还原合法条件:地址Amodalignof(T)=0原始对象类型与 T 相似

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐