Проблемы с абстрактным классом C ++ (я могу сделать это на Java, но не на C ++!) - PullRequest
5 голосов
/ 28 февраля 2012

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

Я пытаюсь заставить конструктор абстрактного класса вызывать функцию, которая является чисто виртуальной.В Java это работает, потому что подкласс обеспечивает реализацию вызываемого абстрактного метода.Тем не менее, в C ++, я получаю эту ошибку компоновщика:

test.o:test.cpp:(.text$_ZN15MyAbstractClassC2Ev[MyAbstractClass::MyAbstractClass
()]+0x16): undefined reference to `MyAbstractClass::initialize()'
collect2: ld returned 1 exit status

Вот мой код:

#include <iostream>

class MyAbstractClass {
protected:
    virtual void initialize() = 0;

public:
    MyAbstractClass() {
        initialize();
    }
};

class MyClass : public MyAbstractClass {
private:
    void initialize() {
        std::cout << "yey!" << std::endl;
    }
};

int main() {
    MyClass *my = new MyClass();
    return 0;
}

В качестве дальнейшего объяснения того, что я пытаюсь сделать, вот код вJava, которая достигает моей цели:

public abstract class MyAbstractClass {

    public MyAbstractClass() {
        initialize();
    }

    protected abstract void initialize();
}

public class MyClass extends MyAbstractClass {

    protected void initialize() {
        System.out.println("Yey!");
    }

    public static void main(String[] args) {
        MyClass myClass = new MyClass();
    }
}

Этот код выводит «Yey!».Любая помощь высоко ценится!

Ответы [ 6 ]

4 голосов
/ 28 февраля 2012
MyAbstractClass() {
    initialize();
}

Это не будет выполнять виртуальную диспетчеризацию до MyClass::initialize(), потому что на данном этапе построения объекта его MyClass части еще не созданы.Таким образом, вы действительно вызываете MyAbstractClass::initialize(), и, как таковое, оно должно быть определено.(Да, могут быть определены чисто виртуальные функции-члены.)

Старайтесь избегать вызова виртуальных функций-членов из конструкторов, потому что такого рода вещи будут происходить и поймать вас.Это редко имеет смысл делать.

Также старайтесь избегать функций initialize();у вас уже есть конструкторы, с которыми можно поиграть.


Обновление

На самом деле, хотя вы можете воспринимать приведенное выше как прочтенное для любой другой виртуальной функции-члена, вызывая pure Виртуальная функция-член из конструктора дает неопределенное поведение.Так что даже не пытайтесь!

2 голосов
/ 28 февраля 2012

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

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

#include <iostream>

class MyAbstractClass {
public:
    MyAbstractClass() {
        // don't do anything special to initialise the derived class
    }
};

class MyClass : public MyAbstractClass {
public:
    MyClass() {
        std::cout << "yey!" << std::endl;
    }
};

int main() {
    MyClass my;
    return 0;
}

Обратите внимание, что я также изменил my на автоматическую переменную;вы должны привыкнуть использовать их всякий раз, когда вам не нужно динамическое выделение, и научиться использовать RAII для управления динамическими ресурсами, когда они действительно вам нужны.

1 голос
/ 28 февраля 2012

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

Два решения, реализованные на разных языках, различаются в попытках понять, что пытается сделать ваш код. В C ++ решение заключается в том, что во время конструирования базового объекта и до тех пор, пока не начнется построение производного объекта, фактический тип объекта будет base , что означает, что не будет динамической диспетчеризации. То есть тип объекта в любой момент - это тип выполняемого конструктора [*] . Хотя это удивительно для некоторых (и вас, в том числе), оно дает разумное решение проблемы.

[*] И наоборот, деструктор . Тип также изменяется по мере завершения большинства производных конструкторов.

Альтернатива в Java состоит в том, что объект имеет конечный тип с самого начала, даже за до завершения построения . В Java, как вы продемонстрировали, вызов будет отправлен окончательному переопределению (здесь я использую сленг C ++: до последней реализации виртуальной функции в цепочке выполнения), и это может вызвать нежелательное поведение. Рассмотрим, например, эту реализацию initialize():

public class MyClass extends MyAbstractClass {
   final int k1 = 1;
   final int k2;
   MyClass() {
      k2 = 2;
   }
   void initialize() {
      System.out.println( "Constant 1 is " + k1 + " and constant 2 is " + k2 );
   }
}

Каков вывод предыдущей программы? (Ответ внизу)

Больше, чем просто игрушечный пример, учтите, что MyClass предоставляет некоторые инварианты, которые устанавливаются во время построения и сохраняются в течение всего времени жизни объекта. Может быть, он содержит ссылку на регистратор, в который могут быть выгружены данные. Посмотрев на класс, вы увидите, что регистратор установлен в конструкторе, и предположите, что он не может быть сброшен нигде в коде:

public class MyClass extends MyAbstractClass {
   Logger logger;
   MyClass() {
      logger = new Logger( System.out );
   }
   void initialize() {
      logger.debug( "Starting initialization" );
   }
}

Вы, наверное, теперь видите, куда это идет. Глядя на реализацию MyClass, кажется, нет ничего плохого вообще. logger устанавливается в конструкторе, поэтому его можно использовать во всех методах класса. Теперь проблема в том, что если MyAbstractClass вызывает виртуальную функцию, которая будет отправлена, то приложение будет аварийно завершать работу с NullPointerException.

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

( Ответ : это может зависеть от компилятора / JVM, но когда я попробовал это давным-давно, строка, напечатанная Константа 1, равна 1, а константа 2 - 0 . Если вы довольны этим, хорошо для меня, но я обнаружил, что это удивительно ... Причиной 1/0 в этом компиляторе является то, что процесс инициализации сначала устанавливает значения, которые находятся в определении переменной, а затем вызывает конструкторы. Это означает, что первый шаг построения установит k1 перед вызовом MyAbstractBase конструктора, который вызовет initialize() до запуска MyBase конструктора и установить значение второй константы).

1 голос
/ 28 февраля 2012

Позвольте мне процитировать Скотта Мейерса здесь (см. Никогда не вызывать виртуальные функции во время строительства или разрушения ):

Пункт 9: Никогда не вызывать виртуальные функции во время строительства или разрушения.

Я начну с резюме: вы не должны вызывать виртуальные функции во время строительства или разрушения, потому что вызовы не будут делать то, что вы думаете, и если они это сделают, вы все равно будете несчастны. Если вы выздоравливающий программист на Java или C #, обратите пристальное внимание на этот пункт, потому что это место, где эти языки изменяются, а C ++ - застывает.

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

0 голосов
/ 28 февраля 2012

Во время создания и уничтожения виртуальная таблица настраивается соответствующим образом для создаваемого или уничтожаемого базового подобъекта.Это теоретически правильная вещь, потому что более производный класс не жив (его время жизни еще не началось или уже закончилось).

0 голосов
/ 28 февраля 2012

Как уже объяснялось @Seth, вы не можете вызывать виртуальные функции в конструкторе. В частности, механизм виртуальной диспетчеризации отключен во время строительства и уничтожения. Либо сделайте вашу функцию-член initialize не виртуальной и внедрите ее в базовый класс, либо попросите пользователя вызвать ее явно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...