如何不修改现有构造函数让非拷贝移动类成为隐式生存期类型?
直接引用C++23规范第11.2节:
9 A class S is an implicit-lifetime class if (9.1) — it is an aggregate whose destructor is not user-provided or (9.2) — it has at least one trivial eligible constructor and a trivial, non-deleted destructor.
但这里的“至少一个”指的是什么?听起来像是类中任意一个构造函数都算。不过规范同时指出,平凡构造函数必须满足:
Implicitly declared or explicitly defaulted (= default) on its first declaration.
由于只有特殊成员函数能满足上述条件,严格来说只有特殊成员函数可以是平凡构造函数。因此我初步判断答案是不可能,但不确定是否遗漏了标准中的细节,想确认这一点。
先看一个简化的现有类示例:
template <typename T> struct Foo { T t; Foo() : t() { } Foo(Foo const &) = delete; Foo(Foo &&) = delete; }; static_assert(not std::is_trivially_default_constructible_v<Foo<int>>); static_assert(not std::is_trivially_copy_constructible_v<Foo<int>>); static_assert(not std::is_trivially_move_constructible_v<Foo<int>>);
我们希望当T满足隐式生存期要求时,Foo的实例也能用于要求隐式生存期类型的场景,但目前Foo并非隐式生存期类型。
于是尝试添加一个构造函数:
template <typename T> struct Foo { // ... 原有成员 Foo(tag_t) { } // 寄希望于这个构造函数能生效... };
实际上,Foo<int>::Foo(tag_t)的行为和平凡构造函数类似(就像Foo() {}一样),但严格来说它并不是平凡构造函数。
另外,P2674中std::is_implicit_lifetime的参考实现只检查默认/拷贝/移动构造函数的平凡性,这是因为检查其他构造函数需要编译器支持。不过标准本身提到“任意”构造函数,而且编译器能看到这个构造函数不执行任何操作,理论上可以检测到存在一个等效的平凡构造函数——我对此抱有一丝希望,但猜测实际并非如此,目前主流编译器的正式版本也都未支持这种特性。
那么问题来了:在默认/拷贝/移动构造函数都不平凡的情况下,如何在不修改现有构造函数(避免破坏现有代码)的前提下让Foo成为隐式生存期类型?
补充说明:这不是我自己写的类,是帮朋友解决的问题。标准库中已经存在至少一个类似的类,大量现有代码都把它当作隐式生存期类型使用,但严格来说它并不是。我希望不用提交标准提案就能解决这个问题。
编辑
原本不想涉及具体类型,只想基于标准探讨解决方案,但大家都在问细节,这里就简要说明:
在C++20中,std::atomic的默认构造函数被修改为强制零初始化。这个改动提升了多数场景的安全性,但意外导致std::atomic失去了平凡默认构造函数和隐式生存期特性,使其不再适用于共享内存场景。
现在在共享内存中使用std::atomic<int>会导致未定义行为。虽然std::atomic_ref提供了一种临时解决方案,但牺牲了封装性和类型安全性。尽管标准在无锁操作的规范中认可了共享内存的使用场景,但目前并未提供专门用于共享内存原子操作的类型。
我认为这是当时的一个疏漏。P0883提案原本只是确保std::atomic<int> i{}会被零初始化,但后来进一步将所有std::atomic类型的默认初始化改为零初始化,而非原来的默认初始化行为。
std::atomic的拷贝/移动构造函数从一开始就被删除了,这是合理的。而在C++20中,即使T是int,其默认构造函数也不再是平凡的——这正是上述Foo示例对应的真实场景。
内容的提问来源于stack exchange,提问作者Jody Hagins

