Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

CRTP. Static polymorphism. MixIn. Размышления на тему

Anna | 24.06.2014 | нет комментариев
В этом посте я поразмышляю на тему статического полиморфизма в С , архитектурных решениях, строящихся на его основе. Разгляжу увлекательную идиому — CRTP. Приведу несколько примеров ее применения. В частности, разгляжу доктрину MixIn классов. Пишу, Дабы классифицировать личные познания, но может быть и вы сумеете обнаружить что-то увлекательное для себя.

Вступление

Как вестимо, С является мультипарадигмовым языком. На нем дозволено писать в процедурном жанре, применять языковые конструкции, обеспечивающие поддержку объектно-ориентированного программирования, образцы делают допустимым обобщенное программирование, STL и новые вероятности языка (lambda, std::function, std::bind) разрешают при желании писать в функциональном жанре в рантайме, а метапрограммирование образцов представляет собой функциональное программирование в чистом виде вcompile time.
Невзирая на то, что в всякий реальной огромный программе скорее каждого дозволено встретить смесь всех этих техник, объектно-ориентированная парадигма, реализуемая с поддержкой доктрины классов, открытого интерфейса и закрытой реализации (инкапсуляция), наследования, и динамического полиморфизма, реализуемого посредством виртуальных функций, бесспорно, является особенно обширно применяемой.

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

Статический полиморфизм

В то время как динамический полиморфизм является полиморфизмом времени выполнения и очевидных интерфейсов, статический полиморфизм является полиморфизмом времени компиляции и неявных интерфейсов. Давайте разберемся что это значит.
Глядя на дальнейший код

void process(base* b)
{
	b->prepare();
	b->work();
	...
}

мы можем сказать следующее: переданный в функцию process() указатель, должен указывать на объект, реализующий интерфейс (наследующий) base, и выбор реализаций функций prepare() и work() будет осуществлен во время выполнения программы в зависимости от того на объект какого именно производного отbase типа указывает b.

Если же мы разглядим следущий код:

template<typename T>
void process(T t)
{
	t.prepare();
	t.work();
}

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

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

Непредвзятые же поводы дозволено так либо напротив свести к тому, что позже инстанциирования шаблонные классы (функции) имеют различные, Зачастую никак не связанные друг с ином типы.
Отчего это нехорошо? Объекты таких типов без дополнительных ухищрений (см. boost::variant, boost::tuple, boost::any, boost::fusion etc.) немыслимо положить в один контейнер и следственно пакетно обработать. Немыслимо, скажем, во время исполнения подменить объект — член класса, объектом иного типа, в рамках реализации “Стратегии” либо “Состояния”. И правда и эти паттерны дозволено реализовать и другими методами без классовых иерархий, скажем применяя std::function либо легко указатели на функции, лимитация, тем не менее, на лицо.

Но никто не принуждает нас сурово придерживаться какой-то одной парадигмы. Самые сильные, эластичные и увлекательные решения появляются на стыке этих 2-х подходов, на стыке ООП парадигмы и genericпарадигмы. Идиома CRTP как раз и является одним из примеров такого слияния парадигм.

CRTP

CRTP (Curiously Recurring Template Pattern) — это идиома проектирования, заключающаяся в том, что класс наследует от базового шаблонного класса с самим собой в качестве параметра образца базового класса. Звучит запутано, но в коде выглядит достаточно легко.

template <class T> class base{};
class derived : public base<derived> {};

Что это может нам дать? Такая конструкция делает допустимым обращение к производному классу из базового.

template<typename D>
struct base
{
	void foo() {static_cast<D*>(this)->bar();}
};

struct derived : base<derived>
{
	void bar();
};

А вероятность такой коммуникации, в свою очередь, открывает несколько увлекательных вероятностей.

Очевидный интерфейс

В главе про статический полиморфизм я назвал неимение очевидных интерфейсов субъективным недостатком статического полиморфизма. На эту тему дозволено спорить, но так либо напротив, очевидный интерфейс нетрудно определить, применяя CRTP. Подлинно, мы можем определить комплект непременных функций интерфейса через вызовы этих функций из базового класса.

template<typename D>
struct base_worker
{
	void work() {static_cast<D*>(this)->work_impl();}
    void prepare() {static_cast<D*>(this)->prepare_impl();}
};

struct some_concrete_worker : base_worker<some_concrete_worker>
{
	void work_impl();    // Без этих функций дерзкий
	void prepare_impl(); // код не скомпилируется
};

template<typename Worker>
void polymorhic_work(const Worker& w)
{
	w.prepare();
	w.work();
};

int main()
{
	some_concrete_worker w1;
	some_concrete_worker_2 w2;
	polymorhic_work(w1); // Скомпилируется только при наличии
	polymorhic_work(w2); // функций prepare_impl() и work_impl() в w1 и w2
}

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

MixIn

MixIn — это прием проектирования, при котором класс (интерфейс, модуль и т.п.) реализует некоторую функциональность, которую дозволено “подмешать”, внести в иной класс. Независимо же MixIn класс традиционно не применяется. Данный прием не является специфическим для С , и в некоторых других языках он поддерживается на ярусе языковых конструкций.
В С нет нативной поддержки MixIn’ов, но тем не менее эту идиому абсолютно дозволено реализовать с поддержкой CRTP.
Скажем, MixIn класс может реализовывать функциональность синглтона либо подсчета ссылок на объект. А для того Дабы применять такой класс довольно отнаследовать от него с “собой” в качестве параметра образца.

template<typename D>
struct singleton{...};

class my_class : public singleton<my_class>{...};

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

struct singleton{...};
class my_class : singleton{...};

Дело в том, что внутри MixIn’а нам необходим доступ к функциям наследуемого класса (в случае синглтона к конструктору) и тут на поддержка приходит CRTP. И если пример с синглтоном кажется надуманным (подлинно, кто сегодня использует синглтон?), то ниже вы обнаружите два больше близких к действительности примера.

Enable_shared_from_this

MixIn конструкция (boost)std::enable_shared_from_this разрешает получить shared_ptr на объект, не создавая новую группу владения.

struct bad
{
	std::shared_ptr<bad> get() {return std::shared_ptr<bad>(this);}
};

В этом случае всякий shared_ptr, полученный с поддержкой функции bad::get(), открывает новую группу владения объектом, и когда настанет время разрушения shared_ptr’ов, delete для нашего объекта вызовется огромнее чем один раз.

Верно же делать вот так:

struct good : std::enable_shared_from_this<good>
{
	std::shared_ptr<good> get() 
  {
    return shared_from_this(); // Эта функция наследуется
                           // из enable_shared_from_this
  }
};

Устроена эта вспомогательная конструкция приблизительно так:

template<typename T>
struct enable_shared
{
	weak_ptr<T> t_;
	enable_shared()
	{
		t_ = weak_ptr<T>(static_cast<T*>(this));
	}
	shared_ptr<T> shared_from_this()
	{
		return shared_ptr<T>(t_); 
	}
};

Как видите, тут CRTP разрешает базовому классу “увидеть” тип производного класса и воротить указатель именно на него.

MixIn функции

MixIn функциональность не непременно должна быть включена вовнутрь некоторого класса. Изредка допустимо ее реализовать в виде свободной функции. В качестве примера реализуем оператор “!=” для всех классов, у которых определен оператор “==”.

template<typename D>
struct non_equalable{};

template<typename D>
bool operator != (const non_equalable<D>& lhs, const non_equalable<D>& rhs)
{
    return !(static_cast<const D&>(lhs) == static_cast<const D&>(rhs));
}

Как видите, внутри operator != мы используем тот факт, что non_equalable может воспользоваться оператором “==”, определенным в производном типе.
Применять данный MixIn дозволено дальнейшим образом:

struct some_struct : non_equalable<some_struct>
{
    some_struct(int w) : i_(w){}
    int i_;
};

bool operator == (const some_struct& lhs, const some_struct& rhs)
{
    return lhs.i_ == rhs.i_;
}

int main()
{
    some_struct s1(3);
    some_struct s2(4);
    std::cout << (s1 != s2) << std::endl;
}

MixIn напротив

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

class space_ship
{
public:
    // ...
    void move()
    {
        if(!fuel()) return;
        int current_speed = speed();
	  // further actions ...
    }
    virtual ~space_ship(){}
private:
    virtual bool fuel() const = 0;
    virtual int speed() const = 0;
};

class interceptor : public space_ship
{
public:
    // ...
private:
    bool fuel() const { ... }
    int speed() const { ... }
};

class other_ship : public space_ship { ... };
class other_ship_2 : public space_ship { ... };
// …

Сейчас испробуем применить CRTP.

template<typename D>
class space_ship
{
public:
    void move()
    {
        if(!static_cast<D*>(this)->fuel())
            return;
        int current_speed = static_cast<D*>(this)->speed();
	  // ...
    }
};

class interceptor : public space_ship<interceptor>
{
public:
    bool fuel() const;
    int speed() const;
};

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

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB