6

我到处读到关于 CRTP 的信息,实际上在我编写的代码中,CTRP 类层次结构如下所示:

template< class T >
class Base
{

public:

    int foo_interface() 
    { 
        return static_cast< T* >(this)->foo_implementation();                        
    }

};

class Derived : public Base< Derived >
{

    friend class Base< Derived >;

    int foo_implementation() 
    { 
        return 5;
    }

};

即接口和实现方法的名称不同。现在,我通常不希望实现方法从外部可见,这需要上面的朋友声明,并且在多级层次结构中被证明是一个主要的组合(即使使用此处描述的技巧)。

现在,我想出了以下内容:

// Base class
template< class T >
class A
{

public:

    int foo() 
    {             
        std::cout << "I'm in A's foo!\n";
        return static_cast< T * >(this)->foo();            
    }

};

// Deriving class
class B : public A< B >
{

public:

    int foo()
    { 
        std::cout << "I'm in B's foo!\n";
        return 5; 
    }

};

// Deriving class with a nasty surprise...
class C: public A< C >
{

public:

    // ...crap, no foo to be found!

    int bar() 
    {             
        std::cout << "I'm in C's bar!\n";
        return 12; 
    }

};

template< class T >
int call_foo(A< T > & t)
{
    return t.foo();
}

B b;
C c;

现在,call_foo(b)就像我期望的那样工作,调用 B 的 foo() 实现。同样地,call_foo(c)也可以按预期工作(因为它不会……由于显而易见的原因,它会陷入无限循环)。我可以看到的一个缺点是,如果我忘记在派生类中实现一个方法(或拼写错误,忘记将其限定为 const,无论如何......),我会得到一个无限循环,所以它可能会产生那种错误有点难找到,因为它们没有在编译时被捕获。不过除此之外,它几乎就像普通的虚函数一样简单,而且我不必为隐藏实现方法而战。它看起来简单而优雅,但似乎没有人使用它......我的问题是,那么,有什么问题?为什么不是 没有使用这种方法吗?隐藏实现方法不是什么大不了的事吗?或者,当我在实际项目中尝试这种方法时,是否有某种不可估量的邪恶邪恶力量潜伏在那里,准备吞噬我的灵魂?

4

2 回答 2

5

除了你提到的同名接口函数及其实现的问题之外,CRTP 背后并没有潜伏着“邪恶力量”,但在我看来,这有点像“自找麻烦”。

至于“为什么不使用这种方法?”这个问题。,我认为情况并非如此。这种方法在需要时被广泛使用;但是没有任何设计模式总是有意义。每个都在某些特定的设计情况下派上用场,CRTP 也不例外。

当您有多个支持公共接口但实现方式略有(不完全)不同的类时,CRTP最有用。如果我用粗体表示的这些词中的任何一个都不能描述您的设计用例,那么 CRTP 可能没有意义:

  • “几个”:如果你只有一个这样的类而不是很多,为什么要把它的接口分解成一个超类呢?它只是引入了一些多余的命名约定;
  • “不相关”:如果您的类是相关的,这意味着它们派生自一个公共基类,因为需要运行时多态性(这种情况很常见,例如,如果您想要拥有异构的对象集合),那么通常将公共接口分解到该基类中变得很自然;
  • "Common":如果你的类不共享一个相当广泛的通用接口,那么基类就没有什么可考虑的了。例如,如果您的所有类都只有一个size()共同的方法,那么创建一个基类来保存该方法可能是一种无用的细粒度分解;
  • “略”:如果你的接口以完全不同的方式实现,意味着没有共同的实现,那么创建一个简单地将所有函数调用转发给子类的基类是没有意义的。做什么的?

但是,当您的设计情况是上述四个属性适用时,那么您肯定有 CRTP 的用例:没有虚函数开销,编译器可以完全优化您的代码,您可以清晰地分离接口和实现,您可以通过捕获通用实现逻辑等来实现最小冗余。

但是,您可能会意识到这种情况并不常见。希望这能回答你的问题。

于 2013-01-31T13:04:21.307 回答
4

您几乎说出了原因:未能实现该功能会导致无限循环。

通过将接口与实现分离,它允许在派生类不提供实现时发生两件事

1) 如果基类有它自己的实现,它的行为就像一个普通的虚函数,其中基类有一个默认实现。
2) 如果基类没有提供实现,则编译失败。同样,这类似于纯虚函数。

最后,有些人(例如 Herb Sutter)建议在使用虚方法时始终将接口(公共函数)与实现(私有函数)分开。阅读http://www.gotw.ca/publications/mill18.htm。通过将分离作为 CRTP 的一部分执行,您可以获得相同的好处。

于 2013-01-31T12:34:27.573 回答