C++反射下的简单重定位实现与探索
源作者:Barry Revzin
我对 C++中的反射功能感到兴奋的原因之一是,它允许你作为一个库来实现许多之前需要语言特性的功能。在这篇文章中,我将介绍如何实现 P2786R8(“C++26 的简单可重定位性”)。
或者,至少是简单重定位特性。库的内容就是基于此构建的。
这里的目标不是说设计是对还是错(尽管语法确实值得商榷),而是要展示反射能解决的问题类型。
我们将直接按照措辞将其翻译成代码:
简单可重定位类型 标量类型、简单可重定位类类型(11.2 [class.prop])、这些类型的数组以及这些类型的 cv 限定版本统称为简单可重定位类型。
这听起来确实像是一个类型特征!除了在反射的世界里,那些只是函数。我们该如何实现这样的功能呢?我们可以从以下方式开始:
consteval auto is_trivially_relocatable(std::meta::info type) -> bool
{
type = type_remove_cv(type);
return type_is_scalar(type) or (type_is_array(type)
and is_trivially_relocatable(
type_remove_all_extents(type) ))
or is_trivially_relocatable_class_type(type);
}
这是一个相当直接的翻译,其中 is_trivially_relocatable_class_type 是接下来要写的。但关于 type_remove_all_extents 类型特征(即 std::remove_all_extents)的一个有趣的事情是,它也适用于非数组类型,只是返回相同的类型。因此我们可以进一步简化它:
consteval auto is_trivially_relocatable(std::meta::info type)
-> bool
{ type = type_remove_cv(type_remove_all_extents(type));
return type_is_scalar(type) or is_trivially_relocatable_class_type(type);
}
好的,接下来。
注意,每个 std::meta::type*meow
函数都是直接翻译到 consteval
反射域中的类型特征 std::meow
(例如,type_remove_cv(type)
执行的操作与 std::remove_cv_t<type>
相同,只是前者接受一个 info 并返回一个 info,而后者接受一个类型并返回一个类型)。不幸的是,我们不能简单地引入它们,同时保留所有名称,因为一些名称冲突——is_function(f)
需要返回 f 是否是函数的反射,但类型特征 std::is_function<F>
检查 F 是否是函数类型。目前,我们的设计方案是给所有特征加上 type*
前缀,以便我们得到一些容易记住的东西。这还没有讨论过,所以命名约定可能还会改变。
符合简单重定位条件 除非一个类具有:
任何虚拟基类, 不是简单可重定位类的基类, 非静态数据成员是非引用类型且不是简单可重定位类型,
否则该类符合简单重定位条件。
这是另一个类型特征…呃,函数:
consteval auto is_eligible_for_trivial_relocation(std::meta::info type) -> bool
{ return std::ranges::none_of(bases_of(type),
[](std::meta::info b){
return is_virtual(b) or not is_trivially_relocatable(type_of(b));
})
and std::ranges::none_of(nonstatic_data_members_of(type),
[](std::meta::info d){
auto t = type_of(d); return not type_is_reference(t)
and not is_trivially_relocatable(t); });
}
这是另一个相当直接的翻译。我在第一种情况下使用 is_trivially_relocatable 而不是 is_trivially_relocatable_class_type,只是因为这样更短。你对 none_of()的调用是否比 any_of()的否定调用更易读可能会有所不同,特别是在非静态数据成员检查中。
下一个。
简单可重定位类 我们的最后一个术语是最复杂的一个: 如果一个类 C 符合简单重定位条件,并且
有一个类简单可重定位指定符,或者 是一个没有用户声明的特别成员函数的联合体,或者 满足以下所有条件: a. 当一个类型为 C 的对象从类型为 C 的 xvalue 直接初始化时,重载决议会选择既不是用户提供的也不是删除的构造函数,并且 b. 当一个类型为 C 的 xvalue 被赋值给一个类型为 C 的对象时,重载决议会选择既不是用户提供的也不是删除的赋值操作符,并且 c. 它有一个既不是用户提供的也不是删除的析构函数。
前言部分很直接,我们先把它解决掉:
consteval auto is_trivially_relocatable_class_type(std::meta::info type)
-> bool {
if (not is_eligible_for_trivial_relocation(type)) {
return false; }
案例 1 现在,在论文中,类简单可重定位指定符是上下文敏感关键字 memberwise_trivially_relocatable,你把它放在类之后。但这只会让你无条件地选择加入,而且它只是类名之后的漂浮词,所以我们这里要做的更好。
我们将引入一个注释(P3394),但我们还允许它有一个额外的 bool 值:
struct TriviallyRelocatable {
bool value;
constexpr auto operator()(bool v) const -> TriviallyRelocatable {
return {v}; }
};
inline constexpr TriviallyRelocatable trivially_relocatable{true};
这种设置意味着你可以这样使用它:
// true
struct [[=trivially_relocatable]] A { ... };
// 也是true,只是明确了
struct [[=trivially_relocatable(true)]] B { ... };
// false
struct [[=trivially_relocatable(false)]] C { ... };
注释设计让我们可以测试这个注释的存在。案例 1 就是使用它的值,如果提供了的话: annotations
consteval auto is_trivially_relocatable_class_type(std::meta::info type)
-> bool
{
if (not is_eligible_for_trivial_relocation(type)) {
return false;
}
// case 1
if (auto specifier = annotation_of<TriviallyRelocatable>(type)) {
return specifier->value;
}
// TODO
}
案例 2 好的,我们继续案例 2: 是一个没有用户声明的特别成员函数的联合体。
这用我们有的查询很简单:
// 案例1
if (auto specifier = annotation_of<TriviallyRelocatable>(type)) { return specifier->value; }
// TODO
}
consteval auto is_trivially_relocatable_class_type(std::meta::info type)
-> bool
{ if (not is_eligible_for_trivial_relocation(type)) {
return false; }
// 案例1 if (auto specifier = annotation_of<TriviallyRelocatable>(type)) {
return specifier->value;
}
// 案例2 if (type_is_union(type)
and std::ranges::none_of(members_of(type),
[](std::meta::info m){ return is_special_member_function(m)
and is_user_declared(m);
})) { return true; }
案例 3 第三种情况更复杂,因为它是以重载决议为条件的,而我们目前的反射设计中还没有做类似的事情:
满足以下所有条件: 当一个类型为 C 的对象从类型为 C 的 xvalue 直接初始化时,重载决议会选择既不是用户提供的也不是删除的构造函数,并且 当一个类型为 C 的 xvalue 被赋值给一个类型为 C 的对象时,重载决议会选择既不是用户提供的也不是删除的赋值操作符,并且 它有一个既不是用户提供的也不是删除的析构函数。
现在,能够通过既不是用户提供的也不是删除的构造函数从类型为 C 的 xvalue 初始化 C 意味着什么?这意味着它必须调用复制构造函数或移动构造函数。如果移动构造函数存在,那么检查这一点就足够了(因为那总是最佳匹配)。真正的问题案例是:
struct Bad {
Bad(Bad const&) = default; // 没有移动构造函数
template <class T>
Bad(T&&);
};
这个案例有一个默认的复制构造函数,它抑制了隐式的移动构造函数,但是从 Bad&&初始化 Bad 会调用转发引用构造函数,而不是复制构造函数。我们希望 Bad 拒绝案例 3,但我们没有特别干净的方法来做到这一点。我能想到的最好的方法是:
如果有移动构造函数,那么该移动构造函数是默认的。 否则,如果有复制构造函数,那么该复制构造函数是默认的,并且没有构造函数模板。 否则,为 false。
这当然不完全正确,但可能足够了。它肯定有误报(构造函数模板可能不适用于移动构造,它甚至可能不是一元的!),但它可能没有漏报。至少我想不出任何漏报。没有漏报就足够好——因为错误的正面反馈会很严重。
consteval auto is_trivially_relocatable_class_type(std::meta::info type)
-> bool
{
if (not is_eligible_for_trivial_relocation(type)) {
return false;
}
// case 1
if (auto specifier = annotation_of<TriviallyRelocatable>(type)) {
return specifier->value;
}
// case 2
if (type_is_union(type)
and std::ranges::none_of(members_of(type),
[](std::meta::info m){
return is_special_member_function(m)
and is_user_declared(m);
})) {
return true;
}
// TODO
}
对于赋值也有类似的技巧,只是我们简单地检查没有其他的赋值。我们可能可以更精确,但这已经是一个不错的启发式开始了。唯一的烦人部分是实际上积累所有特殊成员状态有点烦人。烦人,但可行:
consteval auto is_trivially_relocatable_class_type(std::meta::info type)
-> bool
{
if (not is_eligible_for_trivial_relocation(type)) {
return false;
}
// case 1
if (auto specifier = annotation_of<TriviallyRelocatable>(type)) {
return specifier->value;
}
// case 2
if (type_is_union(type)
and std::ranges::none_of(members_of(type),
[](std::meta::info m){
return is_special_member_function(m)
and is_user_declared(m);
})) {
return true;
}
// case 3
std::optional<std::meta::info> move_ctor, copy_ctor,
move_ass, copy_ass,
dtor;
std::vector<std::meta::info> other_ctor, other_ass;
for (std::meta::info m : members_of(type)) {
// ... update that state ...
}
auto is_allowed = [](std::meta::info f){
return not is_user_provided(f)
and not is_deleted(f);
};
auto p31 = [&]{
if (move_ctor) {
return is_allowed(*move_ctor);
} else {
return copy_ctor
and is_allowed(*copy_ctor)
and other_ctor.empty();
}
};
auto p32 = [&]{
if (move_ass) {
return is_allowed(*move_ass);
} else {
return copy_ass
and is_allowed(*copy_ass)
and other_ass.empty();
}
};
auto p33 = [&]{
return dtor and is_allowed(*dtor);
};
return p31() and p32() and p33();
}
我在这里使用了 lambda,因为我认为这是一种稍微更有表现力的方式来展示三个子项目,而不会失去延迟评估。 结论 最终,我不能精确地实现 P2786 中的设计。最后的启发式是基于重载决议的,这是我们目前还不能在反射设计中做到的。但我可能足够接近实际使用,大约需要 125 行代码(namespace N 的内容)。与设计的另一个区别是,由于提供选择加入和选择退出都很容易,所以我都做了。
现在,很多库都有一些实现简单重定位的方法,有些选择加入或选择退出。所以让像 std::unique_ptr
// 一个类似unique_ptr的类型,必须选择加入
// 才能成为简单可重定位的,因为它有
// 用户提供的移动操作和析构函数
class [[=N::trivially_relocatable]] C {
int* p;
public:
C(C&&) noexcept; C& operator=(C&&) noexcept;
~C();
};
// 没有注释则为false static_assert(N::is_trivially_relocatable(^^C));
但是让这样的类型(正确地)自动成为简单可重定位的,而不需要任何注释,这是完全新的:
struct F {
int i; C c;
};
static_assert(N::is_trivially_relocatable(^^F));
总的来说,我认为这是反射能提供的力量的一个相当酷的展示,以及为什么我对它作为一个语言特性如此兴奋。