当前位置: 首页 > ai >正文

C++ 的 trivially relocatable

1 起源

​ C++ 对象有两种构造方式,一种是拷贝构造,一种是移动构造。一般理解,移动构造有更好的性能(但是也不一定),但是对于某些极其压榨性能的场合,移动构造也不入眼。因为移动构造也是有开销的,并且对象移动之后,源对象依然是合法状态,它还需要析构,在一些情况下,这样标准的移动操作完全没必要。存在比移动更快的方式,那就是按位复制,只需将对象的数据复制到另一个地方,就可以在那个地方“还原”出这个对象,这可比普通的移动更有吸引力。

​ 按照现行 C++ 规范,一块未初始化的存储位置必须通过构造函数才被视为一个有效的对象,同时,由于 C++ 对象模型的复杂性,随便按位复制和移动是危险的,对任意 C++ 对象取地址然后 memcpy 或 memmove 它到其他位置是未定义行为(UB)。但是有一类对象支持将自身复制到其他内存位置,然后再复制回来,同时能保证对象的值不变。这就是现在 C++ 标准中已经支持的平凡可复制 (TriviallyCopyable) 类型。平凡可复制类型的定义是这样的:

  • 标量类型
  • 平凡可复制的类类型
  • 以上(平凡可复制)类型的数组
  • 带有 CV 限定的以上(平凡可复制)类型

第一条标量类型很容易理解,第二条所说的平凡可复制的类类型,必须同时满足以下条件:

  • 至少有一个合格的拷贝构造函数、移动构造函数、拷贝赋值运算符或移动赋值运算符
  • 每个合格的拷贝构造函数都是平凡的
  • 每个合格的移动构造函数都是平凡的
  • 每个合格的拷贝赋值运算符都是平凡的
  • 每个合格的移动赋值运算符都是平凡的
  • 有一个平凡的析构函数,且不能是标记为 delete

标准库提供了一个类型特征 std::is_trivially_copyable 用于判断类型是否是平凡可复制,满足平凡可复制类型的对象,将其按位复制到一个 char 或 std::byte 数组上,再复制回来,对象的值不变。

​ 但是,我们其实并不想复制这个对象,而且也不是想移动这个对象,因为移动完了源对象还要销毁,没必要。我们只是想把对象数据搬运到另一个地方,源位置的对象结束的生命周期(lifetime)转移到新位置上的对象上,析构函数也不必调用(P2786 语义)。为了区别于现在的移动语义,我们将这样的操作称为重定位(relocate)。对于重定位操作如果用平凡可复制的条件要求就显得太严格了,比如一些对象提供了自定义的移动构造或移动赋值运算符,就不符合这个条件了,它们其实是可以按位复制的。如果我们对不满足平凡可复制,但是实际可以按位复制的对象使用了 memcpy,会导致 UB。在这些情况下,通常什么都不会发生,但是 UB 就是 UB,伤害性不大,侮辱性极强。

2 平凡重定位(trivially relocatable)

​ 需要解决的问题非常简单,实际上,一些类的设计者已经开始这么做了,他们通常提供一些按位交换的方法让对象移动位置,但是从语言规范上看,这应该是 UB。所以,需要新定义登场,就是定义一种新的平凡重定位类型,然后修改标准中的措辞,就说这种类型的按位交换不是 UB 就好了。修改措辞这事儿,C++ 也不是第一次这么干了。比如那个 void,将其定义为字面类型,于是 无返回值的函数就也可以是常量函数了,这就是定义学。

​ 一个类在满足以下条件时可以被平凡重定位(trivially relocatable):

  1. 没有虚基类;
  2. 所有基类都是可平凡重定位的类;
  3. 所有非静态数据成员的类型都是可平凡重定位的;
  4. 没有标记为 delete 的析构函数。

但以下情况由实现定义(implementation-defined):

  • 如果一个联合体(union)本身满足上述条件,但包含一个或多个多态类类型的子对象,则该联合体是否可平凡重定位由具体实现决定。

联合体是否可平凡重定位由具体实现决定,目前看实际的约束就是不能有用户声明的特殊成员函数。另外,满足可平凡重定位的类都是默认可移动的,毕竟语意上不支持移动的类却可以重定位,总觉得哪里不对劲吧?此外,也可以使用类属性说明符:trivially_relocatable_if_eligible 说明一个类支持平凡重定位,但不是必须的,编译器会自己推断。从定义看,平凡重定位的要求比平凡可复制宽松(不要求拷贝构造和拷贝赋值运算符也是平凡的)。

​ 支持平凡重定位有什么好处?像 std::vector 这样的连续容器,在删除元素的时候会有大量移动对象的操作,如果对象是平凡重定位类型,就可以直接按位交换的方式将对象重定位到一个新位置上,这样会有更好的性能。如果对象不是平凡重定位类型,则使用普通的移动+销毁的方式完成对象的移动,性能一般。

3 P2786 和 P1144 提案之争

​ 为了引入平凡重定位,在标准制定过程中先后涌现出多个提案,其中影响力最大的两个就是 P2786 和 P1144 两个提案。两个提案在 EWG 极限拉扯,P1144 更新到 R12,P2786 改进到 R13,最终在长期争吵之后,P2786 终于在 2025 年 2 月被采纳,最终进入 C++ 26。其实 P2786 也是一波三折,甚至出现了 2024 年 2 月(Tokyo)被投进 CWG,但是在同年 6 月(St Louis)又被退回的尴尬情况,过程十分艰辛,可见对两个提案的争吵有多激烈。

​ P2786 要求可平凡重定位的对象不能有用户定义的移动操作和析构函数,也就是说,只有同时满足 trivially_move_constructible 和 rivially_destructible 的才可以用按位复制的方式重定位,否则重定位操作就退化成在新位置移动构造,然后旧位置的对象析构销毁的方式。当使用按位复制的方式重定位对象的时候,需要转移对象生命周期的管理,为此需要语言核心(Core-language)支持一种新类型,就是平凡重定位类型,并且对象的生命周期管理模型也必须扩展,以便支持对平凡重定位类型进行重定位操作。

​ P1144 提案认为只要是平凡可复制的类型,就可以看作是平凡可重定位,也就是说,平凡可复制是平凡可重定位类型的子集。这是个保守的方案,倾向于不改变语言核心,只是将许多行业代码库已经使用的易于理解的技巧合法化,好处就是不破坏现有的库的 API 和 ABI 的向后兼容性。这些所谓的行业代码库包括但不仅限于 BSL 的 bslmf::IsBitwiseMoveable、folly 库的 folly::IsRelocatable(folly::fbvector 支持)、Abseil 库的 absl::is_trivially_relocatable、QT 库的 QTypeInfo::isRelocatable(QList 支持)。P1144 提案对已经存在的这些库的实现是一种补充,不改变现有代码的实现,所以得到了这些库的作者们的强烈支持。资料 [6] 的 P3236 提案可以看作是这些库的作者们的一次“逼宫”,它们要求 WG21 工作组拒绝 P2786,选择 P1144。

​ 资料 [9] 是 P1144 作者不甘心的体现,它介绍了 P1144 的朋友圈,暗示没人使用 P2786 的语义。真是这样吗?clang 据说在 2022 年就内置支持平凡重定位,而那时 P2786 还没有发布。不过, P1144 作者自己也承认,clang 自己的实现更接近 P2786 的语义。2024 年,Abseil、Qt、AMC、Subspace、HPX 库的作者们联合其他几个小的库的作者们一起给 clang 提了一个 pull request, 试图让 clang 的 __is_trivially_relocatable(T) 支持 P1144 的语义,但是被拒绝了。

4 Pre C++ 26 体验

​ 截至此文时间(2025 年 4 月),还没有编译器完全支持 C++ 26,不过 clang 编译器提供了 P1144 和 P2632(P2786 的前身) 提案预览版,就在 Compiler Explorer 上可以体验一下(编译器选择 x86_64 clang (experimental P1144))。

​ 首先定义 Foo,我们人为给它加上一个析构函数,这样它就不满足平凡重定位要求:

struct Foo {~Foo() { }int kknd;double dbVal;std::string strVal;
};

接着我们用 std::vector::resize() 做测试,用一个循环依次增加大小,循环 100000 次,这是测试代码:

int main() {static_assert(!std::is_trivially_relocatable_v<Foo>);std::vector<Foo> fv;auto start = system_clock::now();  for(std::size_t i = 0; i < 100000; i++) {fv.resize(i * 2);}system_clock::duration diff = system_clock::now() - start;auto elapse = duration_cast<milliseconds>(diff).count();std::cout << "time elapse: " << elapse << " ms" << std::endl;
}

静态断言为了确定 Foo 不符合平凡重定位,此时测试 100000 次 resize 需要的时间在 24ms - 40ms 之间(因为是在 Compiler Explorer 网站上测试,时间波动范围比较大)。如果我们注释掉 Foo 的析构函数,这样它就满足平凡重定位(可修改 static_assert 测试断言确认),此时测试 100000 次 resize 需要的时间在 15ms - 32ms 之间,可见性能提升的效果还是非常显著的。

​ 如果使用 P2632 预览版时需要注意,因为此时的提案用的类专属说明符的名字还是 memberwise_trivially_relocatable 和 memberwise_replaceable。

参考资料

[1] https://en.cppreference.com/w/cpp/named_req/TriviallyCopyable

[2] https://en.cppreference.com/w/cpp/types/is_trivially_copyable

[3] CWG 2094: Trivial copy/move constructor for class with volatile member

[4] P2786: Trivial Relocatability For C++26

[5] P1144: std::is_trivially_relocatable

[6] P3236: Please reject P2786 and adopt P1144

[7] P2814: Trivial Relocatability — Comparing P1144 with P2786

[8] P3233: Issues with P2786

[9] Arthur O’Dwyer, Who uses P2786 and P1144 for trivial relocation?

[10] KDAB Group, Qt and Trivial Relocation (Part 1): What is relocation?

[11] Arthur O’Dwyer, Should the compiler sometimes reject a [[trivially_relocatable]] warrant? 2023

[12] Arthur O’Dwyer, STL algorithms for trivial relocation

[13] https://stackoverflow.com/questions/59422888/can-we-detect-trivial-relocatability-in-c17

关注作者的算法专栏
https://blog.csdn.net/orbit/category_10400723.html

关注作者的出版物《算法的乐趣(第二版)》
https://www.ituring.com.cn/book/3180

http://www.xdnf.cn/news/1838.html

相关文章:

  • 解决ssh拉取服务器数据,要多次输入密码的问题
  • PyTorch 实现食物图像分类实战:从数据处理到模型训练
  • 植物合成生物学:上帝之手,万物皆可合
  • 【MQ篇】RabbitMQ的消费者确认机制实战!
  • 【金仓数据库征文】金仓数据库:开启未来技术脑洞,探索数据库无限可能
  • 脚本批量启动Node服务器
  • 【金仓数据库征文】_AI 赋能数据库运维:金仓KES的智能化未来
  • 复杂地形越野机器人导航新突破!VERTIFORMER:数据高效多任务Transformer助力越野机器人移动导航
  • 计算机组成原理第二章 数据的表示和运算——2.1数制与编码
  • HTMLcss实现网站抽奖
  • Ubuntu 下 Nginx 1.28.0 源码编译安装与 systemd 管理全流程指南
  • 本地使用Ollama部署DeepSeek
  • 30天通过软考高项-第三天
  • redis 数据类型新手练习系列——string类型
  • 【Java并发】【LinkedBlockingQueue】适合初学体质的LinkedBlockingQueue入门
  • 计算机组成原理-408考点-数的表示
  • 全面解析 MCP(Model Context Protocol):AI 大模型的“万能连接器”
  • 通讯录完善版本(详细讲解+源码)
  • 基于归纳共形预测的大型视觉-语言模型中预测集的**数据驱动校准**
  • 使用 硅基流动+Doris+DeepSeek搭建RAG知识库(保姆级教程)
  • XBIT以创新技术引领币圈十大APP,开启数字货币交易新时代
  • SpringBoot 学习
  • 基于STM32的大棚温度环境调控系统设计方案
  • 2015-2023 各省 GDP 数据,用QuickBI 进行数据可视化——堆叠图!
  • 基于HTML+CSS实现的动态导航引导页技术解析
  • 聚客AI手把手实战:用LlamaIndex+代码实现亿级数据的智能问答系统
  • 【C++指南】告别C字符串陷阱:如何实现封装string?
  • 深入浅出Sentinel:分布式系统的流量防卫兵
  • 5.3 Dify:低代码平台,适用于企业快速部署合规AI应用
  • Linux系统中命令设定临时IP