43

我使用Curiously Recurring Template Pattern了解静态多态性的机制。我只是不明白它有什么好处。

公开的动机是:

我们为了速度牺牲了一些动态多态的灵活性。

但是为什么要为如此复杂的事情烦恼:

template <class Derived>
class Base
{
public:
    void interface()
    {
         // ...
         static_cast<Derived*>(this)->implementation();
         // ...
    }
};

class Derived : Base<Derived>
{
private:
     void implementation();
};

当你可以这样做时:

class Base
{
public: 
    void interface();
}

class Derived : public Base
{
public: 
    void interface();
}

我最好的猜测是代码中没有语义差异,这只是良好的 C++ 风格问题。

赫伯萨特写道Exceptional C++ style: Chapter 18

更喜欢将虚拟功能设为私有。

当然伴随着详尽的解释为什么这是好的风格

在本指南的上下文中,第一个示例很好,因为:

示例中的void implementation()函数可以假装是虚拟的,因为它在这里执行类的自定义。因此它应该是私有的。

第二个例子是bad,因为:

我们不应该干预公共接口来执行定制。

我的问题是:

  1. 我对静态多态性缺少什么?都是关于良好的 C++ 风格的吗?
  2. 什么时候应该使用它?有哪些指导方针?
4

3 回答 3

52

我对静态多态性缺少什么?都是关于良好的 C++ 风格的吗?

静态多态和运行时多态是不同的东西,实现不同的目标。它们在技术上都是多态性,因为它们根据某物的类型决定执行哪段代码。运行时多态性将某些东西的类型(以及运行的代码)推迟到运行时,而静态多态性在编译时完全解决。

这导致了每个人的利弊。例如,静态多态性可以在编译时检查假设,或者在否则不会编译的选项中进行选择。它还为编译器和优化器提供了大量信息,它们可以内联完全了解调用的目标和其他信息。但是静态多态性要求编译器可以在每个翻译单元中检查实现,这可能导致二进制代码大小膨胀(模板是花哨的裤子复制粘贴),并且不允许在运行时发生这些确定。

例如,考虑类似std::advance

template<typename Iterator>
void advance(Iterator& it, ptrdiff_t offset)
{
    // If it is a random access iterator:
    // it += offset;
    // If it is a bidirectional iterator:
    // for (; offset < 0; ++offset) --it;
    // for (; offset > 0; --offset) ++it;
    // Otherwise:
    // for (; offset > 0; --offset) ++it;
}

没有办法使用运行时多态来编译它。您必须在编译时做出决定。(通常你会用标签调度来做到这一点,例如)

template<typename Iterator>
void advance_impl(Iterator& it, ptrdiff_t offset, random_access_iterator_tag)
{
    // Won't compile for bidirectional iterators!
    it += offset;
}

template<typename Iterator>
void advance_impl(Iterator& it, ptrdiff_t offset, bidirectional_iterator_tag)
{
    // Works for random access, but slow
    for (; offset < 0; ++offset) --it; // Won't compile for forward iterators
    for (; offset > 0; --offset) ++it;
}

template<typename Iterator>
void advance_impl(Iterator& it, ptrdiff_t offset, forward_iterator_tag)
{
     // Doesn't allow negative indices! But works for forward iterators...
     for (; offset > 0; --offset) ++it;
}

template<typename Iterator>
void advance(Iterator& it, ptrdiff_t offset)
{
    // Use overloading to select the right one!
    advance_impl(it, offset, typename iterator_traits<Iterator>::iterator_category());
}  

同样,在某些情况下,您在编译时确实不知道类型。考虑:

void DoAndLog(std::ostream& out, int parameter)
{
    out << "Logging!";
}

在这里,对它得到DoAndLog的实际实现一无所知ostream——并且可能无法静态确定将传入什么类型。当然,这可以转换为模板:

template<typename StreamT>
void DoAndLog(StreamT& out, int parameter)
{
    out << "Logging!";
}

但这强制DoAndLog在头文件中实现,这可能是不切实际的。它还要求所有可能的实现StreamT在编译时都是可见的,这可能不是真的——运行时多态性可以跨 DLL 或 SO 边界工作(尽管不推荐这样做)。


什么时候应该使用它?有哪些指导方针?

这就像有人来找你说“当我在写一个句子时,我应该使用复合句还是简单句”?或者也许是一位画家说“我应该总是使用红色油漆还是蓝色油漆?” 没有正确的答案,也没有一套可以在这里盲目遵循的规则。您必须查看每种方法的优缺点,并决定哪种方法最适合您的特定问题域。


至于 CRTP,大多数用例是允许基类在派生类方面提供一些东西;例如 Boost 的iterator_facade. 基类需要有类似DerivedClass operator++() { /* Increment and return *this */ }内部的东西——在派生的成员函数签名方面指定。

它可以用于多态目的,但我还没有看到太多。

于 2013-09-28T03:30:20.430 回答
4

您提供的链接提到提升迭代器作为静态多态性的一个例子。STL 迭代器也展示了这种模式。让我们看一个例子,并思考为什么这些类型的作者认为这种模式是合适的:

#include <vector>
#include <iostream>
using namespace std;
void print_ints( vector<int> const& some_ints )
{
    for( vector<int>::const_iterator i = some_ints.begin(), end = some_ints.end(); i != end; ++i )
    {
        cout << *i;
    }
}

现在,我们将如何实现int vector<int>::const_iterator::operator*() const;我们可以为此使用多态吗?嗯,不。我们的虚函数的签名是什么?void const* operator*() const? 那没用!该类型已被删除(从 int 降级为 void*)。相反,奇怪地重复出现的模板模式帮助我们生成迭代器类型。这是我们需要实现上述迭代器类的粗略近似:

template<typename T>
class const_iterator_base
{
public:
    const_iterator_base():{}

    T::contained_type const& operator*() const { return Ptr(); }
    T::contained_type const& operator->() const { return Ptr(); }
    // increment, decrement, etc, can be implemented and forwarded to T
    // ....
private:
    T::contained_type const* Ptr() const { return static_cast<T>(this)->Ptr(); }
};

传统的动态多态无法提供上述实现!

一个相关且重要的术语是参数多态性。这允许您在 Python 中实现类似的 API,您可以使用 C++ 中奇怪的重复模板模式。希望这有帮助!

我认为值得一试的是所有这些复杂性的根源,以及为什么像 Java 和 C# 这样的语言大多试图避免它:类型擦除!在 C++ 中,没有有用的包含Object有用信息的所有类型。相反,我们拥有void*,一旦你拥有,void*你就真的一无所有!如果您的接口衰减到void*唯一的恢复方法是做出危险的假设或保留额外的类型信息。

于 2013-09-28T03:46:04.487 回答
0

虽然可能存在静态多态性有用的情况(其他答案列出了一些),但我通常认为这是一件坏事。为什么?因为您实际上不能再使用指向基类的指针,所以您总是必须提供一个模板参数来提供确切的派生类型。在这种情况下,您也可以直接使用派生类型。而且,坦率地说,静态多态不是面向对象的内容。

静态多态和动态多态之间的运行时差异恰好是两个指针解引用(如果编译器真的在基类中内联调度方法,如果由于某种原因没有,静态多态会更慢)。这并不是很昂贵,尤其是因为第二次查找实际上应该总是命中缓存。总而言之,这些查找通常比函数调用本身更便宜,并且获得动态多态性提供的真正灵活性当然是值得的。

于 2013-09-28T07:29:03.860 回答