Почему реализация CRTP и методы интерфейса называются по-разному?

Везде, где я читаю о 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, что угодно …), я получу бесконечный цикл, так что он может делать ошибки такого рода немного сложнее найти, так как они не попадают во время компиляции. Однако, кроме этого, это почти так же просто, как простые виртуальные функции, и мне не нужно бороться с сокрытием методов реализации. Это кажется простым и элегантным, но никто, кажется, не использует его … тогда мой вопрос, в чем прикол? Почему этот подход не используется? Разве скрытие методов реализации не имеет большого значения? Или там скрывается какая-то неизмеримо зловещая злая сила, готовая поглотить мою душу, как только я попробую этот подход в реальном проекте?

5

Решение

Вы в значительной степени указали причину: неудача в реализации функции приводит к бесконечному циклу.

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

1) Если базовый класс имеет собственную реализацию, он ведет себя как обычная виртуальная функция, в которой базовый класс имеет реализацию по умолчанию.
2) Если базовый класс не предоставляет реализацию, он не компилируется. Опять же, это похоже на чисто виртуальную функцию.

Наконец, есть некоторые (например, Херб Саттер), которые предлагают всегда отделять интерфейс (публичную функцию) от реализации (приватная функция) при использовании виртуальных методов. Дать http://www.gotw.ca/publications/mill18.htm читать. Выполняя разделение как часть CRTP, вы получаете те же преимущества.

4

Другие решения

За CRTP не скрывается «сила зла», кроме, возможно, проблемы, о которой вы упомянули, с одинаково именованными интерфейсными функциями и их реализациями, но это немного похоже на «навязывание проблем», на мой взгляд.

Что касается вопроса «Почему этот подход не используется?», Я не думаю, что это так. Этот подход широко используется когда это необходимо; но никакой шаблон дизайна не имеет смысла все время. Каждый из них пригодится в определенной ситуации проектирования, и CRTP не является исключением.

CRTP в основном полезен, когда у вас есть несколько, несвязанный классы, которые поддерживают общий интерфейс, но понять это немного (не совсем) по-другому. Если любой из этих слов, которые я выделил жирным шрифтом, не описывается ваш вариант использования, вероятно, CRTP не имеет смысла:

  • «Несколько»: Если у вас есть только один такой класс вместо многих, зачем выделять его интерфейс в суперкласс? Это только вводит некоторое избыточное соглашение об именах;
  • «Unrelated»: Если ваши классы связаны, это означает, что они являются производными от общего базового класса, потому что необходим полиморфизм времени выполнения (это очень часто имеет место, например, если вы хотите иметь гетерогенный набор объектов), то часто становится естественным включить общий интерфейс в этот базовый класс;
  • «Общие»: Если ваши классы не делятся достаточно обширный общий интерфейс, не так много, чтобы выделить в базовый класс. Если все ваши занятия только иметь size() общий метод, например, создание базового класса только для хранения этого метода, вероятно, является бесполезно тонкой гранулярностью факторинга;
  • «Немного»: Если ваши интерфейсы реализованы в совершенно разные кстати, значит нет общего реализация, тогда нет смысла создавать базовый класс, который просто перенаправляет все вызовы функций в подклассы. Зачем?

Когда ваша дизайнерская ситуация, однако, такова, что все из четырех указанных выше свойств, у вас определенно есть сценарий использования для CRTP: нет накладных расходов на виртуальные функции, компилятор может полностью оптимизировать ваш код, у вас есть четкое разделение интерфейса и реализации, вы достигаете минимальной избыточности, захватывая общую реализацию логика и тд.

Тем не менее, вы можете понять, что эта ситуация просто не тот общий. Надеюсь, это ответит на ваш вопрос.

5

По вопросам рекламы ammmcru@yandex.ru
Adblock
detector