ITPub博客

首页 > 数据库 > 数据库开发技术 > 国外C++程序员分享:告别复杂的元编程,抢先预览C++26的新特性

国外C++程序员分享:告别复杂的元编程,抢先预览C++26的新特性

原创 数据库开发技术 作者:讳疾忌医-note 时间:2025-02-07 01:20:35 0 删除 编辑
创作不易,方便的话点点关注,谢谢
本文是经过严格查阅相关权威文献和资料,形成的专业的可靠的内容。全文数据都有据可依,可回溯。特别申明:数据和资料已获得授权。本文内容,不涉及任何偏颇观点,用中立态度客观事实描述事情本身。

文章结尾有最新热度的文章,感兴趣的可以去看看。

文章有点长(4879字阅读时长:8分),期望您能坚持看完,并有所收获


大家好,今天我继续给大家分享干货。熟悉我的人,都知道我真正的干货一般在中间和末尾部分(文章中间部分:有项目文字和视频介绍)。请耐心看完!谢谢。您可以在文章末尾处获取完整的源代码文件。

 

在过去十年里,C++在让模板元编程(TMP)变得更友好易用方面取得了重大进展。诸如if constexpr、概念(concepts)以及改进后的模板参数包等特性,降低了编写高级元编程代码的难度门槛。然而,即便有了这些改进,开发者们仍常常会碰到一些看似寻常的操作,实现起来却棘手得很,编译速度慢不说,甚至根本无法简洁地表达出来。

在本文中,我将带大家了解一些可能会在C++26(或后续版本)中出现的改进提案,这些改进旨在降低复杂性、缩短编译时间,让开发者不再抓狂。我们会探讨基于语言层面的新方法,用于对参数包进行切片、索引、反射以及返回。我们还会简要看看在诊断方面可能的改进,以及这些变化如何与反射相辅相成。

我会尽量用通俗易懂的方式,并结合实际场景的示例来讲解。其中一些提案直接借鉴了诸如肖恩·巴克斯特(Sean Baxter)的Circle等实验性语言和编译器,以及ISO C++委员会中流传的一些小型提案。让我们开始吧。

1. 拓展参数包的能力

参数包本质上是编译期的一组序列,可以是类型或表达式。C++20引入了一些实用特性(比如折叠表达式),但有时,像 “获取参数包的第3个元素” 或 “截取最后两个元素” 这样基础的操作,实现起来仍得费一番周折。

1.1 参数包索引

索引是一种非常基础的操作,许多刚接触TMP的开发者都很惊讶它居然不是C++标准的一部分。大家反复提出的问题是:“为什么我们不能直接用T...[3]从typename...T中获取第4个类型呢?”

按照目前的做法,要获取类型包中的第N个元素,通常需要多层模板技巧,就像这样:

// 经典的递归方法
template <std::size_t I, typename T, typename... Ts>
struct nth_element_impl {
    using type = typename nth_element_impl<I - 1, Ts...>::type;
};

template <typename T, typename... Ts>
struct nth_element_impl<0, T, Ts...> {
    using type = T;
};
template <std::size_t I, typename... Ts>
using nth_element = typename nth_element_impl<I, Ts...>::type;

虽然这种方法可行,但编译速度很慢(因为会不断实例化),而且容易出错。从Circle和P1858R2提案中得到启发,改进后的方法可以写成这样:

template<std::size_t N, typename... T>
auto getElement(T&&... args) -> decltype(auto) 
{
    // T...[N] -> 对类型包进行索引
    return args...[N];  // 对值包进行索引
}

这样不仅减轻了思维负担 —— 不用再绞尽脑汁写复杂代码了,对编译器性能也有好处。此外,一些提案中还提到了负索引(T...[-1]),用于获取参数包中的最后一个元素。这很方便,但也带来了一个问题,即如何区分 “合法的负索引” 和 “下溢情况”。有些语言(比如C#)引入了一个单独的操作符(C#中是^)用于反向索引。这对C++来说可能也是个可行的方向。

1.2 参数包切片

除了提取单个元素,获取子参数包的情况也很常见,比如跳过第一个类型,收集接下来的一组类型等等。目前,要实现这个功能,你得使用重叠模板展开或手动递归。一种更直接的切片语法可能是这样:

// 假设的切片语法
template <typename... Ts>
void example(Ts... args) {
    // 假设参数包是 [int, double, std::string, char]
    // 我们可以从索引1(包含)切片到3(不包含)。
    // 所以pack...[1:3] = [double, std::string]
    process(args...[1:3]...);
}

我们甚至可以将切片与展开结合起来,例如忽略第一个元素或跳过最后一个元素:

template <typename... Ts>
void ignoreFirst(Ts... args) {
    // 跳过第一个类型,传递其余的
    doSomething(args...[1:]...);
}

虽然切片看起来只是个小改进,但它能让类似元组的结构(std::tuple、std::variant、std::ranges::cartesian_product_view等)的库代码变得简洁得多,还有可能缩短编译时间。

1.3 结构体和类中的参数包

人们在处理std::tuple或std::variant时会用到私有继承技巧,原因之一是无法直接声明 “成员参数包”。一种直接的做法可能是这样:

template <typename... Ts>
struct my_tuple {
    // 声明一个成员参数包,每个成员的类型为Ts[i]
    Ts...elements;
    // 构造函数等
};

目前,这种写法在lambda表达式之外是不合法的。但如果可以的话,库的实现者就无需进行复杂的展开操作来存储每个元素了。这会影响默认构造或复制操作的写法,但在代码清晰度上是个巨大的提升。

这里有个示例:

template <typename... Ts>
struct my_variant {
    union {
        Ts...members;
    };
    std::size_t activeIndex = 0;
};

标准库的开发者们肯定会喜欢这种方式,因为它减少了内部模板的使用(继承变少、实例化减少、调试符号更简单),进而缩短编译时间。

2. 通用模板参数

有时候,你希望能把 “任何东西” 作为模板参数传递,比如类型、非类型,甚至是模板本身。这对于通用库来说尤为重要,因为通用库既要处理 “template <class T, auto SomeValue> class X”,也要处理 “template <class T> concept Y”。

考虑下面这个例子:

template <template <typename> concept C>
auto myFunction(C auto x) {
    return x + 1; // 或者任何C所要求的操作
}

这样就明确了C本身是一个概念模板参数。另一种情况是,你可能想传入一个变量模板。Circle语言支持template<template<typename...> auto V>这样的语法来实现。虽然通用模板参数看起来比较高级,但对于那些追求真正通用解决方案的库开发者来说至关重要,比如构建一个可以处理各种容器(从小型向量small_vector到新颖的静态映射static_map,每个容器都有不同的模板签名形式)的管道。

3. 反射与TMP的交叉

反射(如P1240R2等提案中所提及)主要是让你能够在编译期检查类型的成员或基类。这在诸如通用序列化、生成访问者代码或实现高级属性系统等任务中非常有用。但反射并不能解决所有问题。在日常的很多情况下,你只需要快速索引或部分展开。

这就是为什么我提倡逐步改进:先把基于参数包的小型、简单特性和索引操作符纳入标准,然后再看看反射如何与之互补。

一个有趣的协同方式是:

  • • 对一个类型进行反射,获取其数据成员列表。
  • • 用一些元算法转换这个列表。
  • • 将结果转换回参数包或元组。

如果语言没有直接支持对参数包的索引或切片展开,你可能需要编写复杂的多步代码。但如果反射能生成一个成员参数包,你就可以对其进行切片、重新排序,然后传递给一个模板函数等等。

4. 增强编译期三元运算符

虽然if constexpr在C++17中引入了编译期分支的重大变革,但对于那些必须在编译期短路的小表达式,我们仍然严重依赖立即调用的lambda表达式或std::conditional_t。Circle语言中有个不错的想法,即一种类似constexpr? :的编译期三元运算符。

看看这个例子:

template <std::size_t N>
constexpr auto pickValue() {
    // 我们需要一个编译期决策:只有一个分支是有效的
    return (N == 0) constexpr? 42
           : (N == 1) constexpr? 100
           : -1;
}

这是if constexpr的直接表达式形式类比。你无需担心分支具有相同的类型,或者在运行时是否有效。一个更强大的变体是基于参数包的多条件运算符:

int findTrueIndex(auto... booleans) {
    // 参数包中第一个为'true'的返回其索引,否则返回 -1
    return booleans...? int... : -1;
}

这会展开成类似if (b0) return 0; else if (b1) return 1;... else return -1;这样的链式结构,但写成了单个表达式。对于某些高级调度场景,这种写法非常优雅。

5. 改进诊断和编译期用户消息

5.1 更好的static_assert表达式支持

当编译期断言失败时,我们希望错误消息能更动态地引用失败的表达式。现在有强烈的呼声允许这样的写法:

template <std::string_view RegexPattern>
constexpr auto compileRegex() {
    //...
    static_assert(isValidRegex(RegexPattern),
                  std::format("Invalid regex '{}'", RegexPattern));
    //...
}

更棒的是,有人提议让static_assert可以出现在展开中,或者纯粹作为一个用于短路的表达式。例如:

template <class... Ts>
void checkComparisonOperators() {
    static_assert(requires { (std::declval<Ts>() < std::declval<Ts>()); },
                  "All Ts must define operator<")...; // static_assert的参数包展开
}

虽然这种强大的功能可能不会马上实现,但这是很多人希望看到的发展方向。

5.2 内置类型特性

我们经常依赖<type_traits>,但大量宏和函数模板带来的开销会不断累积。一种直接的 “成员特性” 方式(比如Circle语言中的T.remove_cvref)在日常编码中非常方便:

using Cleaned = decltype(T.remove_cvref);

即使我们永远无法实现类型特性的点语法,也很可能会看到一些扩展或特殊内置功能,它们编译速度更快,并且能产生更清晰的错误消息。

6. 用一个示例整合所有内容

下面是一个小代码片段,展示了这些提议的特性如何在假设的 “编译期反射/序列化” 场景中减少模板样板代码:

#include <iostream>
#include <string>

// 假设的未来C++:直接的参数包展开、切片等
struct Person {
    std::string name;
    int age;
    bool active;
};
template <typename T>
concept Reflectable = requires { T.[:] /* 如果T是一个“结构化”类型,我们可以对其成员进行索引或切片 */; };
template <Reflectable R>
void serialize(const R& r) {
    // 无需手动枚举每个成员,将它们展开成一个循环:
    auto [...fields] = r;  // 结构化绑定到一个参数包
    (..., (std::cout << fields << "\\n"));
}
int main() {
    Person p{"Alice", 30, true};
    serialize(p);
    return 0;
}

在 “当今的C++” 中,你得依赖std::tuple_size、std::tuple_element,或者专门的反射库来获取每个字段。如果要统一处理多个成员,可能还得创建一个局部访问者或使用递归。有了对反射参数包的直接索引和切片,或者有一个标准方法将聚合类型视为 “字段参数包”,这段代码会变得简单明了得多,也更易读。

7. 未来方向与结论

  • • 非尾随参数包:像P2347R2这样的提案允许你将参数包放在函数参数中间。结合切片/索引功能,对于高级函数签名来说会更加灵活。
  • • 返回参数包:我们是否应该从函数中返回参数包(而不是返回std::tuple)呢?有些人认为这在像保证返回值优化(guaranteed RVO)这样的特殊情况下可能会有帮助。但也有人对此表示怀疑。
  • • 反射协同效应:一旦反射被标准化,基于反射的元信息与直接参数包展开之间的协同效应将非常强大。你可以将一组反射得到的成员转换为普通参数包,对其进行操作(过滤、排序等),然后将其传递给一个模板函数或聚合类型,所有操作都能在一行内完成。
  • • 简化标准组件:我们最终可能会看到std::tuple、std::variant或某些并发抽象的重写,以利用这些新特性。库维护者们一直在寻找减少实例化和改善错误消息的方法。

最终,根本目标是让模板元编程不再那么高深莫测,而是成为一套简单直接的工具。即使你不以编写层层嵌套的库为生,这些变化也能在你涉足泛型编程时,带来更快的编译速度和更人性化的编译错误提示。

感谢阅读!希望对高级参数包处理、通用模板参数、反射交互以及改进的编译期条件的深入探讨,能让你看到未来C++标准的巨大潜力。通过逐步构建语言功能,我们可以将模板元编程从 “小众领域” 转变为 “日常实用工具”。以上就是我的分享。这些分析皆源自我的个人经验,希望上面分享的这些东西对大家有帮助,感谢大家!

参考文献及进一步阅读

 

原文作者:讳疾忌医-note;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2024-08-12

  • 博文量
    126
  • 访问量
    73132