【问题标题】:Is it possible to access child types in c++ using CRTP?是否可以使用 CRTP 访问 c++ 中的子类型?
【发布时间】:2021-06-14 12:36:01
【问题描述】:

我有这个玩具例子,

template <typename TChild>
struct Base {
    template <typename T>
    using Foo = typename TChild::template B<T>;
};

struct Child : Base<Child> {
    template <typename T>
    using B = T;
};


using Bar = Child::Foo<int>;

编译失败。目的是我有一个父类,它提供基于子类成员的类型计算。子类是通过 CRTP 提供的。然而这条线

using Foo = typename TChild::template B<T>;

编译失败:

<source>: In instantiation of 'struct Base<Child>':
<source>:16:16:   required from here
<source>:13:11: error: invalid use of incomplete type 'struct Child'
   13 |     using Foo = typename TChild::template B<T>;
      |           ^~~
<source>:16:8: note: forward declaration of 'struct Child'
   16 | struct Child : Base<Child> {
      |        ^~~~~

我是否天真地期望这样的构造起作用?

https://godbolt.org/z/5Prb84 的失败代码

【问题讨论】:

  • 使用传统的“traits”类可能是更好的选择。
  • @evg 你可能是对的,但我已经为上述stackoverflow.com/a/66668833/158285 提出了一个解决方案,但我不明白为什么原来的尝试没有这样做。模板仍然需要检查。看起来一种途径的规则比另一种更宽松。
  • @bradgonesurfing 使用嵌套类型需要完整的嵌套类型和封闭类。 Base 模板在从中派生一些子类时被实例化。如果它有自己的子类,则在具体Base 之前声明的特征类是一个完整的类(并将在之前实例化)。使用 Base 的成员仅是因为它们不是类型,并且在 CRTP 的情况下,模板参数在实例化时被认为是完整的类型。仍然应该小心,有时使用this-&gt; 以避免与全局范围冲突。

标签: c++ c++14 template-meta-programming crtp


【解决方案1】:

让我发布另一种方法:

template<typename TChild, class T>
struct GetB {
    using Type = typename TChild::template B<T>;
};

template<typename TChild>
struct Base {
    template<typename T>
    using Foo = typename GetB<TChild, T>::Type;
};

struct Child : Base<Child> {
    template<typename T>
    using B = T;
};

我没有语言律师式的解释为什么会这样,但它应该与额外的间接级别有关。当编译器看到

using Foo = typename TChild::template B<T>;

它可以(并且将)在这一点上检查并抱怨使用了不完整的类型。但是,当我们将对B&lt;T&gt; 的访问封装到函数或结构中时,

using Foo = typename GetB<TChild, T>::Type;

那么此时我们并没有访问TChild 的内部,我们只是使用它的名称,这很好。

【讨论】:

    【解决方案2】:

    CRTP的问题是CRTP定义中派生类不完整,所以不能使用它的using

    template <typename T>
    using Foo = typename TChild::template B<T>;
    
    • 由于::,所以需要完整类型的TChild
    • TChild 不依赖于模板 T,因此应进行第一次通过检查(但失败)

    您可以使用外部特征来处理这种情况

    template <typename C, typename T>
    struct Traits_For_Base
    {
        using type = typename C::template B<T>;
    };
    
    template <typename TChild>
    struct Base {
        template <typename T>
        using Foo = typename Traits_For_Base<TChild, T>::type;
    };
    

    Traits_For_Base&lt;TChild, T&gt;T依赖,因此无需进行首次通过检查。 并且,通过第二次通过检查(依赖 of T),Child 将完成。

    Demo

    或者你可以改变你的别名,使其类型依赖于Base类的模板参数:

    template <typename TChild>
    struct Base {
        template <typename T,
                  typename C = TChild,
                  std::enable_if_t<std::is_same_v<C, TChild>, int> = 0> // To avoid hijack
        using Foo = typename C::template B<T>;
    };
    

    C依赖模板,所以不能在第一阶段检查。

    Demo

    【讨论】:

      【解决方案3】:

      以下给出了与要求完全相同的结果。以前的技术不起作用时,为什么它应该起作用仍然是一个谜。

      #include <type_traits>
      #include <string>
      
      template <typename TChild>
      struct Base {
      
          template <typename T>
          static auto foo(){
              return typename TChild::template B<T>();
          }
      
          template <typename T>
          using Foo = std::decay_t<decltype(foo<T>())>;
      };
      
      struct Child : Base<Child> {
          template <typename T>
          using B = T;
      };
      
      static_assert(std::is_same<Child::B<int>,int>::value,"");
      static_assert(std::is_same<Child::B<std::string>,std::string>::value,"");
      

      https://godbolt.org/z/b6Y5Tb

      【讨论】:

      • 如果你还想处理没有默认构造函数的类类型,或者私有或删除的默认构造函数,或者作为返回类型不合法的类型,你可以使用type_identity&lt;typename TChild::template B&lt;T&gt;&gt;{}作为返回键入,然后 Foo = typename decltype(foo&lt;T&gt;())::type。 (std::type_identity 不在 C++14 中,但自己编写很简单)
      【解决方案4】:

      这是否符合您的意思?

      #include <type_traits>
      template <typename TChild>
      struct Base {
          template <typename T>
          static auto constexpr getFoo() {
              return typename TChild::template B<T>{};
          }
      };
      
      struct Child : Base<Child> {
          template <typename T>
          using B = T;
      };
      
      
      using Bar = decltype(Child::getFoo<int>());
      static_assert(std::is_same_v<Bar, int>);
      

      我基本上用模板静态函数替换了模板别名,该函数返回您希望模板别名编码的类型的默认构造对象,因此decltype-ing 它的结果应该给出您想要的类型。

      【讨论】:

      • 关闭。在我看到你的解决方案之前,我自己想出了一个类似的解决方案,除了我的(见下文)保持我想要的形式。还是 +1
      【解决方案5】:

      目前的问题是嵌套模板的实例化需要完整类型的封闭类和模板B的声明:

      template <typename TChild>
      struct Base {
      
          // TChild should be complete at the moment of this declaration
          // template B should be declared at this moment.
          template <typename T>
          using Foo = typename TChild::template B<T>;
      };
      

      Base的实例化

      struct Child : Base<Child>   
      /*  TChild = Child at this moment is incomplete */
      {
          template <typename T>
          using B = T;
          /* Point where template B begins to exist */
      }; /* point where Child is complete */
      

      这些规则是语言设计的,它们的目标是避免强制编译器在代码中多次往返,可能是无限递归,以实际实例化你的意思。弱类型的解释器语言通常没有这样的问题,因为它们可以在以后“纠正”自己。

      案例 1。 静态函数解决方案之所以有效,是因为没有进行类型声明。您已经声明了一个实际上具有全局范围的函数模板,但尚未创建具体的函数或类型。

      struct Base {
          template <typename T>
          static auto constexpr getFoo() {
              return typename TChild::template B<T>{};
          }
      };
      
      struct Child : Base<Child> {
          template <typename T>
          using B = T;
          /* At this point we can instantiate Child::getFoo<int>()*/
      }; /* Child is complete now */
      

      此时Child::getFoo&lt;T&gt; 的实例化是可能的,但它只需要函数的返回类型。

      using Bar = decltype(Child::getFoo<int>());
      

      您可以在声明B 之后将此声明放入Child,因为此时B&lt;int&gt; 将完成。你还是不能在Base声明它

      案例 2。 您的解决方案声明了另一个模板 Foo,它不会在 Base 中实例化它。此模板不明确依赖于TChild,但需要foo() 的原型才能存在于实例化点。

      template <typename TChild>
      struct Base {
      
          template <typename T>
          static auto foo(){
              return typename TChild::template B<T>();
          }
      
          // Foo is a template 
          template <typename T>
          using Foo = std::decay_t<decltype(foo<T>())>;
      };
      

      实例化发生在您将使用Base::Foo&lt;T&gt; 的地方,而实际上您并没有。该声明在您的解决方案中是无效的。在B 声明之后使用它是合法的。您不能在 Base 内或声明 B 之前的任何地方使用它。

      现在如果您实际上需要在Base 中使用B 的实例怎么办?特质类解决方案来了:

      案例 3。 特征可以是专门用于子类或具体类的模板,这是一种设计选择。特征作为 CRTP 基类的基类,是一种混合形式。它的作用是为 CRTP 提供有用的声明。最灵活的特征命名的可能解决方案之一:

      template <typename TChild, template<class> typename Trait>
      struct Base : public Trait<TChild> {
      
          // Trait<TChild>:: tells compiler that Foo is dependant on TChild 
          // and is declared in base class Trait. As compiler had reached this
          // point, the substitution was successful and thus Trait is complete
          using Foo = typename Trait<TChild>::template B<int>;
      
          // Foo is assumed to be a complete type, we can use it here!
          Foo make_foo() { return Foo{}; }
      };
      
      // Declaring trait template in this case.
      template <typename T> struct ChildTrait;
      
      // And specializing
      template <>
      struct ChildTrait<struct Child> {
          template <typename T>
          using B = T; 
      };
      
      struct Child : Base<Child,ChildTrait> {    
          using Bar = typename Base::Foo;
      };
      
      static_assert(std::is_same<Child::B<int>,int>::value,"");
      static_assert(std::is_same<Child::B<std::string>,std::string>::value,"");
      

      这里的想法是Trait&lt;TChild&gt; = ChildTrait&lt;Child&gt; 必须是并且可以是Base 中的一个完整类,否则我们无法从中派生Base。稍作修改(省略 usingstatic_asserttypename 使用)这将在 C++98 中编译,因为它不需要 decltype。此方法由标准组件的某些实现使用,例如std:: streams.

      特征可以描述具体的存储类型、分配器等。重要的是生成的具体类型没有关系,但具有在Base 中声明的共享接口。

      【讨论】:

        猜你喜欢
        • 2021-10-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-09-20
        • 1970-01-01
        • 1970-01-01
        • 2016-05-27
        • 1970-01-01
        相关资源
        最近更新 更多