【问题标题】:What is the motivation behind static polymorphism in C++?C ++中静态多态性背后的动机是什么?
【发布时间】:2013-10-04 10:59:06
【问题描述】:

我了解使用Curiously Recurring Template Patternstatic polymorphism 的机制。我只是不明白它有什么好处。

声明的动机是:

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

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

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. 什么时候应该使用它?有哪些指导方针?

【问题讨论】:

  • 请注意,该函数不是虚拟的,因此通过基类指针调用Base::interface 而不是Derived::interface - 您只是隐藏了继承的名称。那里没有多态性。
  • 其实我也很好奇这个。我的理解是,它纯粹与性能相关,因为多态性将在编译时而不是运行时解决。

标签: c++ coding-style


【解决方案1】:

我对静态多态性遗漏了什么?都是关于良好的 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 */ } 之类的东西——根据成员函数 signatures 中的派生来指定。

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

【讨论】:

  • 很好的答案!谢谢!
  • 除了std::ostream 没有虚方法,通常不应该被子类化。想要多态输出行为的代码应该从std::basic_streambuf&lt;char&gt;派生出来,然后从那里构造一个普通的std::ostream
  • @aschepler:如果你想定义一个新的接收器,是的。然而,std::sstream vs std::fstream vs std::cout(即使它的类型未指定)是运行时多态的,它们派生自ostream
  • @BillyONeal 您的回答中有一句话我无法理解:“无法使用运行时多态性来编译它。”难道不可能有一个迭代器的抽象类,每个迭代器都实现它的高级功能吗?为什么这应该在编译时完成?
  • @Gupta 没有办法用运行时多态性实现advance,因为我们有迭代器的定义。如果您可以更改任何内容,那么可以,您可以要求所有迭代器都派生自 iterator 基类/接口。然而,这意味着 pointers 现在是一个类类型。
【解决方案2】:

您提供的链接提到了 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&lt;int&gt;::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++ 中,没有有用的 all 包含 Object 类型的有用信息。相反,我们有void*,一旦你拥有void*,你就真的什么都没有了!如果您的接口衰减为void*,则恢复的唯一方法是做出危险的假设或保留额外的类型信息。

【讨论】:

  • 我不确定你的例子是否令人信​​服;如果多态性,您的第二种情况有一个示例使用,但第一种情况没有。类定义的长度大致相同。不经意的观察者也不清楚为什么第二个示例比第一个慢...
  • 是的,这个例子并不是很有用。我可能应该只是核爆它。关于类型擦除的重点是我应该谈论的全部内容。
  • 不知道为什么一定要有魔法。 std::vector&lt;std::unique_ptr&lt;IDrawable&gt;&gt; 提供所有必要的魔法。
  • 哈哈,对不起。我在写完评论后立即取消了评论。我是怪物!
【解决方案3】:

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

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

【讨论】:

  • 我不确定我是否购买了灵活性参数。无论涉及的类型如何,静态多态性都有效——甚至内置于指针之类的类型中(请参阅我的回答中的高级示例)。运行时多态性要求一个类型明确声明它希望通过派生于其他类型来参与您的泛型。至于“不是面向对象的意义”——是的,许多人认为这是一件好事。 OOP 对于某些问题领域来说是一个很好的范例,但它并不是人们常说的灵丹妙药。
  • @BillyONeal 无论涉及的类型如何,静态多态都可以工作——即使是像指针这样的内置类型你肯定有一点。我什至同意你的观点,OOP 不是一个真正的范式。但是,我坚信很多人对 OOP 的真正含义有错误的印象,因为他们甚至不知道像 Objective-C 和 Cocoa 那样设计良好、纯粹面向对象的编程语言和库所带来的灵活性。不要误会我的意思,我不是苹果的拥护者,而是 wrt。语言,他们确实做了正确的事。
  • 这怎么能比静态完成的更灵活呢? Cocoa 使用运行时多态是因为 Objective-C 没有其他类型,而不是天生的优越性。
  • @BillyONeal 你用过它吗?我有,而且差别很大。关键是,对于静态多态性,编译器必须知道所有事物的精确类型——有时甚至程序员也不想知道。它基本上打败了继承带来的灵活性。
  • @BillyONEal 在我看来,我们应该同意不同意。澄清一下,这是我对你的问题的回答“你有一个代码示例,它明确地显示了运行时多态性是可能的,而静态多态性是不可能的?”,库中的循环显然是可以的不能通过模板来实现。我不说这很重要。重要的是使用静态多态性污染代码的模板参数的数量。但我想,你必须认真研究动态多态性才能真正欣赏它。
猜你喜欢
  • 2012-03-15
  • 2014-09-27
  • 1970-01-01
  • 2013-10-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-06
  • 1970-01-01
相关资源
最近更新 更多