C++反射下的简单重定位实现与探索

文摘   2024-10-23 07:41   江苏  

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 符合简单重定位条件,并且

  1. 有一个类简单可重定位指定符,或者
  2. 是一个没有用户声明的特别成员函数的联合体,或者
  3. 满足以下所有条件: 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));

总的来说,我认为这是反射能提供的力量的一个相当酷的展示,以及为什么我对它作为一个语言特性如此兴奋。


编程悟道
自制软件研发、软件商店,全栈,ARTS 、架构,模型,原生系统,后端(Node、React)以及跨平台技术(Flutter、RN).vue.js react.js next.js express koa hapi uniapp Astro
 最新文章