вызов чистой виртуальной функции из конструктора базового класса - PullRequest
34 голосов
/ 25 декабря 2011

У меня есть базовый класс MyBase, который содержит чисто виртуальную функцию:

void PrintStartMessage() = 0

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

затем я помещаю его в конструктор базового класса (MyBase)

 class MyBase
 {
 public:

      virtual void PrintStartMessage() =0;
      MyBase()
      {
           PrintStartMessage();
      }

 };

 class Derived:public MyBase
 {     

 public:
      void  PrintStartMessage(){

      }
 };

void main()
 {
      Derived derived;
 }

но я получаю ошибку компоновщика.

 this is error message : 

 1>------ Build started: Project: s1, Configuration: Debug Win32 ------
 1>Compiling...
 1>s1.cpp
 1>Linking...
 1>s1.obj : error LNK2019: unresolved external symbol "public: virtual void __thiscall MyBase::PrintStartMessage(void)" (?PrintStartMessage@MyBase@@UAEXXZ) referenced in function "public: __thiscall MyBase::MyBase(void)" (??0MyBase@@QAE@XZ)
 1>C:\Users\Shmuelian\Documents\Visual Studio 2008\Projects\s1\Debug\s1.exe : fatal error LNK1120: 1 unresolved externals
 1>s1 - 2 error(s), 0 warning(s)

Я хочу заставить все производные классы ...

A- implement it

B- call it in their constructor 

Как я должен это сделать?

Ответы [ 6 ]

31 голосов
/ 25 декабря 2011

Есть много статей, которые объясняют, почему вы никогда не должны вызывать виртуальные функции в конструкторе и деструкторе в C ++.Взгляните здесь и здесь , чтобы узнать подробнее, что происходит за сценой во время таких вызовов.Поэтому, когда вы пытаетесь вызвать виртуальную функцию из конструктора базового класса, переопределение из производных классов еще не произошло, потому что производные конструкторы еще не были вызваны.

14 голосов
/ 25 декабря 2011

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

Самое близкое к выполнению чего-либо подобного - это полная сборкасначала ваш объект, а затем вызов метода после:

template <typename T>
T construct_and_print()
{
  T obj;
  obj.PrintStartMessage();

  return obj;
}

int main()
{
    Derived derived = construct_and_print<Derived>();
}
8 голосов
/ 25 декабря 2011

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

Вызов PrintStartMessage после создания MyBase

Предположим, что вы хотите сделать что-то вроде этого:

class MyBase {
public:
    virtual void PrintStartMessage() = 0;
    MyBase() {
        printf("Doing MyBase initialization...\n");
        PrintStartMessage(); // ⚠ UB: pure virtual function call ⚠
    }
};

class Derived : public MyBase {
public:
    virtual void PrintStartMessage() { printf("Starting Derived!\n"); }
};

То естьжелаемый результат:

Doing MyBase initialization...
Starting Derived!

Но это именно то, для чего нужны конструкторы!Просто удалите виртуальную функцию и сделайте так, чтобы конструктор Derived выполнил работу:

class MyBase {
public:
    MyBase() { printf("Doing MyBase initialization...\n"); }
};

class Derived : public MyBase {
public:
    Derived() { printf("Starting Derived!\n"); }
};

Вывод вполне соответствует ожидаемому:

Doing MyBase initialization...
Starting Derived!

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

Вызов PrintStartMessage до создания MyBase

Как сказано выше, если вы хотитечтобы вызвать PrintStartMessage до того, как Derived был построен, вы не можете сделать это, потому что еще нет Derived объекта для вызова PrintStartMessage.Нет смысла требовать, чтобы PrintStartMessage был нестатическим членом, поскольку он не имел бы доступа ни к одному из Derived членов данных.

Статическая функция с фабричной функцией

В качестве альтернативы мы можем сделать его статическим членом, например, так:

class MyBase {
public:
    MyBase() {
        printf("Doing MyBase initialization...\n");
    }
};

class Derived : public MyBase {
public:
    static void PrintStartMessage() { printf("Derived specific message.\n"); }
};

Возникает естественный вопрос о том, как он будет называться?

Есть два решения, которые я вижу: одно похоже наэто @greatwolf, где вы должны вызвать его вручную.Но теперь, поскольку это статический член, вы можете вызвать его до того, как будет создан экземпляр MyBase:

template<class T>
T print_and_construct() {
    T::PrintStartMessage();
    return T();
}

int main() {
    Derived derived = print_and_construct<Derived>();
}

Вывод будет

Derived specific message.
Doing MyBase initialization...

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

Второе решение - прибегнуть к шаблону Curily Recurring Template Pattern (CRTP).Сказав MyBase полный тип объекта во время компиляции, он может выполнить вызов из конструктора:

template<class T>
class MyBase {
public:
    MyBase() {
        T::PrintStartMessage();
        printf("Doing MyBase initialization...\n");
    }
};

class Derived : public MyBase<Derived> {
public:
    static void PrintStartMessage() { printf("Derived specific message.\n"); }
};

Вывод будет таким, как ожидается, без необходимости использования выделенной функции фабрики.

Доступ к MyBase из PrintStartMessage с CRTP

Пока выполняется MyBase, он уже в порядке для доступа к своим членам.Мы можем сделать так, чтобы PrintStartMessage смог получить доступ к MyBase, который вызвал его:

template<class T>
class MyBase {
public:
    MyBase() {
        T::PrintStartMessage(this);
        printf("Doing MyBase initialization...\n");
    }
};

class Derived : public MyBase<Derived> {
public:
    static void PrintStartMessage(MyBase<Derived> *p) {
        // We can access p here
        printf("Derived specific message.\n");
    }
};

Следующее также допустимо и очень часто используется, хотя и немного опасно:

template<class T>
class MyBase {
public:
    MyBase() {
        static_cast<T*>(this)->PrintStartMessage();
        printf("Doing MyBase initialization...\n");
    }
};

class Derived : public MyBase<Derived> {
public:
    void PrintStartMessage() {
        // We can access *this member functions here, but only those from MyBase
        // or those of Derived who follow this same restriction. I.e. no
        // Derived data members access as they have not yet been constructed.
        printf("Derived specific message.\n");
    }
};

Нет решения для шаблонов - редизайн

Еще один вариант - немного изменить код.IMO, это действительно предпочтительное решение, если вам абсолютно необходимо вызвать переопределенную PrintStartMessage из MyBase конструкции.

Это предложение состоит в том, чтобы отделить Derived от MyBase следующим образом:

class ICanPrintStartMessage {
public:
    virtual ~ICanPrintStartMessage() {}
    virtual void PrintStartMessage() = 0;
};

class MyBase {
public:
    MyBase(ICanPrintStartMessage *p) : _p(p) {
        _p->PrintStartMessage();
        printf("Doing MyBase initialization...\n");
    }

    ICanPrintStartMessage *_p;
};

class Derived : public ICanPrintStartMessage {
public:
    virtual void PrintStartMessage() { printf("Starting Derived!!!\n"); }
};

Вы инициализируете MyBase следующим образом:

int main() {
    Derived d;
    MyBase b(&d);
}
5 голосов
/ 25 декабря 2011

Вы не должны вызывать функцию virtual в конструкторе. Период . Вам придется найти обходной путь, например, сделать PrintStartMessage non- virtual и явно указать вызов в каждом конструкторе.

1 голос
/ 27 декабря 2012

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

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

0 голосов
/ 06 октября 2018

Я знаю, что это старый вопрос, но я столкнулся с тем же вопросом, работая над моей программой.

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

#include <iostream>

class MyBase
{
public:
    virtual void UniqueCode() = 0;
    MyBase() {};
    void init(MyBase & other)
    {
      std::cout << "Shared Code before the unique code" << std::endl;
      other.UniqueCode();
      std::cout << "Shared Code after the unique code" << std::endl << std::endl;
    }
};

class FirstDerived : public MyBase
{
public:
    FirstDerived() : MyBase() { init(*this); };
    void  UniqueCode()
    {
      std::cout << "Code Unique to First Derived Class" << std::endl;
    }
private:
    using MyBase::init;
};

class SecondDerived : public MyBase
{
public:
    SecondDerived() : MyBase() { init(*this); };
    void  UniqueCode()
    {
      std::cout << "Code Unique to Second Derived Class" << std::endl;
    }
private:
    using MyBase::init;
};

int main()
{
    FirstDerived first;
    SecondDerived second;
}

Вывод:

 Shared Code before the unique code
 Code Unique to First Derived Class
 Shared Code after the unique code

 Shared Code before the unique code
 Code Unique to Second Derived Class
 Shared Code after the unique code
...