Clang下含volatile拷贝赋值运算符的类数组高效初始化难题
现有如下类定义:
class A { public: constexpr A(int v) : v(v) { /* logic */}; A& operator=(const A&) = default; // 错误:显式默认的拷贝赋值运算符参数不能是volatile // A& operator=(const volatile A&) = default; // 该函数会禁用通过全局常量memcpy初始化A的优化 A& operator=(const volatile A& a) { v = a.v; return *this; } int v; }; void f() { // 需求1:大数组的快速常量初始化 A a[20] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}; // 需求2:支持从volatile对象赋值 volatile A b(1); a[0] = b; }
需要实现两个目标:
- a) 确保
f()中的代码能编译,尤其是支持从volatile对象赋值(已有大量用户依赖该特性,无法轻易移除); - b) 让
A类型的大数组初始化更高效:移除volatile版本的operator=时,Clang会创建全局常量并使用memcpy优化,但添加该重载后优化失效,且Clang仅对平凡可拷贝类型执行此优化。
尝试显式默认A& operator=(const volatile A& a),但Clang不支持该语法。查看Clang源码(CGExprAgg.cpp中的AggExprEmitter::EmitArrayInit)发现,优化触发的前提是类型为平凡可拷贝。
请问:
- 是否存在可行的代码写法,能同时满足上述两个需求?
- 如果没有可行写法,修改Clang使其在拷贝构造函数平凡时,即便类非平凡可拷贝也执行常量全局初始化优化,是否符合C++标准?
一、可行的代码写法:用SFINAE分流重载
可以借助SFINAE技术,给volatile和普通对象分别提供赋值重载,同时保留默认赋值运算符的平凡性,让编译器能识别出类型的可优化特性。
修改后的代码如下:
#include <type_traits> class A { public: constexpr A(int v) : v(v) { /* logic */}; A& operator=(const A&) = default; // 只有当右值是volatile限定的A时,这个重载才会生效 template <typename T, typename = std::enable_if_t<std::is_same_v<std::remove_cv_t<T>, A> && std::is_volatile_v<T>>> A& operator=(const T& a) { v = a.v; return *this; } int v; };
为什么这样可行:
- 默认的
operator=(const A&)保持默认状态,编译器会把它认定为平凡拷贝赋值运算符,加上A的拷贝构造函数是编译器自动生成的平凡构造,所以A仍然是平凡可拷贝类型; - 模板重载只会匹配
volatile A类型的右值,满足从volatile对象赋值的需求; - 因为
A还是平凡可拷贝类型,Clang会照常对大数组初始化做memcpy优化,两个需求同时满足。
二、修改Clang的合理性与标准符合性
如果不想用上面的代码写法,修改Clang让它在拷贝构造函数平凡时,哪怕类不是完全的平凡可拷贝类型也执行常量全局初始化优化,是符合C++标准的,原因如下:
常量初始化的本质:像
A a[20] = {0,1,...}这种大数组初始化属于聚合初始化,每个元素都通过constexpr构造函数初始化,最终能生成一个常量表达式对应的内存值。编译器完全有权把这种初始化优化成直接拷贝预定义的全局常量内存——只要优化后的结果和逐元素构造的结果完全一致。as-if规则的允许:C++标准允许编译器做任何不改变程序可观察行为的优化(也就是as-if规则)。如果拷贝构造函数是平凡的,那么逐元素构造出来的对象内存布局,和直接拷贝常量内存的结果没有区别,所以这种优化完全符合as-if规则,不违反标准。
当前Clang的限制是实现选择:Clang现在只对平凡可拷贝类型做这个优化,是它自己实现层面的限制,不是标准强制要求的。把触发条件放宽到“拷贝构造函数平凡”,只要保证优化后的行为和原代码一致,就没问题。
内容的提问来源于stack exchange,提问作者Matthias Gehre

