Как раз то, о чем спрашивает тема. Также хочу знать, почему ни в одном из обычных примеров CRTP не упоминается virtual
dtor.
РЕДАКТИРОВАТЬ: Ребята, пожалуйста, опубликуйте также о проблеме CRTP, спасибо.
Как раз то, о чем спрашивает тема. Также хочу знать, почему ни в одном из обычных примеров CRTP не упоминается virtual
dtor.
РЕДАКТИРОВАТЬ: Ребята, пожалуйста, опубликуйте также о проблеме CRTP, спасибо.
Только виртуальные функции требуют динамической отправки (и, следовательно, поиска vtable), и даже не во всех случаях. Если компилятор может определить во время компиляции, что является окончательным переопределителем для вызова метода, он может не выполнять диспетчеризацию во время выполнения. Пользовательский код также может отключить динамическую отправку, если он того желает:
struct base {
virtual void foo() const { std::cout << "base" << std::endl; }
void bar() const { std::cout << "bar" << std::endl; }
};
struct derived : base {
virtual void foo() const { std::cout << "derived" << std::endl; }
};
void test( base const & b ) {
b.foo(); // requires runtime dispatch, the type of the referred
// object is unknown at compile time.
b.base::foo();// runtime dispatch manually disabled: output will be "base"
b.bar(); // non-virtual, no runtime dispatch
}
int main() {
derived d;
d.foo(); // the type of the object is known, the compiler can substitute
// the call with d.derived::foo()
test( d );
}
На вопрос о том, следует ли предоставлять виртуальные деструкторы во всех случаях наследования, ответ отрицательный, не обязательно. Виртуальный деструктор требуется только в том случае, если объекты code delete
s производного типа хранятся с помощью указателей на базовый тип. Общее правило состоит в том, что вы должны
Вторая часть правила гарантирует, что пользовательский код не может удалить ваш объект через указатель на базу, а это означает, что деструктор не обязательно должен быть виртуальным. Преимущество заключается в том, что если ваш класс не содержит виртуальных методов, это не изменит никаких свойств вашего класса - макет памяти класса изменяется при добавлении первого виртуального метода - и вы сохраните указатель vtable в каждом случае. По двум причинам, первая из которых важна.
struct base1 {};
struct base2 {
virtual ~base2() {}
};
struct base3 {
protected:
~base3() {}
};
typedef base1 base;
struct derived : base { int x; };
struct other { int y; };
int main() {
std::auto_ptr<derived> d( new derived() ); // ok: deleting at the right level
std::auto_ptr<base> b( new derived() ); // error: deleting through a base
// pointer with non-virtual destructor
}
Проблема в последней строке main может быть решена двумя разными способами. Если typedef
изменить на base1
, то деструктор будет правильно отправлен объекту derived
, и код не вызовет неопределенного поведения. Цена состоит в том, что derived
теперь требуется виртуальная таблица, а каждому экземпляру требуется указатель. Что еще более важно, derived
больше не совместим с other
. Другое решение - изменить typedef
на base3
, и в этом случае проблема решается за счет крика компилятора в этой строке. Недостатком является то, что вы не можете удалить через указатели на базу, преимущество в том, что компилятор может статически гарантировать, что не будет неопределенного поведения.
В частном случае шаблона CRTP (извините за повторяющийся шаблон) большинство авторов даже не заботятся о том, чтобы защитить деструктор, поскольку намерение не состоит в том, чтобы удерживать объекты производного типа ссылками на базовый (шаблонный) тип. Чтобы быть в безопасности, они должны пометить деструктор как защищенный, но это редко является проблемой.
В самом деле, очень маловероятно. В стандарте нет ничего, что могло бы останавливать компиляторы, выполняющие целые классы глупо неэффективных вещей, но невиртуальный вызов по-прежнему остается невиртуальным вызовом, независимо от того, есть ли у класса виртуальные функции. Он должен вызывать версию функции, соответствующую статическому типу, а не динамическому типу:
struct Foo {
void foo() { std::cout << "Foo\n"; }
virtual void virtfoo() { std::cout << "Foo\n"; }
};
struct Bar : public Foo {
void foo() { std::cout << "Bar\n"; }
void virtfoo() { std::cout << "Bar\n"; }
};
int main() {
Bar b;
Foo *pf = &b; // static type of *pf is Foo, dynamic type is Bar
pf->foo(); // MUST print "Foo"
pf->virtfoo(); // MUST print "Bar"
}
Таким образом, в реализации нет абсолютно никакой необходимости помещать не виртуальные функции в vtable, и действительно, в vtable для Bar
вам понадобятся два разных слота в этом примере для Foo::foo()
и Bar::foo()
. Это означает, что использование vtable будет особым случаем, даже если реализация хочет сделать это. На практике он этого делать не хочет, в этом нет смысла, не беспокойтесь об этом.
Базовые классы CRTP действительно должны иметь невиртуальные и защищенные деструкторы.
Виртуальный деструктор требуется, если пользователь класса может взять указатель на объект, привести его к типу указателя базового класса, а затем удалить его. Виртуальный деструктор означает, что это будет работать. Защищенный деструктор в базовом классе останавливает их попытки (delete
не компилируется, поскольку нет доступного деструктора). Таким образом, один из виртуальных или защищенных решает проблему, когда пользователь случайно вызывает неопределенное поведение.
См. Рекомендацию № 4 здесь и обратите внимание, что «недавно» в этой статье означает почти 10 лет назад:
http://www.gotw.ca/publications/mill18.htm
Ни один пользователь не будет создавать Base<Derived>
собственный объект, это не объект Derived
, поскольку это не то, для чего предназначен базовый класс CRTP. Им просто не нужно иметь доступ к деструктору - поэтому вы можете оставить его вне общедоступного интерфейса или, чтобы сохранить строку кода, вы можете оставить ее общедоступной и положиться на пользователя, который не сделает что-то глупое.
Причина, по которой он нежелательно быть виртуальным, учитывая, что это не обязательно, заключается в том, что нет смысла давать классу виртуальные функции, если они ему не нужны. Когда-нибудь это может чего-то стоить с точки зрения размера объекта, сложности кода или даже (что маловероятно) скорости, так что было бы преждевременной пессимизацией всегда делать вещи виртуальными. Среди тех программистов на C ++, которые используют CRTP, предпочтительный подход состоит в том, чтобы быть абсолютно ясным, для каких классов предназначены, предназначены ли они вообще как базовые классы, и если да, то предназначены ли они для использования в качестве полиморфных баз. Базовые классы CRTP - нет.
Причина того, что у пользователя нет бизнес-преобразования в базовый класс CRTP, даже если он общедоступный, заключается в том, что он на самом деле не обеспечивает «лучший» интерфейс. Базовый класс CRTP зависит от производного класса, поэтому вы не переключаетесь на более общий интерфейс, если приведете Derived*
к Base<Derived>*
. Никакой другой класс никогда не будет иметь Base<Derived>
в качестве базового класса, если он также не имеет Derived
в качестве базового класса. Это просто бесполезно в качестве полиморфной основы, так что не делайте ее таковой.
public
. Если интерфейс опубликованный правильный, то все остальное будет недоступно, но это не то, о чем все люди всегда будут беспокоиться. Особенно, когда показываю вам их умный класс CRTP в каком-то сообщении в блоге ;-) Итак, я полагаю, что я имею в виду, не обязательно предполагать, что то, что вы видите, имеет вескую причину и является наилучшей практикой.
- person Steve Jessop; 13.10.2010
Ответ на ваш первый вопрос: Нет. Только вызовы виртуальных функций вызовут косвенное обращение через виртуальную таблицу во время выполнения.
Ответ на ваш второй вопрос: Любопытно повторяющийся шаблон шаблона обычно реализуется с использованием частного наследования. Вы не моделируете отношения IS-A и, следовательно, не передаете указатели на базовый класс.
Например, в
template <class Derived> class Base
{
};
class Derived : Base<Derived>
{
};
У вас нет кода, который принимает Base<Derived>*
, а затем вызывает для него удаление. Таким образом, вы никогда не пытаетесь удалить объект производного класса с помощью указателя на базовый класс. Следовательно, деструктор не обязательно должен быть виртуальным.
struct derived : base {}
эквивалентно class derived : public base {}
. Но общий аргумент в силе.
- person David Rodríguez - dribeas; 13.10.2010
Во-первых, я думаю, что ответ на вопрос ОП был дан достаточно хорошо - это твердое НЕТ.
Но я просто схожу с ума или что-то серьезно не так в сообществе? Мне было немного страшно видеть, как так много людей говорят, что хранить указатель / ссылку на Base бесполезно / редко. Некоторые из популярных ответов выше предполагают, что мы не моделируем отношения IS-A с CRTP, и я полностью не согласен с этим мнением.
Широко известно, что в C ++ нет такого понятия, как интерфейс. Итак, чтобы написать тестируемый / макетируемый код, многие люди используют ABC как «интерфейс». Например, у вас есть функция void MyFunc(Base* ptr)
, и вы можете использовать ее так: MyFunc(ptr_derived)
. Это традиционный способ моделирования отношений IS-A, который требует поиска vtable при вызове любых виртуальных функций в MyFunc. Итак, это первый образец для моделирования отношений IS-A.
В некоторой области, где производительность имеет решающее значение, существует другой способ (второй шаблон) для моделирования отношений IS-A тестируемым / имитируемым образом - через CRTP. И действительно, прирост производительности может быть впечатляющим (600% в статье) в некоторых случаях, см. Это ссылка. Итак, MyFunc будет выглядеть так template<typename Derived> void MyFunc(Base<Derived> *ptr)
. Когда вы используете MyFunc, вы делаете MyFunc(ptr_derived);
. Компилятор сгенерирует копию кода для MyFunc (), которая лучше всего соответствует типу параметра ptr_dehibited - MyFunc(Base<Derived> *ptr)
. Внутри MyFunc мы вполне можем предположить, что вызывается некоторая функция, определяемая интерфейсом, и указатели статически приводятся во время компиляции (проверьте функцию impl () в ссылке), нет никаких накладных расходов для поиска vtable.
Теперь, может кто-нибудь, пожалуйста, скажите мне, что я говорю безумную ерунду или в приведенных выше ответах просто не учитывался второй шаблон для моделирования отношений IS-A с CRTP?