Подтвердить что ты не робот

Какова мотивация статического полиморфизма в С++?

Я понимаю механику статического полиморфизма с помощью любопытно повторяющегося шаблона шаблона. Я просто не понимаю, для чего это полезно.

Заявленная мотивация такова:

Мы жертвуем некоторой гибкостью динамического полиморфизма для скорости.

Но зачем беспокоиться о чем-то таком сложном, как:

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();
}

Моя лучшая догадка заключается в том, что в коде нет семантической разницы и что это всего лишь хороший стиль С++.

Херб Саттер написал в Exceptional C++ style: Chapter 18, что:

Предпочитают делать частные виртуальные функции.

Сопровождается, конечно, с подробным объяснением, почему это хороший стиль.

В контексте этого руководства первый пример хороший, потому что:

Функция void implementation() в примере может претендовать на виртуальность, так как здесь требуется выполнить настройку класса. Поэтому он должен быть закрытым.

И второй пример bad, так как:

Мы не должны вмешиваться в открытый интерфейс для выполнения настройки.

Мой вопрос:

  • Что мне не хватает в статическом полиморфизме? Это все о хорошем стиле С++?
  • Когда он должен использоваться? Каковы некоторые рекомендации?
4b9b3361

Ответ 1

Что мне не хватает в статическом полиморфизме? Это все о хорошем стиле С++?

Статический полиморфизм и полиморфизм времени выполнения - разные вещи и достигают разных целей. Они оба являются технически полиморфизмом, поскольку они решают, какой фрагмент кода выполнять по типу чего-либо. Полиморфизм времени выполнения откладывает привязку типа чего-либо (и, следовательно, кода, который выполняется) до времени выполнения, тогда как статический полиморфизм полностью разрешен во время компиляции.

Это приводит к плюсам и минусам для каждого. Например, статический полиморфизм может проверять допущения во время компиляции или выбирать среди вариантов, которые иначе не компилировались. Он также предоставляет тонны информации компилятору и оптимизатору, который может встроить, зная целиком вызовы и другую информацию. Но статический полиморфизм требует, чтобы реализации были доступны для компилятора для проверки в каждой единицы перевода, может привести к раздуванию размера двоичного кода (шаблоны представляют собой причудливые скопированные копии штанов) и не позволяют этим определениям встречаться во время выполнения.

Например, рассмотрим что-то вроде 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 */ } внутри - указан в терминах производных в подписях функций-членов.

Он может использоваться для полиморфных целей, но я не видел слишком много таких.

Ответ 2

Ссылка, которую вы указываете, повышает итераторы как пример статического полиморфизма. Итераторы 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(); }
};

Традиционный динамический полиморфизм не мог обеспечить вышеуказанную реализацию!

Связанным и важным термином является параметрический полиморфизм. Это позволяет реализовать аналогичные API-интерфейсы, например, в python, с помощью которого вы можете использовать любопытно повторяющийся шаблон шаблона на С++. Надеюсь, это будет полезно!

Я думаю, что стоит взять удар в источник всей этой сложности, и почему такие языки, как Java и С#, в основном стараются избегать этого: type erasure! В С++ нет полезной информации, содержащей тип Object с полезной информацией. Вместо этого у нас есть void*, и как только у вас есть void*, у вас действительно ничего нет! Если у вас есть интерфейс, который распадается на void*, единственный способ восстановить - это сделать опасные предположения или сохранить дополнительную информацию о типе.

Ответ 3

Хотя могут быть случаи, когда статический полиморфизм полезен (другие ответы перечислены несколькими), я обычно рассматривал бы это как плохое. Зачем? Поскольку вы больше не можете использовать указатель на базовый класс, вам всегда нужно предоставить аргумент шаблона, предоставляющий точный производный тип. И в этом случае вы можете точно использовать производный тип напрямую. И, если честно, статический полиморфизм - это не ориентация объекта.

Разница во времени между статическим и динамическим полиморфизмом - это, в точности, два указателя-указателя (если компилятор действительно вводит метод отправки в базовый класс, если по какой-то причине статический полиморфизм медленнее). Это не очень дорого, тем более, что второй поиск практически всегда попадает в кеш. В общем, эти запросы обычно дешевле самого вызова функции и, безусловно, стоят того, чтобы получить реальную гибкость, обеспечиваемую динамическим полиморфизмом.