C++26 抢先看~

新添加进 C++26 的新特性其实不少,毕竟最终版还在演进,今天八戒只提一下部分语言核心更新,比如一直以来都很受期待的反射 Reflection、合约 Contracts 等等,还有其它一些小更新扩展。

C++26 抢先看~

C++26 的提案出了好多份,预计在明年基本可以完成。无论是简中圈子还是墙外,网上已经有很多博主在预测这个版本的 C++ 将是下一个里程碑,在满怀激动的心情下,八戒和大家一起来体验一下 C++26 的最新特性。

新添加进 C++26 的新特性其实不少,毕竟最终版还在演进,今天八戒只提一下部分语言核心更新,比如一直以来都很受期待的反射 Reflection、合约 Contracts 等等,还有其它一些小更新扩展。

事不宜迟,开始吧~

反射 Reflection

八戒在之前的文章里尝试过以枚举值转换为字符串为例摸索实现反射的过程,这不,C++26 终于有了实现标准,下面的代码在初次看到时,你可能会觉得有些烧脑,但是我可以明确告诉你,万事开头难,看多几遍也会有眉目的。

提案中有个以枚举值转换为字符串的例子:

#include <iostream>
#include <experimental/meta>
#include <string>
#include <type_traits>

namespace __impl {
  template<auto... vals>
  struct replicator_type {
    template<typename F>
      constexpr void operator>>(F body) const {
        (body.template operator()<vals>(), ...);
      }
  };

  template<auto... vals>
  replicator_type<vals...> replicator = {};
}

template<typename R>
consteval auto expand(R range) {
  std::vector<std::meta::info> args;
  for (auto r : range) {
    args.push_back(std::meta::reflect_value(r));
  }
  return substitute(^__impl::replicator, args);
}

template<typename E>
requires std::is_enum_v<E>
constexpr std::string enum_to_string(E value) {
  std::string result = "<unnamed>";
  [:expand(std::meta::enumerators_of(^E)):] >>
    [&]<auto e> {
      if (value == [:e:]) {
        result = std::meta::identifier_of(e);
      }
    };
  return result;
}

int main() {
    enum Color { red, green, blue };
    std::cout << "enum_to_string(Color::red): "
      << enum_to_string(Color::red) << std::endl;
}

定义一个命名空间 __impl 来封装一些实现细节:

  • replicator_type 是一个模板结构体,接受一组参数 vals...。
  • operator>> 是一个模板成员函数,接受一个函数对象 body,并对其每个 vals 调用 body。
  • 定义一个 replicator 变量,它是 replicator_type 的实例。

定义即时函数(consteval)expand,接受一个范围 R 作为参数:

  • 创建一个 std::vector<std::meta::info> 类型的 args 向量。
  • 遍历 range 中的每个元素 r,并使用 std::meta::reflect_value 获取其反射信息,并将其推到 args 向量中。
  • 使用 substitute 函数将 __impl::replicator 替换为 args 向量中的内容,并返回结果,这里使用了一个很特别的操作符 ^

定义模板函数 enum_to_string,接受一个枚举类型 E 的参数 value。

  • 使用 requires std::is_enum_v<E> 约束,确保 E 是一个枚举类型。
  • 初始化 result 为
  • 使用 :expand(std::meta::enumerators_of(^E)): 扩展 E 枚举的所有枚举值。
  • 对于每个枚举值 e,如果 value 等于 e,则将 result 设置为 e 的标识符(通过 std::meta::identifier_of 获取),并返回 result。

合约 Contracts

关于合约,就是为函数接口添加了更多的限制,通过提供类似断言的检查,检查可以在函数调用前、函数执行过程中、函数调用返回之后。

下面看下提案里列举的一个例子,定义一个函数 f():

int f(const int x)
  pre (x != 1) // 前提断言
  post(r : r != 2) // 后置断言,r 指向接口返回值
{
  contract_assert (x != 3); // 断言
  return x;
}

int main()
{
  f(0); // 合法
  f(1); // 违反了接口 f 的合约前置条件
  f(2); // 违反了接口 f 的合约后置条件
  f(3); // 违反了接口 f 的合约断言
  f(4); // 合法

  return 0;
}

前提断言 pre 在函数 f() 执行之前检查,后置断言 post 在函数 f() 执行之后再检查,contract_assert 在函数内检查。

合约的意义是提高接口的非法使用门槛,增加安全性。

占位符 _

有些时候,有些变量的名字是没有意义的,也应该省去为其起名的麻烦。

所以 C++26 引入了占位符 _ (下划线)来代表匿名变量。

匿名变量比较适合的场合,比如以前在应用锁管理工具 lock_guard 管理互斥锁之时,往往会只声明对象,而从来不引用它:

void fun() {
    // 构造 lock_guard 时锁定互斥锁 mtx
    std::lock_guard<std::mutex> guard(mtx);
    // ...
}

在 fun() 函数中,变量 guard 被声明后就不需要再打理它,那么它的名字其实也就是无意义的了,刚好可以使用占位符代替。

名字的最大意义,是让人有所牵挂。

另外,匿名变量在后边将要介绍的结构化绑定中也有非常广泛的使用,可以先看看这个例子:

auto [data, _] = get_data();

不细说,往下看。

条件语句允许结构化绑定

结构化绑定是什么?看一个例子:

auto [position, length] = get_result(text, offset);

这里定义一个聚合类型对象,它包含两个子对象变量 position 和 length。这两个子对象变量和聚合类型对象的关系就是绑定,绑定提供了对聚合类型的解构,需要访问哪个元素就绑定哪个,大大简化代码的编写。

在之前的版本中,绑定只能在普通语句中定义,比如:

if (auto result = std::to_chars(p, last, 42))
{
​​​​    auto [ptr, _] = result;
​​​​    // okay to proceed
​​​​} 
else 
{
​​​​    auto [ptr, ec] = result;
​​​​    // handle errors
​​​​}

在上面的代码中,结构化绑定如果能放在 if 语句中,就可以简化后边的代码。

而 C++26 把结构化绑定拓展到了条件语句中,比如 if、while、for 等等,所以上面的示例代码可以改写成:

​​​​if (auto [ptr, ec] = std::to_chars(p, last, 42))
{
​​​​    // okay to proceed
​​​​} 
else 
{
​​​​    // handle errors
​​​​}

模板改进

模版一直是 C++ 元编程的核心,这次 C++26 也带来了不少更新改进,这里介绍一下特别令人关注的包索引,算得上是很有用的变更了。

包索引就是提供了索引,用于在输入多个参数的模版中指定引用某个序号的参数。

提案里有个例子:

template <typename... T>
constexpr auto first_plus_last(T... values) -> T...[0] {
  return T...[0](values...[0] + values...[sizeof...(values)-1]);
}

int main() {
  //first_plus_last(); // ill formed
  static_assert(first_plus_last(1, 2, 10) == 11);
}

T...[0] 表示引用第一个类型参数,values...[0] 表示引用 first_plus_last 被调用时输入的第一个参数。

static_assert 扩展

之前一旦断言报错,断言只会输出编译期指定的说明,C++26 之后用户可以输出自己的说明字符串了。

断言 static_assert 第一个参数依然是判断条件,第二个参数接收用户指定的说明字符串。当不符合条件并报错时,编译期会输出第二个参数的内容。

static_assert(sizeof(int) == 4,
  std::format("Expected 4, actual {}",
              sizeof(int)));

delete 扩展

自从 C++11 以来,我们可以对类的成员方法添加 delete 修饰。一旦被 delete 修饰后,该成员方法将不能被合法使用。

使用 delete 的目的是为了禁用类的某些功能,防止误用。

每个人的能力是有边界的,比如男人不应该声称自己可以怀孕,否则真的很荒谬啦。

提案里的示例代码:

class NonCopyable
{
public:
    // ...
    NonCopyable() = default;
    // copy members
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
    // maybe provide move members instead
};

这是一个禁止拷贝的类型,所以需要用 delete 修饰它的拷贝构造函数和拷贝赋值操作符。

如果用户在使用这个类型 NonCopyable 时不小心动用了它的拷贝相关能力,那么编译器就会识别到并在编译期输出错误信息,这些信息并不是太容易理解,导致很多人在看编译期的错误信息时都难以理解到底哪里出了问题。

于是,来到 C++26 之后,在修饰词 delete 后边允许添加用户的说明:

class NonCopyable
{
public:
    // ...
    NonCopyable() = default;
    // copy members
    NonCopyable(const NonCopyable&)
        = delete("Since this class manages unique resources, copy is not supported; use move instead.");
    NonCopyable& operator=(const NonCopyable&)
        = delete("Since this class manages unique resources, copy is not supported; use move instead.");
    // provide move members instead
};

关于 C++26 的新特性还有很多,八戒在这里只是和大家浅浅地一笔带过~

看完上面的内容,你心动了吗?关注我,带你了解更多!