【问题标题】:Can a concept be checked against incomplete type可以根据不完整类型检查概念吗
【发布时间】:2021-08-29 07:54:17
【问题描述】:

我偶然发现了这个:

#include <type_traits>
#include <concepts>

template<class T>
concept IsFoo = requires(T a)
{
    {a.a} -> std::same_as<int>;
};

#if 1
// Will not compile, because Foo currently has incomplete type
template<IsFoo AFoo>
struct AcceptsFoo
{};
#else
template<class AFoo>
struct AcceptsFoo
{};
#endif

struct Foo
{
    int a;
    int b;

    AcceptsFoo<Foo> obj;
};

https://gcc.godbolt.org/z/j43s4z

其他变体(crtp)https://gcc.godbolt.org/z/GoWfhq

Foo 是不完整的,因为它必须实例化AcceptsFoo,但要这样做,Foo 必须是完整的,否则它无法检查IsFoo。这是 GCC 中的错误,还是标准这么说?后者会很糟糕,因为这会阻止概念与一些众所周知的模式(例如 CRTP)一起使用。

我注意到 clang 确实给出了类似的错误:https://gcc.godbolt.org/z/d5bEez

【问题讨论】:

    标签: c++ c++20 c++-concepts


    【解决方案1】:

    您可以针对不完整的类型检查一个概念 - 但如果该概念检查实际上需要对需要它完成的类型执行任何操作,那么概念检查将失败。

    即使在那里,您也必须小心,因为允许(并且将)缓存概念检查以更快地编译 - 因此,如果您尝试 C&lt;T&gt;T 不完整,并在 T 完成时重试,而那些应该给出不同的答案,你是在自找麻烦。

    Foo 在您检查它时并不完整,因此它自然没有名为a 的成员。这真的不可能。

    因为这会阻止概念与一些众所周知的模式(例如 CRTP)一起使用。

    以这种方式一起使用,是的。与使用 CRTP 一样,您也无法直接从传递给基类的模板参数中访问任何内容,您始终必须小心延迟该类型的任何实例化,直到它完成。

    这最终是同一个原因:

    template <typename Derived>
    struct B { 
        typename Derived::type x;
    };
    
    struct D : B<D> {
        using type = int;
    };
    

    不工作。

    【讨论】:

    • "所以 自然 它没有名为 a" 的成员(强调我的)。对于某些部分(可能确实只在类定义中),“under definition” 可以看到已经定义的部分(但不能稍后)(我已经不得不将成员从下到上移动以修复代码(@987654329 @返回类型例如))。所以AcceptsFoo&lt;Foo&gt; 看到已经定义的成员可能是合理的(即使是错误的)。 (CRTP 会有问题,它是定义的第一部分,所以无论如何都看不到任何东西)。
    • "因此,如果您在 T 不完整时尝试 C,并在 T 完成时重试,而那些应该给出不同的答案,您就是在自找麻烦。"这似乎是标准在找麻烦,如果完整类型可以给出不同的结果,为什么还要允许对不完整类型进行概念检查?换句话说:是否有任何用例可以提供帮助?
    • @NoSenseEtAl 这不是完整与不完整的问题 - 程序中的一个点之间的类型可以改变很多事情。如果您有一个检查类似...foo(t) 的概念,那么HasFoo&lt;T&gt; 在某一时刻可能是false,然后添加了一个重载,然后它变成了true。完整/不完整只是其中一个例子,可能不是最常见的例子。
    【解决方案2】:

    [编辑] 这在 g++ 10.3、11.2 和当前的 clang 上按预期工作。一些 cmets 表明这是未定义的行为,因此请注意未来编译器可能出现的意外更改。

    原解决方案:

    我也只是偶然发现了这一点,并通过允许类型不完整或具有所需约束的完整,成功地使我的 CRTP 与概念一起工作。

    除了定义的IsFoo,我还定义了IsComplete helper(使用sizeof技巧),最后,IsFooIncomplete以如下方式:

    template<class T> 
    concept IsFooIncomplete = !IsComplete<T> || IsFoo<T>;
    

    这样我可以确保在 Foo 的处理过程中,它是不完整的,并且在 class 完成后,它是完整的并且匹配所需的 IsFoo 约束。

    #include <concepts>
    #include <type_traits>
    
    template<class T>
    concept IsFoo = requires(T self)
    {
       {
          self.a
          } -> std::same_as<int&>;
    };
    
    template<class T>
    concept IsComplete = requires(T self)
    {
       {
          // You can't apply sizeof to an incomplete type
          sizeof(self)
       };
    };
    
    template<class T>
    concept IsFooIncomplete = !IsComplete<T> || IsFoo<T>;
    
    #if 0
    // Will not compile, because Foo currently has incomplete type
    template<IsFoo AFoo>
    struct AcceptsFoo
    {};
    #else
    // will compile with IsFooIncomplete
    template<IsFooIncomplete AFoo> // no need to use 'class AFoo' here...
    struct AcceptsFoo
    {};
    #endif
    
    struct Foo
    {
       int a;
       int b;
    
       // Foo is incomplete here, but that's fine!
       static_assert(!IsComplete<Foo>);
       AcceptsFoo<Foo> obj;
    };
    
    // Foo is now complete, and that's also fine!
    static_assert(IsFoo<Foo>);
    

    g++ 版本10.3.0(带有标志--std=c++20)上运行良好,我希望它也适用于其他编译器。

    [EDIT] 正如 cmets 中所指出的,这接受任何不完整的类型,但这是有意的。只有外部静态断言会过滤完整的类型案例。感谢@David Herring 在 Bar 上的示例,我在这里写了它以进行测试:https://godbolt.org/z/sqc75qqMv


    [EDIT2] 现在它处理 CRTP 变体,没有任何未定义的行为,也没有 IsComplete 解决方法,只需存储类型以在类完成后进行检查。

    // Will not compile, because Foo currently has incomplete type
    template<IsFoo AFoo>
    struct AcceptsFoo
    {};
    #else
    template<class AFoo> // will not check IsFoo directly here...
    struct AcceptsFoo
    {
        using IsFooType = AFoo; // will store type on IsFootType for later checks
    };
    #endif
    
    struct Foo : public AcceptsFoo<Foo> // will check only when complete
    {
        int a;
        int b;
    };
    
    // Foo is now complete, and that's also fine!
    static_assert(IsFoo<Foo::IsFooType>);
    
    struct FooBar : public AcceptsFoo<FooBar> // will fail once it is complete
    {
        //int a;
        int b;
    };
    // will fail here
    static_assert(IsFoo<FooBar::IsFooType>);
    

    这也适用于主要编译器:https://godbolt.org/z/e1Gc4Kj5n

    【讨论】:

    • "就在课程结束后,它就完成了并且匹配了所需的 IsFoo 约束。"...如果不是,那又如何呢?该标准要求,如果一个概念在程序中的某个点针对某个类型产生结果,那么它总是针对该类型产生该结果。那么如果类型 匹配“期望的 IsFoo 约束”会发生什么?
    • 这是 IFNDR,因为一个概念的价值从一个使用点到另一个使用点发生变化:请注意 struct Bar {AcceptsFoo&lt;Bar&gt; obj;}; 有效,并且在 3 个主要实现中导致 AcceptsFoo&lt;Bar&gt;Bar 时被接受 已完成,但明天可能会改变。
    • @Nicol Bolas 我真的没有看到这里的问题,因为 IsComplete 将被评估为 false 类型的 Foo,并且 IsFoo 正确匹配约束(如果我更改它们,编译器会正确指示哪个匹配失败)。你能给我一个失败的例子吗?
    • 这是真的@Davis Herring, struct Bar 在 g++10 上被接受...我不知道为什么。谢谢你的建议。 godbolt.org/z/Wbe7E1z4E
    • @igormcoelho:他们确实说IsFoo&lt;Bar&gt;false,但他们记得IsFooIncomplete&lt;Bar&gt; 成立,即使Bar 既不完整也不完整,也不会再次检查以供进一步使用福。这很容易导致违反 ODR 和其他各种疯狂行为。
    猜你喜欢
    • 1970-01-01
    • 2021-10-01
    • 2020-10-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-30
    相关资源
    最近更新 更多