为什么 CRTP 实现和接口方法的名称不同

Why are CRTP implementation and interface methods named differently?

本文关键字:方法 CRTP 实现 接口 为什么      更新时间:2023-10-16

在我读到的关于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,等等),我会得到一个无限循环,所以它可能会使这种错误更难找到,因为它们没有在编译时被捕获。除此之外,它几乎就像普通的虚拟函数一样简单,我不必与隐藏实现方法作斗争。它看起来简单而优雅,但似乎没有人使用它......那么,我的问题是,有什么收获?为什么不使用这种方法?隐藏实现方法根本不是什么大问题吗?还是有某种不可估量的邪恶力量潜伏在那里,准备在我在一个真正的项目中尝试这种方法的那一刻吞噬我的灵魂?

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

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

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

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

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

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

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

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

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

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