找回密码
立即注册
搜索
发回帖 发新帖

4968

积分

0

好友

634

主题
发表于 半小时前 | 查看: 4| 回复: 0

在之前的 C++26 反射文章中,我们接触到了一个新特性 template for。当时我们用它可以更直接地遍历结构体成员。不过,template for 究竟是什么?

从技术层面来说,它是一个与静态反射完全独立的提案,被称为扩展语句(Expansion Statement,P1306)。然而,这两项特性在设计上又相辅相成。本文就来聊聊扩展语句的基本原理,以及它如何解决 C++ 最古老的问题之一:遍历异构数据。

异构迭代

在标准 C++ 中,for 循环要求容器中的每个元素都具有完全相同的类型。

例如对于 std::vector<int>,下面这种写法可以正常工作:

for (int x : vec) {
    // ...
}

但如果你面对的是一个 std::tuple<int, double, std::string> 呢?

auto my_tuple = std::make_tuple(42, 3.14, "Hello");

// 错误:'x' 的类型是什么?
// 在第 1 次循环中它是 int,第 2 次是 double,第 3 次是 string!
for (auto x : my_tuple) {
    std::cout << x << '\n';
}

因为 C++ 是静态类型语言,传统的运行时 for 循环无法在每次迭代中改变变量 x 的类型。

在过去,解决这个问题通常需要编写递归模板函数,或者依赖 std::apply、Boost.Hana 这类工具。虽然可行,但为了一个看似简单的需求,往往要写出远超预期的模板代码。

template for

C++26 引入了扩展语句。

在 for 循环前加上 template 关键字,就可以告诉编译器在编译期展开代码。编译器会在编译期间把循环体展开为一连串独立的语句,运行时不复存在任何循环对象或循环计数器。

理解 template for 的关键在于:它并不是一个真正的循环,而是一系列孤立的代码块。因为每一次迭代都有自己独立的作用域 { },所以变量 x 在每次展开时都是重新声明的。这意味着 x 在第一个代码块中可以是 int,在第二个代码块中又可以轻松变成 double。

注:与模板实例化类似,扩展语句会根据展开元素的数量成比例地增加生成的代码体积。如果你遍历一个包含 100 个元素的 tuple,编译器将生成 100 份循环体的副本。这是循环展开的经典权衡:以二进制体积为代价,换取执行速度和极大的类型灵活性。

看下面这段代码:

#include <tuple>
#include <iostream>

void print_tuple()
{
    auto tup = std::make_tuple(10, 3.14, "Hello");

    template for (auto value : tup)
    {
        std::cout << value << '\n';
    }
}

int main()
{
    print_tuple();
    return 0;
}

其汇编代码精简后如下:

"print_tuple()":
    call    "std::get<0ul, int, double, char const*>(...)"
    call    "std::get<1ul, int, double, char const*>(...)"
    call    "std::get<2ul, int, double, char const*>(...)"

    call    "std::ostream::operator<<(int)"
    call    "std::ostream::operator<<(double)"
    call    "std::operator<<(..., char const*)"

可以理解为编译器生成了类似下面的代码:

{
    const auto& field = std::get<0>(tup); // int
    std::cout << field << '\n';
}
{
    const auto& field = std::get<1>(tup); // double
    std::cout << field << '\n';
}
{
    const auto& field = std::get<2>(tup); // std::string
    std::cout << field << '\n';
}

支持类型

看完 tuple 的遍历后,你可能会好奇:template for 都支持哪些类型?根据标准文档,它主要支持三类数据。

可解构类型

只要能在某个类型上使用结构化绑定,比如:

auto [a, b] = obj;

就能在其上使用 template for。这包括 std::tuple、std::pair 以及标准结构体。

struct Size {
    int width;
    short height;
};

consteval long sum_size(Size size)
{
    long result = 0;

    template for (auto value : size) {
        result += value;
    }

    return result;
}

static_assert(sum_size({100, 20}) == 120);

初始化列表

也可以把多个表达式包在花括号 {} 中,直接遍历这个表达式列表。这在编写参数包时非常有用,可以避免使用复杂的折叠表达式。

#include <iostream>
#include <string_view>

template <typename... Ts>
void print_all(const Ts&... values)
{
    template for (const auto& value : {values...}) {
        std::cout << value << '\n';
    }
}

int main()
{
    print_all(7, 2.5, std::string_view{"ready"});
}

虽然看起来像 std::initializer_list,但内存中实际上不会创建任何数组。花括号只是告诉编译器逐个展开这些表达式。

静态反射

template for 还能与 <meta> 头文件很好地结合,遍历任何值的范围,只要该范围是一个常量表达式。

在之前的反射文章中,我们遍历过结构体成员。但由于 std::meta::nonstatic_data_members_of 这类函数返回的是 std::vector,它会在堆上分配内存,因此直接将它们传给 template for 会违反 C++ 的内存规则。

为此,反射提案提供了 std::meta::define_static_array() 工具。它能把 vector 冻结为一个安全的编译期 span,以便安全地进行扩展遍历:

struct Point {
    int x;
    int y;
};

void print_offsets()
{
    constexpr auto members = std::meta::define_static_array(
        std::meta::nonstatic_data_members_of(^^Point)
    );

    template for (constexpr auto mem : members) {
        std::cout << "Offset: "
                  << std::meta::offset_of(mem).bytes
                  << '\n';
    }
}

如果你希望系统性地理解 template for 这类现代 C++ 特性背后的模板机制,可以看看这门 template for 课程,它从基础语法讲到 C++20 Concepts、变长模板与完美转发,对现代 C++ 范式覆盖得比较完整。

一些细节

前面例子中可能存在一个微妙差异:有时写的是 template for (auto x : ...),有时又是 template for (constexpr auto x : ...)。

  • 使用 auto:当提取的是运行时数据时,例如从 std::tuple 或结构体中取元素。循环本身在编译期展开,但变量保存的是运行时数据。
  • 使用 constexpr auto:当遍历的是编译期数据时,例如反射描述符 std::meta::info。

如果计划拼接该变量,例如 [: mem :],或者把它传递给类型萃取,那么该变量必须为 constexpr。

控制流

普通 for 循环中可以使用 continue 和 break,template for 也不例外。

#include <iostream>

int main()
{
    template for (auto number : {1, 2, 3, 4, 5}) {
        if (number % 2 == 0) {
            continue;
        }

        std::cout << number << '\n';

        if (number == 3) {
            break;
        }
    }
}

看到这段代码,第一反应可能是:它们会阻止编译器实例化后续语句吗?

其实不会。它们的作用和运行时一样,控制的是运行时执行流程,并不会阻止编译器实例化后续语句。

执行到 2 时,continue 跳过当前这份语句的剩余部分,接着执行下一份;执行到 3 时,break 结束整个扩展语句。因此,4 和 5 对应的语句虽然已经被实例化,但这次运行不会执行到它们。

结语

扩展语句让 C++ 能以接近普通范围 for 的写法处理 tuple、参数包和结构体成员等异构数据。与静态反射结合后,我们还可以针对不同成员分别生成代码,而不用先写一套递归模板。

使用时记住一点就够了:循环体在编译期分别实例化,循环体中的操作仍然可以在运行时执行。它让异构迭代更容易表达,同时也要留意元素数量增加带来的编译开销和代码体积。




上一篇:Claude访问限制放宽:Anthropic向认证安全人员开放三级权限
下一篇:开发板这么多,为什么我还要为TinyML重新做一款STM32板子?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-11 04:53 , Processed in 0.075677 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表