为什么CRTP实现和接口方法命名不同?

jay*_*ica 6 c++ templates naming crtp

我读到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;
    }

};
Run Code Online (Sandbox Code Playgroud)

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

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

// 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;
Run Code Online (Sandbox Code Playgroud)

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

And*_*owl 5

在CRTP背后隐藏着"邪恶的力量",除了您提到的具有同样命名的界面功能及其实现的问题,但在我看来,这有点像"寻找麻烦".

关于"为什么不采用这种方法?"的问题.,我认为不是这样的.这种方法在需要时被广泛使用; 但没有设计模式是有道理的所有的时间.在某些特定的设计情况下,每个都很方便,CRTP也不例外.

当你有CRTP是最有用的几个,不相关的,支持一类常见的接口,但实现它(不完全)不同.如果我用粗体字表示的这些单词中的任何一个都没有描述你的设计用例,那么可能CRTP没有意义:

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

但是,当你的设计情况是上述所有四个属性都适用时,你肯定有一个CRTP的用例:没有虚函数开销,编译器可以完全优化你的代码,你有一个干净的界面分离和实现,您通过捕获通用实现逻辑实现最小冗余,等等.

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

  • @jaymmer:我明白了.好吧,无论无限递归问题如何,如果基类中的函数正在执行*more*而不是它们调用的派生类中的"实现"(如果不是这样,上面的第4点不适用),那么实现只是实现逻辑的*部分*,它由基类中的函数实现.它们不是"实现",而是"钩子".事实上,他们正在做一些*少*意味着他们正在做一些*不同的事情*.这反过来意味着他们应该有不同的名字. (2认同)

Dav*_*e S 4

您几乎已经说明了原因:未能实现该函数会导致无限循环。

通过将接口与实现分离,当派生类不提供实现时,它允许发生两种情况

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

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