2013-05-23 6 views
6

Я читаю Effective C++, и есть «Пункт 9: Никогда не вызывайте виртуальные функции во время строительства или разрушения». И мне интересно, если мой код хорошо, даже если он нарушает это правило:Вызов виртуальной функции от конструктора

using namespace std; 

class A{ 
    public: 
     A(bool doLog){ 
      if(doLog) 
       log(); 
     } 

     virtual void log(){ 
      cout << "logging A\n"; 
     } 
}; 


class B: public A{ 
public: 
    B(bool doLog) : A(false){ 
     if(doLog) 
      log(); 
    } 

    virtual void log(){ 
     cout << "logging B\n"; 
    } 
}; 


int main() { 
    A a(true); 
    B b(true); 
} 

там что-то не так с этим подходом? Могу ли я попасть в беду, когда я делаю что-то более сложное?

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

Я хотел бы подчеркнуть, что выход моей программы выглядит следующим образом:

logging A 
logging B 

Так я получаю вошедшие, когда он построен и B регистрируется, когда он построен. И это то, что я хочу хочу! Но я спрашиваю, находите ли вы что-то неправильное (потенциально опасное) с моим «взломом», чтобы преодолеть проблему с вызовом виртуальной функции в конструкторе.

+1

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

+2

@JoachimPileborg это не так: поведение * определено *. Во время строительства вызовы виртуальных функций отключены (например, используется реализация используемого в настоящее время типа). –

ответ

11

И мне интересно, если мой код хорошо, даже если он нарушает это правило:

Это зависит от того, что вы имеете в виду под «отлично». Ваша программа хорошо сформирована, и ее поведение четко определено, поэтому оно не будет вызывать неопределенное поведение и тому подобное.

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

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

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

+0

Красиво объяснил, но я должен сказать, что считаю это очень интуитивным. Я программировал на C++ в течение многих лет, и когда позже я обнаружил языки, которые называли версию большинства производных классов, я был шокирован и потрясен. – Steve

15

С этим подходом что-то не так?

Ответ от Бьярне Страуструп:

Могу ли я вызвать виртуальную функцию из конструктора?

Да, но будьте осторожны. Он может не делать то, что вы ожидаете. В конструкторе механизм виртуального вызова отключен, потому что переопределение из производных классов еще не произошло. Объекты строятся из базы вверх, «база до производной». Рассмотрим:

#include<string> 
    #include<iostream> 
    using namespace std; 

class B { 
public: 
    B(const string& ss) { cout << "B constructor\n"; f(ss); } 
    virtual void f(const string&) { cout << "B::f\n";} 
}; 

class D : public B { 
public: 
    D(const string & ss) :B(ss) { cout << "D constructor\n";} 
    void f(const string& ss) { cout << "D::f\n"; s = ss; } 
private: 
    string s; 
}; 

int main() 
{ 
    D d("Hello"); 
} 

программа собирает и производит

B constructor 
B::f 
D constructor 

Примечание не D :: F. Подумайте, что произойдет, если правило было иным, чтобы D :: f() был вызван из B :: B(): поскольку конструктор D :: D() еще не был запущен, D :: f() попробуйте присвоить его аргумент неинициализированной строке s. Результатом, скорее всего, станет немедленная авария. Уничтожение выполняется «производным классом до базового класса», поэтому виртуальные функции ведут себя так же, как и в конструкторах: используются только локальные определения - и не вызываются вызовы для переопределения функций, чтобы не касаться (теперь уничтоженной) производной части класса объекта.

Подробнее см. D & E 13.2.4.2 или TC++ PL3 15.4.3.

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

4

Это «отлично» в смысле того, что он четко определен. Это может быть не «отлично» в смысле того, что вы ожидаете.

Вы вызываете переопределение из класса, который в настоящее время строится (или уничтожается), а не окончательного переопределения; поскольку окончательный производный класс еще не был построен (или уже уничтожен), и поэтому его невозможно получить. Поэтому у вас могут возникнуть проблемы, если вы хотите, чтобы здесь вызывается окончательное переопределение.

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

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

Смежные вопросы