Будет ли использование виртуального деструктора заставлять невиртуальные функции выполнять поиск в v-таблице?

Как раз то, о чем спрашивает тема. Также хочу знать, почему ни в одном из обычных примеров CRTP не упоминается virtual dtor.

РЕДАКТИРОВАТЬ: Ребята, пожалуйста, опубликуйте также о проблеме CRTP, спасибо.


person nakiya    schedule 13.10.2010    source источник
comment
Почему вы думаете, что невиртуальные функции могут выполнять поиск в v-таблице, когда существует виртуальный деструктор?   -  person the_drow    schedule 13.10.2010
comment
Хм. Хорошая точка зрения! Просто я так не думаю. Не знаю, что происходит, поэтому и спрашиваю: D.   -  person nakiya    schedule 13.10.2010


Ответы (4)


Только виртуальные функции требуют динамической отправки (и, следовательно, поиска 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 deletes производного типа хранятся с помощью указателей на базовый тип. Общее правило состоит в том, что вы должны

  • предоставить общедоступный виртуальный деструктор или защищенный невиртуальный деструктор

Вторая часть правила гарантирует, что пользовательский код не может удалить ваш объект через указатель на базу, а это означает, что деструктор не обязательно должен быть виртуальным. Преимущество заключается в том, что если ваш класс не содержит виртуальных методов, это не изменит никаких свойств вашего класса - макет памяти класса изменяется при добавлении первого виртуального метода - и вы сохраните указатель 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 (извините за повторяющийся шаблон) большинство авторов даже не заботятся о том, чтобы защитить деструктор, поскольку намерение не состоит в том, чтобы удерживать объекты производного типа ссылками на базовый (шаблонный) тип. Чтобы быть в безопасности, они должны пометить деструктор как защищенный, но это редко является проблемой.

person David Rodríguez - dribeas    schedule 13.10.2010
comment
Это и ответ Стива Джессопа информативны. Я разделен на двоих. Отметим, что это решено только из-за того, что ваша репутация ниже: D. - person nakiya; 14.10.2010

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

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 в качестве базового класса. Это просто бесполезно в качестве полиморфной основы, так что не делайте ее таковой.

person Steve Jessop    schedule 13.10.2010
comment
чтобы сохранить строку кода, вы можете оставить ее общедоступной и полагаться на то, что пользователь не сделает что-то глупое, и это очень плохая идея, лень в этом случае не одобряется - person the_drow; 13.10.2010
comment
@the_drow: конечно, но некоторые рекомендации по кодированию достаточно смягчены относительно правильности интерфейса public. Если интерфейс опубликованный правильный, то все остальное будет недоступно, но это не то, о чем все люди всегда будут беспокоиться. Особенно, когда показываю вам их умный класс CRTP в каком-то сообщении в блоге ;-) Итак, я полагаю, что я имею в виду, не обязательно предполагать, что то, что вы видите, имеет вескую причину и является наилучшей практикой. - person Steve Jessop; 13.10.2010
comment
@ Стив Джессоп: И почему вы думаете, что это руководство должно быть ослаблено и когда? Я все еще за тебя, кстати, :) - person the_drow; 13.10.2010
comment
@the_drow: Не думаю, что нужно расслабляться. Я думаю, что базовые классы CRTP действительно должны иметь защищенные деструкторы. Я просто говорю о том, что, как я ожидаю, увидит Накия. Также прикрываю свою задницу, потому что я могу сказать с очень большой уверенностью, что если вы найдете пример того, как я пишу базовый класс CRTP, он будет иметь общедоступный деструктор по умолчанию (т.е. я не буду его писать) ;-) - person Steve Jessop; 13.10.2010
comment
Более общие рекомендации по созданию правильного публичного интерфейса могут быть смягчены, если все ваши программисты имеют опыт грамотного использования C или Python, и им можно доверять, что они не всегда автоматически вызывают каждую функцию и получают доступ к каждому элементу данных, который они видят в источнике (например, заголовочный файл ), независимо от того, задокументировано ли это на самом деле. Тем не менее, все же лучше сделать это правильно. - person Steve Jessop; 13.10.2010

Ответ на ваш первый вопрос: Нет. Только вызовы виртуальных функций вызовут косвенное обращение через виртуальную таблицу во время выполнения.

Ответ на ваш второй вопрос: Любопытно повторяющийся шаблон шаблона обычно реализуется с использованием частного наследования. Вы не моделируете отношения IS-A и, следовательно, не передаете указатели на базовый класс.

Например, в

template <class Derived> class Base
{
};

class Derived : Base<Derived>
{
};

У вас нет кода, который принимает Base<Derived>*, а затем вызывает для него удаление. Таким образом, вы никогда не пытаетесь удалить объект производного класса с помощью указателя на базовый класс. Следовательно, деструктор не обязательно должен быть виртуальным.

person Frerich Raabe    schedule 13.10.2010
comment
+1. То, что я вижу в RTCP, - это унификация реализации в базовом классе с использованием шаблонов. Я не вижу ни одного случая, когда мы должны использовать Base ‹Derived› * - person ThR37; 13.10.2010
comment
Теперь я не понимаю, есть ли вообще какое-либо использование CRTP, если у вас нет неопознанного базового объекта, который что-то делает для конкретного объекта в фоновом режиме. Есть ли другое применение? - person nakiya; 13.10.2010
comment
@nakiya: CRTP - это один из инструментов для реализации статического полиморфизма. См., Например, stackoverflow.com/questions/ 262254 / хороший пример. - person Frerich Raabe; 13.10.2010
comment
Обратите внимание, что пример ошибочен в том, что наследование в этом случае не является частным, а скорее общедоступным. struct derived : base {} эквивалентно class derived : public base {}. Но общий аргумент в силе. - person David Rodríguez - dribeas; 13.10.2010
comment
@ Дэвид Родригес - dribeas: Ах, ты прав. Я соответствующим образом скорректировал свой пример. Спасибо за указание на это. - person Frerich Raabe; 13.10.2010
comment
Боюсь, ты ошибаешься. Это абсолютно законно и разумно (по каким причинам? См. Эту ссылку: eli.thegreenplace.net/2013/12/05/) для моделирования отношений Is-A с CRTP. Поправьте меня, если я ошибаюсь - ура. - person h9uest; 21.12.2015

Во-первых, я думаю, что ответ на вопрос ОП был дан достаточно хорошо - это твердое НЕТ.

Но я просто схожу с ума или что-то серьезно не так в сообществе? Мне было немного страшно видеть, как так много людей говорят, что хранить указатель / ссылку на 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?

person h9uest    schedule 21.12.2015
comment
Я согласен с вами, я хотел бы услышать больше от людей, которые ответили выше! - person fdev; 08.07.2021