Некоторое время назад ко мне подошли программисты, недавно начавшие изучать с++ и привыкшие к процедурному программированию, с жалобой на то, что механизм вызова виртуальных методов «тормозит». Я был очень удивлён.

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

Итого имеем следующие дополнительные затраты:

1) Дополнительный указатель в классе (указатель на таблицу виртуальных методов);
2) Дополнительный код в конструкторе класса (для инициализации виртуального табличного указателя);
3) Дополнительный код при каждом вызове виртуального метода (разыменование указателя на таблицу виртуальных методов и поиск по таблице нужного адреса виртуального метода).

К счастью, компиляторы сейчас поддерживают такую оптимизацию как девиртуализация (devirtualization). Суть её заключается в том, что виртуальный метод вызывается напрямую, если компилятор точно знает тип вызываемого объекта и таблица виртуальных методов при этом не используется. Такая оптимизация появилась довольно давно. Например, для gcc — начиная с версии 4.7, для clang'a начиная с версии 3.8 (появился флаг -fstrict-vtable-pointers).

Но всё же, можно ли пользоваться полиморфизмом без виртуальных функций вообще? Ответ: да, можно. На помощь приходит так называемый «странно повторяющийся шаблон» (Curiously Recurring Template Pattern или CRTP). Правда это будет уже статический полиморфизм. Он отличается от привычного динамического.

Давайте рассмотрим пример преобразования класса с виртуальными методами в класс с шаблоном:

class IA {
public:
   virtual void helloFunction() = 0;
};

class B : public IA {
public:
   void helloFunction(){
      std::cout<< "Hello from B";
   }
};

Превращается в:

template <typename T>
class IA {
public:
   void helloFunction(){
      static_cast<T*>(this)->helloFunction();
   }
};

class B : public IA<B> {
public:
   void helloFunction(){
      std::cout<< "Hello from B";
   }
};

Обращение:

template <typename T>
void sayHello(IA<T>* object) {
   object->helloFunction();
}

Класс IA принимает шаблоном порождённый класс и кастует указатель на this к порождённому классу. static_cast производит проверку приведения на уровне компиляции, следовательно, не влияет на производительность. Класс B порождён от класса IA, который в свою очередь шаблонизирован классом B.

Дополнительные затраты — дополнительный указатель в классе, дополнительный код в конструкторе класса, дополнительный код при каждом вызове виртуального метода, как в первом случае отсутствуют. Если ваш компилятор не поддерживает оптимизацию девиртуализации, то такой код будет работать быстрее и занимать меньше памяти.

Спасибо за внимание.

Надеюсь, кому-нибудь заметка будет полезна.
Поделиться с друзьями
-->

Комментарии (62)


  1. VYakushev
    16.08.2016 20:34
    +12

    Так и не понял почему же вызов виртуальных методов «тормозил» у новоиспеченных программистов.


    1. Sirikid
      16.08.2016 22:22
      +3

      Я тоже ожидал увидеть какой-нибудь хитровывернутый кейс, из-за которого немного неверный код новичков тормозил.


    1. elephanten
      17.08.2016 09:05

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


    1. Flame_xXx
      17.08.2016 09:10
      +1

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


      1. Tujh
        17.08.2016 09:45
        +1

        Может именно так и нужно было начинать эту статью, а то уж больно провокационное начало вышло :)


        1. Flame_xXx
          17.08.2016 10:23
          +2

          Возможно. Это моя первая статья. В дальнейшем буду иметь ввиду


      1. vladimirkolyada
        17.08.2016 09:59
        +2

        Так к функциональному или процедурному?:)


        1. Flame_xXx
          17.08.2016 10:10

          *процедурному


    1. Flame_xXx
      17.08.2016 10:51

      по-быстрому набросал пример.

      const int sayHelloCount = 100000;
      int counter = 0;
      
      class IA {
      public:
      	virtual void helloFunction() = 0;
              ....
      	virtual void helloFunction7() = 0;
      };
      
      class B : public IA {
      public:
      	void helloFunction() {
      		counter++;
      	}
              .....
      	void helloFunction7() {
      		counter--;
      	}
      
      };
      
      void sayHello(IA* a) {
      	for (int i = 0; i < sayHelloCount; i++) {
      		a->helloFunction7();
      	}
      }
      
      
      int main() {
      
      	IA* a = new B;
      	LARGE_INTEGER time_n, time_s;
      	QueryPerformanceCounter(&time_s);
      	sayHello(a);
      	QueryPerformanceCounter(&time_n);
      	auto difference = time_n.QuadPart - time_s.QuadPart;
      
      	std::cout << difference;
      	delete a;
      	system("pause");
      	return 0;
      }
      
      

      вывод:
      672Для продолжения нажмите любую клавишу . . .
      


      сравним с
      const int sayHelloCount = 100000;
      int counter = 0;
      
      template <typename T>
      class IA {
      public:
      	void helloFunction()	{
      		static_cast<T*>(this)->helloFunction();
      	};
              ....
      	void helloFunction7() {
      		static_cast<T*>(this)->helloFunction7();
      	}
      };
      
      class B : public IA<B>
      {
      public:
      	void helloFunction() {
      		counter++;
      	}
              ....
      	void helloFunction7() {
      		counter--;
      	}
      
      };
      
      template <typename T>
      void sayHello7(IA<T>* a) {
      	for (int i = 0; i < sayHelloCount; i++) {
      		a->helloFunction7();
      	}
      }
      
      
      int main() {
      
      	B *b = new B;
      	LARGE_INTEGER time_n, time_s;
      	QueryPerformanceCounter(&time_s);
      	sayHello7(b);
      	QueryPerformanceCounter(&time_n);
      	auto difference = time_n.QuadPart - time_s.QuadPart;
      
      	std::cout << difference;
      	delete b;
      	system("pause");
      	return 0;
      }
      

      вывод:
      1Для продолжения нажмите любую клавишу . . .
      


      1. iCpu
        17.08.2016 11:08

        У вас во втором случае бесполезный вызов вырежет компилятор. Нужно выводить его в консоль. И, желательно получать начальное значение из рандома.


        1. Flame_xXx
          17.08.2016 11:13

          Сделал. Ситуация не поменялась


          1. iCpu
            17.08.2016 11:50
            +2

            https://ideone.com/f17DTU
            У меня получились немного другие результаты.
            Дабы не было недопонимания, я не собираюсь оспаривать то, что дёргать виртуальный метод — медленнее, чем дёргать указатель на функцию. Я оспариваю какие-то сверхъестественные числа, которые можно получить только после оптимизации под корень. Медленнее в 5-6 раз, но не на порядки.


            1. Flame_xXx
              17.08.2016 12:03

              Согласен с Вами. Во втором случае компилятор действительно что-то подозрительно наоптимизировал. Но результат одинаковый. Сейчас разбираюсь в чём подвох. Я сам, честно говоря, ожидал разницы в 5-8 раз. Кстати, в Вашем примере всего один виртуальный метод. В моём же было 7


              1. iCpu
                17.08.2016 12:20
                +2

                Ваше право сделать форк и исправить. А наоптимизировал он простую вещь: если 100000 раз вызывать ++, почему бы сразу не вызвать += 100000 всего 1 раз?

                А ещё я слил карму у долбаных функциональщиков и не могу в тэги, что печально. Долбаные функциональщики, размечтались, что могут реализовать одну лишь инкапсуляцию и назвать её ООП! Что эти нигеры себе позволяют!?!


                1. Flame_xXx
                  17.08.2016 12:40

                  Да, действительно, результаты получаются похожими на Ваши. Добавил +rand() и вывод в лог непосредственно в helloFunction7. Значит, мой компилятор лучше оптимизирует код с шаблонами, а об виртуальные функции чаще спотыкается, раз такая разница была в первом случае


  1. ukhegg
    16.08.2016 21:19
    +1

    Если не ошибаюсь, то до полиморфизма здесь далеко. Имеем дело с простым вызовом функций с одинаковым именем у совершенно разных классов.

    struct C : public IA<C>{
        std::string helloFunction(std::string param = "Say hello param") {
            cout<< "Hello from C"; }
    };
    ...
    C c;
    sayHello(&c);
    

    выведет «Hello from C», а вот сигнатура функции уже сооовсем другая


    1. sermp
      16.08.2016 23:33
      +1

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


      1. Flame_xXx
        18.08.2016 14:56

        Да, Вы правы, ещё такой полиморфизм называют обобщённым программированием или статическим полиморфизмом


  1. Error1024
    16.08.2016 21:29
    -4

    О, кажеться еще один способ выстрелить себе в ногу :)


    1. DarkEld3r
      17.08.2016 11:17

      Cпособ совсем не новый.


      1. Error1024
        17.08.2016 18:49

        Я и не писал что он новый, просто ещё один в копилку способов. Лично я просто не догадлся бы так делать.
        Ещё же надо найти test case при котором виртуальные методы тормозят.


  1. DistortNeo
    16.08.2016 22:07
    +5

    Отличие между CRTP и виртальными функциями принципиальное, т.к. CRTP — это статический полиморфизм через шаблоны, а не динамические вызовы.

    Без CRTP вызов функции бы выглядел как:

    template <typename T>
    void sayHello(T* object) {
       object->helloFunction();
    }
    


    CRTP в данном случае играет роль концептов, которые всё никак не введут в C++.
    Минус CRTP — легко выстрелить в ногу, забыв переопределить метод.


    1. monah_tuk
      17.08.2016 07:37

      Минус CRTP — легко выстрелить в ногу, забыв переопределить метод.

      если использовать в связке с NVI. Точнее тут не совсем NVI, но суть та же: интерфейс (в шаблоне) и реализация — разные методы, с разным именем и/или сигнатурой. В таком случае, если забудешь реализацию и где-то будет вызов интерфейса — будет ошибка компиляции.


      Т.е. что-то вроде:


      template <typename T>
      class IA {
      public:
         helloFunction(){
            static_cast<T*>(this)->doHelloFunction();
         }
      };

      минус тут в том, что эта самая doHelloFunction() тоже должна быть публичной (public:), что бы можно было её вызвать из базового класса. В NVI такой проблемы нет. Ну или делать в наследнике что-то вроде:


      friend class IA<C>;

      А так, IMHO, CRTP чаще используется когда нужен реюз кода, и не нужно наследование в виде "C является IA" ("is a").


  1. cranium256
    16.08.2016 22:31
    +1

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

    2. Я правильно понимаю, что при использовании CRTP при вызове helloFunction() для экземпляра базового класса программа уйдёт в нирвану до момента исчерпания стека?


    1. Sirikid
      16.08.2016 23:04

      2. Да, вот тут бы пригодились абстрактные классы C#/Java


    1. Flame_xXx
      17.08.2016 09:25

      1. Да, пропустил. Спасибо, поправил.
      2. Да. Как выше писали, «это еще один способ выстрелить себе в ногу». Причём довольно легко и непринуждённо :)


    1. Dudraug
      18.08.2016 13:01

      #include <iostream>
      #include <type_traits>
      
      #define HAS_MEM_FUNC(func, name)                                            template<typename T, typename Sign>                                     struct name {                                                               typedef char yes[1];                                                    typedef char no [2];                                                    template <typename U, U> struct type_check;                             template <typename _1> static yes &chk(type_check<Sign, &_1::func > *);         template <typename   > static no  &chk(...);                            static bool const value = sizeof(chk<T>(0)) == sizeof(yes);         }
      
      HAS_MEM_FUNC(Do, has_do);
      
      
      
      
      template<class T>
      class IA
      {
      public:
      	void Do()
      	{
      
      		static_assert(has_do<T, void(T::*)()>::value, "Derived class has no Do function");
      		static_cast<T*>(this)->Do();
      	}
      protected:
      	IA() {}
      	IA(const IA&) {}
      	IA(IA&&) {}
      protected:
      };
      
      
      template<class WorkingClass>
      void RunDo(IA<WorkingClass>& p)
      {
      	p.Do();
      }
      
      class C : public IA<C>
      {
      
      };
      
      
      
      
      class B : public IA<B>
      {
      public:
      	void Do()
      	{
      		std::cout << "I'm B!!!" << std::endl;
      	}
      };
      
      
      int main()
      {
      	B b;
      	RunDo(b);
      	C c;
      //	RunDo(c);
      //	IA<B> ia;
      //	RunDo(ia);
      }
      


    1. Flame_xXx
      18.08.2016 17:09

      2. Можно добавить проверку std::is_same<IA,T>


  1. Door
    16.08.2016 22:40

    Как по мне — так тут один решающий недостаток — нельзя написать просто: std::vector<std::unique_ptr<IA>>


  1. qw1
    16.08.2016 22:54
    +3

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


    1. DistortNeo
      16.08.2016 23:16

      Конечно. Это «полиморфизм», разрешающийся на этапе компиляции.


      1. zagayevskiy
        16.08.2016 23:30
        +5

        Почему в кавычках? Это статический полиморфизм.


    1. SBKarr
      17.08.2016 09:11

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


    1. Dudraug
      18.08.2016 13:23
      +1

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


  1. snizovtsev
    16.08.2016 23:32
    +1

    Кто то не знал об CRTP и его недостатках? Уже в C++11 особой надобности в нем нет: если расставить final и использовать конкретные типы, то компилятору не придется гадать о применимости девиртуализации. И волки сыты (код быстрый), и овцы целы (readability, type checking). А если еще использовать LTO… (вот на эту тему было бы интересно увидеть статью).


    А чтобы понять, как правильно применять статический полиморфизм — посмотрите устройство traits в rust. В C++ пока нехватает фич (концептов и модулей), чтобы такой код можно было широко применять в продакшне.


    1. monah_tuk
      17.08.2016 07:41

      CRTP, как минимум, очень полезен при реюзе кода, когда наследование используется, но отношение "is a" неприменимо или применимо с явной натяжкой, так что он пригодится в C++11, и 14, и 17 и т.д.


      1. iCpu
        17.08.2016 08:03

        Погодите, наследуеся но не является? Если это импорт части реализации, тогда это агрегация. А если это ни то, ни другое, то это просто дерьмовый дизайн.


        1. monah_tuk
          17.08.2016 08:56

          Если это импорт части реализации, тогда это агрегация.

          именно, что импорт общей и обобщённой реализации. Просто покажите, как это сделать красиво и удобно с точки зрения пользователя интерфейса класса в C++ чистой агрегацией. Потому как я воспринимал до сиго момента агрегацию как:


          class Foo {
          ...
          };
          
          class Bar {
              Foo m_foo;
          ...
          };

          и как красиво и без лишнего кода вытащить интерфейсы Foo, как часть интерфейса Bar в таком случае — я слабо представляю. Особенно когда обобщённый код должен знать о типе агрегатора, простой пример:


          template<typename T>
          struct Creator
          {
            static std::unique_ptr<T> create() {...}
          };
          
          struct Foo : public Creator<Foo>
          {};
          
          ...
          auto ptr = Foo::create();

          Foo я не является Creator'ом, но подмешивается единожды написанная обобщённая функциональность.


          1. iCpu
            17.08.2016 09:26
            +1

            Ну, так и есть, дерьмовый дизайн. И не нужно этого стыдиться.

            Foo я не является Creator'ом, но подмешивается единожды написанная обобщённая функциональность.
            Ну да, не является Creator, но является Creator(Foo)
            Пруфы в студию!

            Мне интересно, где такое могло понадобиться?


            1. monah_tuk
              17.08.2016 11:07

              Ну, так и есть, дерьмовый дизайн. И не нужно этого стыдиться.

              Ок. Но я до сих пор не вижу вашего примера.


              Ну да, не является Creator, но является Creator(Foo)

              Стоп. С точки зрения языка, да, Foo is a Creator<Foo>, я это даже не пытался оспаривать. Но, внимание, аналогичный код для Bar будет давать Creator<Bar>, при этом Creator<Foo> и Creator<Bar> — это разные классы. Как следствие, при таком подходе не создаётся иерархии классов с общим корнем. Таким образом, с точки зрения языка — это наследование, но по сути — это не реализация отношения "is a", а подмешивания готовой функциональности к данному классу, способ реюза кода.


              1. iCpu
                17.08.2016 11:24
                +1

                Хммм… Меня замкнуло на контексте поста. А если подумать, я был неправ. Опять. Что ж, бывает. Примите мои извинения.


            1. monah_tuk
              17.08.2016 11:29
              +1

              Мне интересно, где такое могло понадобиться?

              Вот, кстати, достаточно интересная статья на тему: http://scrutator.me/post/2014/06/26/crtp_demystified.aspx (пропускаем первую часть про статический полиморфизм и переходит к смешиванию типов) и там же примеры: Boost.Operators (что куда лучше и интереснее std::rel_ops), трюк с std::enable_shared_from_this.


      1. fck_r_sns
        17.08.2016 09:40
        +1

        когда наследование используется, но отношение «is a» неприменимо

        Это по определению приватное наследование. Хотя лучше использовать агрегацию, как уже рядом заметили.


        1. iCpu
          17.08.2016 10:28
          -2

          Нет, приватное наследование не скрывает свойство «является», скрываются лишь методы базового класса.


          1. fck_r_sns
            17.08.2016 10:45
            +3

            Что Вы имеете в виду под «не скрывает свойство «является»»?

            class Base {};
            class Derived : private Base {};
            ...
            Base *b = new Derived();
            ...
            

            Мы получим ошибку компиляции: "'Base' is an inaccessible base of 'Derived'"
            В C++ отношение «is a» достигается только при публичном наследовании. При приватном/защищенном мы не можем пользоваться объектом производного класса через указатель на базовый класс, а это значит, что отношение «is a» не применимо. То есть это свойство не то что «не скрыто» — его просто нет.


            1. iCpu
              17.08.2016 10:47

              Извиняюсь, не знал.


        1. monah_tuk
          17.08.2016 11:07

          При приватном наследовании, как минимум, придётся вручную вытягивать (using ...) в паблик нужные методы из базового класса, ради которых, быть может, всё и затевалось: http://ideone.com/v85SqB. Плюс примера, показывающего использование агрегации для расширения интерфейса класса я всё ещё не увидел в данной ветке.


          Я сам люблю агрегацию. Я не отказываюсь от приватного наследования (привет noncopyable), но иногда случаются ситуации, когда появляются методы в несвязанных классах, почти строка в строку повторяющие друг друга с мелкими отличиями в деталях, вроде имени класса или около того. Обычно такие методы несут какой-то утилитарных характер. Собственно в таких ситуациях возникает вопрос: а как минимальным объёмом кода, не создавая новой иерархии зареюзать подобный код?


          1. fck_r_sns
            17.08.2016 11:13

            Плюс примера, показывающего использование агрегации для расширения интерфейса класса я всё ещё не увидел в данной ветке.

            Элегантного способа в C++ нет, насколько я знаю.
            Не элегантный — делегирование/проксирование.


            1. monah_tuk
              17.08.2016 11:45
              +1

              Не элегантный — делегирование/проксирование.

              CRTP я бы сюда тоже добавил. Причём по объёму дополнительного кода он, возможно, будет даже самым оптимальным (см https://habrahabr.ru/post/307902/#comment_9754908), но тоже не без своих заморочек.


  1. semenyakinVS
    16.08.2016 23:46
    -1

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

    Вообще же — действительно интересно было бы глянуть кейс, при котором понадобились такие неприятные оптимизации.

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


  1. Antervis
    17.08.2016 07:16

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


    1. monah_tuk
      17.08.2016 07:46
      +2

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


  1. Amomum
    17.08.2016 11:10

    Еще небольшой минус CRTP — приходится выдумывать однообразные названия для методов: hello() -> sayHello() -> doSayHello() — потому что одинаково их называть на разных уровнях иерархии нельзя.


    1. Antervis
      17.08.2016 13:19

      Можно же

      проще
      class Base {
      public:
          template <typename T = Base>
          inline void func() {
              if (!std::is_same<T,Base>::value)
                  static_cast<T*>(this)->func();
              else
                  std::cout << "base" << std::endl;
          }
      };
      
      class Derived : public Base {
      public:
          void func() { std::cout << "derived" << std::endl; }
      };
      
      class SecondDerived : public Derived {
      public:
          void func() { std::cout << "second derived" << std::endl; }
      };
      
      int main() {
          Base b; b.func();
          Derived d; d.func();
          SecondDerived sd; sd.func();
      }
      


      1. Shamov
        17.08.2016 14:54

        Можно ещё проще:

        Скрытый текст
        class Base {
        public:
            void func() { std::cout << "base" << std::endl; }
        };
        
        class Derived : public Base {
        public:
            void func() { std::cout << "derived" << std::endl; }
        };
        
        class SecondDerived : public Derived {
        public:
            void func() { std::cout << "second derived" << std::endl; }
        };
        
        int main() {
            Base b; b.func();
            Derived d; d.func();
            SecondDerived sd; sd.func();
        }
        


  1. Shamov
    17.08.2016 13:05
    +1

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


    1. Sirikid
      17.08.2016 22:25
      -3

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


      1. Shamov
        17.08.2016 22:33
        +3

        Причём лучшая половина. Другая же половина ничем не примечательна и есть в любом другом языке.


  1. maydjin
    17.08.2016 18:29
    +2

    [-||||-]


    Да и не ради производительности этот шаблон шаблонного программирования на c++ используют в основном, а для наследования реализации.


    Рекомендую почитать Вандервуда и Джосаттиса "Шаблоны C++. Справочник разработчика", сиё чтиво слегка устарело конечно, но в общем и целом хорошая вводная в метапрограммирование на плюсах.


    1. Shamov
      17.08.2016 19:09
      +1

      Плохо рекламируете :) «Наследование реализации» звучит как-то несногсшибательно. Сразу возникает мысль: «Зачем мне это надо? Наверное, фигня какая-то.» Нужна более привлекательная приманка. Надо сказать, что наследование реализации, полученной через параметр шаблона, позволяет убрать код всех нешаблонных методов в сpp-файл.


  1. skor
    18.08.2016 14:28
    +1

    Довольно низкий уровень статьи, вводит в заблуждение.


  1. Dudraug
    18.08.2016 16:48
    -2

    здесь небольшое улучшение этого шаблона, «на 95% безопасно»=)