Comments 62
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Для продолжения нажмите любую клавишу . . .
У меня получились немного другие результаты.
Дабы не было недопонимания, я не собираюсь оспаривать то, что дёргать виртуальный метод — медленнее, чем дёргать указатель на функцию. Я оспариваю какие-то сверхъестественные числа, которые можно получить только после оптимизации под корень. Медленнее в 5-6 раз, но не на порядки.
А ещё я слил карму у долбаных функциональщиков и не могу в тэги, что печально. Долбаные функциональщики, размечтались, что могут реализовать одну лишь инкапсуляцию и назвать её ООП! Что эти нигеры себе позволяют!?!
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», а вот сигнатура функции уже сооовсем другая
Без CRTP вызов функции бы выглядел как:
template <typename T>
void sayHello(T* object) {
object->helloFunction();
}
CRTP в данном случае играет роль концептов, которые всё никак не введут в C++.
Минус CRTP — легко выстрелить в ногу, забыв переопределить метод.
Минус 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").
2. Я правильно понимаю, что при использовании CRTP при вызове helloFunction() для экземпляра базового класса программа уйдёт в нирвану до момента исчерпания стека?
2. Да. Как выше писали, «это еще один способ выстрелить себе в ногу». Причём довольно легко и непринуждённо :)
#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);
}
Как по мне — так тут один решающий недостаток — нельзя написать просто: std::vector<std::unique_ptr<IA>>
Кто то не знал об CRTP и его недостатках? Уже в C++11 особой надобности в нем нет: если расставить final и использовать конкретные типы, то компилятору не придется гадать о применимости девиртуализации. И волки сыты (код быстрый), и овцы целы (readability, type checking). А если еще использовать LTO… (вот на эту тему было бы интересно увидеть статью).
А чтобы понять, как правильно применять статический полиморфизм — посмотрите устройство traits в rust. В C++ пока нехватает фич (концептов и модулей), чтобы такой код можно было широко применять в продакшне.
CRTP, как минимум, очень полезен при реюзе кода, когда наследование используется, но отношение "is a" неприменимо или применимо с явной натяжкой, так что он пригодится в C++11, и 14, и 17 и т.д.
Если это импорт части реализации, тогда это агрегация.
именно, что импорт общей и обобщённой реализации. Просто покажите, как это сделать красиво и удобно с точки зрения пользователя интерфейса класса в 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
'ом, но подмешивается единожды написанная обобщённая функциональность.
Foo я не является Creator'ом, но подмешивается единожды написанная обобщённая функциональность.Ну да, не является Creator, но является Creator(Foo)
Пруфы в студию!
Мне интересно, где такое могло понадобиться?
Ну, так и есть, дерьмовый дизайн. И не нужно этого стыдиться.
Ок. Но я до сих пор не вижу вашего примера.
Ну да, не является Creator, но является Creator(Foo)
Стоп. С точки зрения языка, да, Foo
is a Creator<Foo>
, я это даже не пытался оспаривать. Но, внимание, аналогичный код для Bar
будет давать Creator<Bar>
, при этом Creator<Foo>
и Creator<Bar>
— это разные классы. Как следствие, при таком подходе не создаётся иерархии классов с общим корнем. Таким образом, с точки зрения языка — это наследование, но по сути — это не реализация отношения "is a", а подмешивания готовой функциональности к данному классу, способ реюза кода.
Мне интересно, где такое могло понадобиться?
Вот, кстати, достаточно интересная статья на тему: http://scrutator.me/post/2014/06/26/crtp_demystified.aspx (пропускаем первую часть про статический полиморфизм и переходит к смешиванию типов) и там же примеры: Boost.Operators (что куда лучше и интереснее std::rel_ops
), трюк с std::enable_shared_from_this
.
когда наследование используется, но отношение «is a» неприменимо
Это по определению приватное наследование. Хотя лучше использовать агрегацию, как уже рядом заметили.
class Base {};
class Derived : private Base {};
...
Base *b = new Derived();
...
Мы получим ошибку компиляции: "'Base' is an inaccessible base of 'Derived'"
В C++ отношение «is a» достигается только при публичном наследовании. При приватном/защищенном мы не можем пользоваться объектом производного класса через указатель на базовый класс, а это значит, что отношение «is a» не применимо. То есть это свойство не то что «не скрыто» — его просто нет.
При приватном наследовании, как минимум, придётся вручную вытягивать (using ...
) в паблик нужные методы из базового класса, ради которых, быть может, всё и затевалось: http://ideone.com/v85SqB. Плюс примера, показывающего использование агрегации для расширения интерфейса класса я всё ещё не увидел в данной ветке.
Я сам люблю агрегацию. Я не отказываюсь от приватного наследования (привет noncopyable
), но иногда случаются ситуации, когда появляются методы в несвязанных классах, почти строка в строку повторяющие друг друга с мелкими отличиями в деталях, вроде имени класса или около того. Обычно такие методы несут какой-то утилитарных характер. Собственно в таких ситуациях возникает вопрос: а как минимальным объёмом кода, не создавая новой иерархии зареюзать подобный код?
Плюс примера, показывающего использование агрегации для расширения интерфейса класса я всё ещё не увидел в данной ветке.
Элегантного способа в C++ нет, насколько я знаю.
Не элегантный — делегирование/проксирование.
Не элегантный — делегирование/проксирование.
CRTP я бы сюда тоже добавил. Причём по объёму дополнительного кода он, возможно, будет даже самым оптимальным (см https://habrahabr.ru/post/307902/#comment_9754908), но тоже не без своих заморочек.
Вообще же — действительно интересно было бы глянуть кейс, при котором понадобились такие неприятные оптимизации.
П.С.: «а в сам класс добавляет виртуальный табличный указатель» — возможно я неправильно понял мысль, но, если я не ошибаюсь, указатель на таблицу виртуальных функций располагается в рамках объекта, а не класса — чтобы ни имелось в виду в качестве «добавления в класс» (ссылка по теме).
Вы забыли про косвенную адресацию при работе с таблицей указателей. Нынче рандомные обращение к памяти не сильно эффективная штука ;-) Но в остальном да — ооооочень интересен кейс, где не хватило возможностей компилятора и вылезли тормоза. Тем паче, что в C-style полиморфизме (структуры с полями-указателями на функцию, которая принимает эту структуру) проблема ровно та же с косвенной адресацией, но только ручками.
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();
}
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();
}
Только это не совсем то, что нужно.
[-||||-]
Да и не ради производительности этот шаблон шаблонного программирования на c++ используют в основном, а для наследования реализации.
Рекомендую почитать Вандервуда и Джосаттиса "Шаблоны C++. Справочник разработчика", сиё чтиво слегка устарело конечно, но в общем и целом хорошая вводная в метапрограммирование на плюсах.
Преобразование обычного класса в странно повторяющийся шаблон