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

60r*_*ogo 18 c++ c++-concepts

我有一个 CRTP 课程

template <typename T>
class Wrapper
{
  // ...
};
Run Code Online (Sandbox Code Playgroud)

旨在导出为

class Type : Wrapper<Type>
{
  // ...
};
Run Code Online (Sandbox Code Playgroud)

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

#include <concepts>

template <typename T>
  requires std::derived_from<T, Wrapper<T>>
class Wrapper
{
  // ...
};
Run Code Online (Sandbox Code Playgroud)

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

Wrapper() requires std::derived_from<T, Wrapper<T>>;
Run Code Online (Sandbox Code Playgroud)

但是如果我有更多的构造函数也必须受到约束,这就不方便了。我可以用析构函数来做

~Wrapper() requires std::derived_from<T, Wrapper<T>> = default;
Run Code Online (Sandbox Code Playgroud)

但是声明析构函数只是为了穿上它感觉有点傻requires

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

Qui*_*mby 5

不,这确实不可能。

现在这是一个语言问题 - 类的名称在实际写入代码之前并不存在。但即使 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{};
Run Code Online (Sandbox Code Playgroud)

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

一个简单的问题: 是Foo的基类吗Me?即 的值是多少std::is_base_of_v<Foo,Me>

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

这可能看起来像是一个人为的例子,但事实确实如此,毕竟你确实问了别的事情。

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

之前需要早期不完整性的另一个例子};是它的用法sizeof可能是一个更常见的参数:

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

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

    struct Me
    {
        int x[sizeof(Me)+1];
    };
    
    Run Code Online (Sandbox Code Playgroud)

    尺寸是多少Me

“朋友把戏”

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

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

struct Me
{
    int size(){
        return sizeof(Me);
    }
};
Run Code Online (Sandbox Code Playgroud)

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