C++20 概念:如何在"requires"子句中引用类名?

C++20 concepts: how to refer to the class name in the `requires` clause?

本文关键字:quot 引用 子句 requires C++20 概念      更新时间:2023-10-16

我有一个 CRTP 类

template <typename T>
class Wrapper
{
// ...
};

旨在派生为

class Type : Wrapper<Type>
{
// ...
};

我想通过对模板参数T施加约束来强制执行这一点。有一个friend技巧可以做到这一点,但我认为在概念时代应该有更好的方法。我的第一次尝试是

#include <concepts>
template <typename T>
requires std::derived_from<T, Wrapper<T>>
class Wrapper
{
// ...
};

但这不起作用,因为我指的是Wrapper在声明之前。我发现一些不完全令人满意的解决方法。我可以将约束添加到构造函数

Wrapper() requires std::derived_from<T, Wrapper<T>>;

但是如果我有更多的构造函数也必须受到限制,那就不方便了。我可以用析构函数做到这一点

~Wrapper() requires std::derived_from<T, Wrapper<T>> = default;

但是声明析构函数只是为了requires它,感觉有点愚蠢。

我想知道是否有更好,更惯用的方法可以做到这一点。特别是,虽然这些方法似乎有效(在 gcc 10 上测试过(,但一件不令人满意的事情是,如果我从Wrapper<OtherType>中派生Type,那么只有在我实例化Type时才会引发错误。在定义Type时是否有可能出现错误?

不,这真的是不可能的。

现在这是一个语言问题 - 类的名称在实际写入代码之前不存在。但是,即使C++编译器多次读取文件并知道名称,仍然不够。允许这样做要么需要对类型系统进行重大更改,而不是为了更好,要么充其量是一个非常脆弱的功能。让我解释一下。

假设可以在requires子句中提及该名称,则代码也会失败,因为此时T=Type仍然是不完整的类型。 @Justin在他值得注意的评论中证明,我的回答是建立在这种基础上的。

但是,为了不让它在这里结束并成为一个非常无聊的"你不被允许这样做"的版本,让我们问问自己为什么Me首先是不完整的?

看看下面这个相当人为的例子,就会发现在其基类中不可能知道完整的Me类型。

#include <type_traits>
struct Foo;
struct Bar{};
template<typename T>
struct Negator {
using type = std::conditional_t<!std::is_base_of_v<Foo,T>, Foo, Bar>;
};
struct Me: Negator<Me>::type{};

当然,这只不过是罗素悖论C++版本,它证明了定义良好的类型/集合不能使用自身来定义。

一个简单的问题:FooMe的基类吗?即std::is_base_of_v<Foo,Me>的价值是什么?

  • 如果不是,则Negator中的条件为真,因此Me是从Negator<Me>::type派生的,即Foo这是一个矛盾。
  • 另一方面,如果它确实来自Foo,我们发现它实际上不是。

这似乎是一个人为的例子,毕竟你确实问了别的东西。

是的,您可能在标准中添加有限数量的段落以允许对您的Wrapper的特定使用并禁止我对Negator的使用,但是必须在这些不太相似的例子之间画一条非常细的线。

};之前需要早期不完整的另一个例子是sizeof的使用,这可能是一个更常见的论点:

  • sizeof(T)显然取决于所有基类的大小。因此,在派生类型的基类Wrapper中使用仍在编写的表达式T是另一个地雷等待您踩踏。

  • 不过,没有任何继承的更简单的例子是:

    struct Me
    {
    int x[sizeof(Me)+1];
    };
    

    Me的大小是多少?

"朋友把戏">

我相信您说的是防止用户从不正确的 CRTP 基数派生。是的,这有效,但出于同样的原因,您将requires放在有效的方法附近。只有在实际生成其调用时,才会检查要删除或无法访问的构造函数,这通常仅在创建实例时进行检查,此时Me是完整类型。

这样做也是有充分理由的,您希望此代码正常工作:

struct Me
{
int size(){
return sizeof(Me);
}
};

方法的存在不能影响Me的类型,所以这不会产生任何问题。