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

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

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

Ответы [ 4 ]

5 голосов
/ 13 октября 2010

Только виртуальные функции требуют динамической отправки (и, следовательно, 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 );
}

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

4 голосов
/ 13 октября 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. Им просто не нужно иметь доступ к деструктору - так что вы можете оставить его вне общедоступного интерфейса или сохранить строку кода, вы можете оставить его открытым и полагаться на то, что пользователь не делает глупостей.

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

Причина, по которой у пользователя нет бизнес-приведения к базовому классу CRTP, даже если он общедоступный, заключается в том, что он на самом деле не предоставляет «лучший» интерфейс. Базовый класс CRTP зависит от производного класса, поэтому вы не будете переключаться на более общий интерфейс, если приведете Derived* к Base<Derived>*. Ни один другой класс никогда не будет иметь Base<Derived> в качестве базового класса, если только он не имеет Derived в качестве базового класса. Это просто бесполезно как полиморфная основа, так что не делайте это.

4 голосов
/ 13 октября 2010

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

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

Например, в

template <class Derived> class Base
{
};

class Derived : Base<Derived>
{
};

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

0 голосов
/ 21 декабря 2015

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

Но, просто я схожу с ума или что-то идет серьезно в сообществе?Я немного испугался, увидев, что так много людей полагают, что указатель / ссылка на базу бесполезно / редко.Некоторые из популярных ответов выше предполагают, что мы не моделируем отношения 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_derived - MyFunc(Base<Derived> *ptr).Внутри MyFunc мы вполне можем предположить, что вызывается некоторая функция, определенная интерфейсом, и указатели статически преобразуются во время компиляции (проверьте функцию impl () в ссылке), нет никаких накладных расходов для поиска в vtable.

Теперь, может кто-нибудь сказать мне, или я говорю чепуху, или ответы выше просто не рассматривали второй шаблон для моделирования отношений IS-A с CRTP?

...