C++ 静态多态性 (CRTP) 和使用派生类的 typedef

Posted

技术标签:

【中文标题】C++ 静态多态性 (CRTP) 和使用派生类的 typedef【英文标题】:C++ static polymorphism (CRTP) and using typedefs from derived classes 【发布时间】:2011-08-25 18:48:59 【问题描述】:

我阅读了Wikipedia article 关于在 C++ 中奇怪地重复出现的模板模式,用于执行静态(阅读:编译时)多态性。我想对其进行概括,以便可以根据派生类型更改函数的返回类型。 (这似乎应该是可能的,因为基类型知道模板参数的派生类型)。不幸的是,以下代码无法使用 MSVC 2010 编译(我现在无法轻松访问 gcc,所以我还没有尝试过)。有人知道为什么吗?

template <typename derived_t>
class base 
public:
    typedef typename derived_t::value_type value_type;
    value_type foo() 
        return static_cast<derived_t*>(this)->foo();
    
;

template <typename T>
class derived : public base<derived<T> > 
public:
    typedef T value_type;
    value_type foo() 
        return T(); //return some T object (assumes T is default constructable)
    
;

int main() 
    derived<int> a;

顺便说一句,我有一个使用额外模板参数的解决方法,但我不喜欢它——当将许多类型传递到继承链上时它会变得非常冗长。

template <typename derived_t, typename value_type>
class base  ... ;

template <typename T>
class derived : public base<derived<T>,T>  ... ;

编辑:

MSVC 2010 在这种情况下给出的错误消息是error C2039: 'value_type' : is not a member of 'derived&lt;T&gt;'

g++ 4.1.2(通过codepad.org)说error: no type named 'value_type' in 'class derived&lt;int&gt;'

【问题讨论】:

请注意,codepad.org 可以为您编译和运行代码,我相信它使用 gcc/g++。所以你永远不会脱离 g++ :) 您能补充一下您遇到的错误,以便我对读者有用。 @Seth:Ideone 肯定使用 gcc,所以它是另一个 :) @Seth:感谢关于 codepad.org 的提示! @Sriram:好电话。我添加了它们。 【参考方案1】:

当您将derived 用作其基类列表中base 的模板参数时,它是不完整的。

一种常见的解决方法是使用特征类模板。这是你的例子,特征化。这显示了如何通过特征使用派生类中的类型和函数。

// Declare a base_traits traits class template:
template <typename derived_t> 
struct base_traits;

// Define the base class that uses the traits:
template <typename derived_t> 
struct base  
    typedef typename base_traits<derived_t>::value_type value_type;
    value_type base_foo() 
        return base_traits<derived_t>::call_foo(static_cast<derived_t*>(this));
    
;

// Define the derived class; it can use the traits too:
template <typename T>
struct derived : base<derived<T> >  
    typedef typename base_traits<derived>::value_type value_type;

    value_type derived_foo()  
        return value_type(); 
    
;

// Declare and define a base_traits specialization for derived:
template <typename T> 
struct base_traits<derived<T> > 
    typedef T value_type;

    static value_type call_foo(derived<T>* x)  
        return x->derived_foo(); 
    
;

您只需将base_traits 专门用于您用于base 的模板参数derived_t 的任何类型,并确保每个专门化提供base 所需的所有成员。

【讨论】:

我创建了一个测试文件并编译了 OP 代码,并在 Ubuntu g++ 4.4.1 中放置了一个 main() 它工作正常。声明像Derived&lt;int&gt; 这样的对象会出错,​​我想弄清楚为什么? @iammilind 那是因为在一个空的main() 中没有模板实例化发生。只有在编译器尝试实例化和使用模板时才会出现某些错误。 对;您必须实例化模板才能在此处出现任何错误;如果您将int main() derived&lt;int&gt;().base_foo(); 添加到我的特征示例的底部(这会强制实例化所有内容),它应该可以使用 Visual C++ 2010、g++ 4.5.1 和最新的 Clang 构建进行编译。 您能否详细解释一下“当您将其用作基类列表中基类的模板参数时,派生是不完整的”是什么意思。哪方面不完整? @JamesMcNellis 和 Sriram,我感觉可能会出现混淆。稍微扁平化的版本,派生不是模板:struct derived : base&lt;derived&gt; ...; 确实使 base_traits 未定义。【参考方案2】:

使用特征的一个小缺点是您必须为每个派生类声明一个。您可以像这样编写一个不那么冗长和冗余的解决方法:

template <template <typename> class Derived, typename T>
class base 
public:
    typedef T value_type;
    value_type foo() 
        return static_cast<Derived<T>*>(this)->foo();
    
;

template <typename T>
class Derived : public base<Derived, T> 
public:
    typedef T value_type;
    value_type foo() 
        return T(); //return some T object (assumes T is default constructable)
    
;

int main() 
    Derived<int> a;

【讨论】:

这里有什么特点? 这看起来很有趣,但我不是很明白,你能解释一下吗? @BenFarmer 我们不依赖派生类型来提取基类中的 value_type,而是直接将其传递给基类。基类现在有两个模板参数。如果只有或两种类型要通过,这很实用,但如果有,这不方便。 @BaptisteWicht 实际上,您可以使用可变参数模板和元组来获得更大的灵活性。 这不是多余的,因为您不传递派生类的类型,而是将其模板作为基类的模板参数传递。 (注意可以去掉派生类中的typedef)【参考方案3】:

在 C++14 中,您可以删除 typedef 并使用函数 auto 返回类型推导:

template <typename derived_t>
class base 
public:
    auto foo() 
        return static_cast<derived_t*>(this)->foo();
    
;

之所以有效,是因为base::foo 的返回类型的推演延迟到derived_t 完成。

【讨论】:

这种技术是否限制返回值?它是否适用于字段或函数参数? C++17 会提供更多帮助吗?【参考方案4】:

需要较少样板的类型特征的替代方法是将派生类嵌套在包含 typedef(或 using's)的包装类中,并将包装类作为模板参数传递给基类。

template <typename Outer>
struct base 
    using derived = typename Outer::derived;
    using value_type = typename Outer::value_type;
    value_type base_func(int x) 
        return static_cast<derived *>(this)->derived_func(x); 
    
;

// outer holds our typedefs, derived does the rest
template <typename T>
struct outer 
    using value_type = T;
    struct derived : public base<outer>  // outer is now complete
        value_type derived_func(int x)  return 5 * x; 
    ;
;

// If you want you can give it a better name
template <typename T>
using NicerName = typename outer<T>::derived;

int main() 
    NicerName<long long> obj;
    return obj.base_func(5);

【讨论】:

聪明,但这有副作用。例如,T 将无法在函数模板中推导出来,例如template&lt;class T&gt; f(outer&lt;T&gt;::derived x)(与template&lt;class T&gt; f(derived2&lt;T&gt; x) 相对。 你可以写template &lt;class C, class T = typename C::value_type&gt; auto f(C x)。如果您担心接受任意类型,可以将 class = std::enable_if_t&lt; std::is_same_v&lt; C, NicerName&lt;T&gt; &gt;, int&gt; 添加到模板参数列表中,但它会很长。【参考方案5】:

我知道这基本上是您找到但不喜欢的解决方法,但我想记录它并说它基本上是此问题的当前解决方案。

我一直在寻找一种方法来做到这一点,但从未找到一个好的解决方案。 不可能的事实是最终,像boost::iterator_facade&lt;Self, different_type, value_type, ...&gt; 这样的东西需要很多参数的原因。

我们当然希望这样的东西可以工作:

template<class CRTP> 
struct incrementable
    void operator++()static_cast<CRTP&>(*this).increment();
    using ptr_type = typename CRTP::value_type*; // doesn't work, A is incomplete
;

template<class T>
struct A : incrementable<A<T>>
    void increment()
    using value_type = T;
    value_type f() constreturn value_type;
;

int main()A<double> a; ++a;

如果可能的话,派生类的所有特征都可以隐式传递给基类。我发现获得相同效果的成语是将特征完全传递给基类。

template<class CRTP, class ValueType> 
struct incrementable
    void operator++()static_cast<CRTP&>(*this).increment();
    using value_type = ValueType;
    using ptr_type = value_type*;
;

template<class T>
struct A : incrementable<A<T>, T>
    void increment()
    typename A::value_type f() constreturn typename A::value_type;
//    using value_type = typename A::value_type;
//    value_type f() constreturn value_type;
;

int main()A<double> a; ++a;

https://godbolt.org/z/2G4w7d

缺点是派生类中的特征必须使用限定的typenameusing 重新启用 访问。

【讨论】:

以上是关于C++ 静态多态性 (CRTP) 和使用派生类的 typedef的主要内容,如果未能解决你的问题,请参考以下文章

c++派生类的类型列表

派生模板类对象的实例化

c++实验四 类的继承派生和多态

C++的多态总结(静态&动态)

C++ 继承&多态

C++ 继承和多态