C++中静态多态性背后的动机是什么?

45

我了解使用奇异递归模板模式静态多态性机制,但我不明白它有什么用处。

声明的动机是:

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

但为什么要费事去做像这样 复杂的事情:

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++风格问题。

Herb Sutter在Exceptional C++ style: Chapter 18中写道:

首选使虚函数为私有。

当然,还附带了一个详细的解释,说明这是好的风格

在这个指南的背景下,第一个例子是好的,因为:

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

第二个例子是不好的,因为:

我们不应该干涉公共接口来执行自定义操作。

我的问题是:

  1. 我对静态多态性有什么误解吗?这完全是关于良好的C++风格吗?
  2. 何时应该使用它?有哪些指导原则?

13
请注意,该函数不是虚函数,因此通过基类指针调用会调用Base::interface而不是Derived::interface--您只是隐藏了继承的名称。在那里不存在多态性。 - Billy ONeal
我也很好奇这个问题。我的理解是,这与性能有关,因为多态性将在编译时而不是运行时解析。 - jab
3个回答

55
我对静态多态性的理解有哪些不足?这是否都与良好的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 ++(){ /*增量并返回*this*/}这样的内容 - 在成员函数签名中以派生形式指定。

它可以用于多态目的,但我没有看到太多这样的情况。


除了 std::ostream 没有虚方法并且通常不应该被子类化。需要多态输出行为的代码应该从 std::basic_streambuf<char> 派生,然后从中构造一个普通的 std::ostream - aschepler
如果你想定义一个新的sink,是的。然而,std::sstream vs std::fstream vs std::cout(即使它的类型未指定)都是运行时多态的,并且它们派生自ostream - Billy ONeal
1
@BillyONeal,你回答中有一句话我不太理解:“没有办法使用运行时多态性使其编译。” 难道不能为迭代器创建一个抽象类,然后每个迭代器都实现它的advance函数吗?为什么这必须在编译时完成呢? - TonySalimi
@Gupta,根据我们所定义的迭代器,无法使用运行时多态性来实现“advance”。如果您可以更改任何内容,那么是的,您可以要求所有迭代器都派生自“iterator”基类/接口。但是这意味着指针现在是一个类类型。 - Caleth
@TonySalimi 没错。所有这些“问题”都是由于糟糕的设计和可能滥用模板和通用编程造成的。 - juzzlin

4

您提供的链接提到了boost迭代器作为静态多态性的例子。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等语言中使用类似于C++中的奇异递归模板模式来实现类似的API。希望这对您有所帮助!

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


1
我不确定你的例子有多少说服力;你的第二个案例有一个使用多态性的例子,但第一个没有。类定义长度大致相同。对于一个非专业观察者来说,第二个例子比第一个例子慢的原因也不太清楚... - Billy ONeal
是的,那个例子并不是很有用。我应该把它删掉。关于类型擦除的重点才是我应该谈论的。 - Dan O
不确定为什么需要魔法。std::vector<std::unique_ptr<IDrawable>> 提供了所有必要的魔法。 - Billy ONeal
1
哈哈,抱歉。我刚写完评论就删除了它。我太可怕了! - Dan O

0

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

静态和动态多态性之间的运行时差异正好是两个指针解引用(如果编译器真的将分派方法内联到基类中,如果由于某种原因没有内联,则静态多态性会更慢)。这并不是非常昂贵的,特别是因为第二次查找应该几乎总是命中缓存。总的来说,这些查找通常比函数调用本身更便宜,并且绝对值得获得动态多态性提供的真正灵活性。


我不确定是否同意“灵活性”论点。静态多态可以在涉及的任何类型上工作——甚至是指针这样的内置类型(请参阅我的答案中的高级示例)。运行时多态要求一种类型明确声明它想通过从某些其他类型派生来参与您的通用性。至于“不是面向对象编程的重点”——是的,许多人认为这是一个好事情。OOP是一种适用于某些问题领域的伟大范式,但它并不像经常被吹嘘的那样是万能的疗法。 - Billy ONeal
@BillyONeal 静态多态性不受涉及类型的影响--即使是指针等内置类型你肯定有一点道理。我甚至同意你的观点,即面向对象编程不是唯一的范例。然而,我坚信许多人对面向对象编程的印象是错误的,因为他们从未了解过由良好设计的纯面向对象编程语言和库所带来的灵活性,例如Objective-C和Cocoa。别误会,我不是苹果的支持者,但就语言而言,他们确实做得很对。 - cmaster - reinstate monica
这比静态实现更灵活的原因是什么?Cocoa使用运行时多态性,因为Objective-C没有其他种类,而不是内在的优越性的结果。 - Billy ONeal
@BillyONeal 你有没有使用过它?我用过,差别很大。问题在于,静态多态性需要编译器精确地知道每个地方的具体类型 - 有时甚至程序员自己都不想知道。这基本上破坏了继承带来的灵活性。 - cmaster - reinstate monica
2
@BillyONeal 听起来我们应该就同意各自的观点。澄清一下,这是我对你的问题“你有一个代码示例明确展示了运行时多态可以实现而静态多态无法实现的东西吗?”的回答,而循环在库中显然是无法通过模板实现的。我并不认为这很重要。重要的是模板参数的数量会污染您的代码,使用静态多态。但我想,只有真正深入研究动态多态才能真正欣赏它的价值。 - cmaster - reinstate monica
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接