Почему виртуальный вызов чистой виртуальной функции от конструктора - UB, а вызов нечистой виртуальной функции разрешен Стандартом?

От 10.4 Абстрактные классы. 6 в стандарте:

"Функции-члены могут быть вызваны из конструктора (или деструктора) абстрактного класса; эффект виртуального вызова чистой виртуальной функции прямо или косвенно для создаваемого (или уничтоженного) объекта из такого конструктора ( или деструктор) undefined."

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

[EDIT] Дополнительные стандарты цитируют о чистых виртуальных функциях:

§ 10.4/2. Виртуальная функция задается чистым, используя в описании функции чисто-спецификатор (9.2) в объявлении функции. Чистая виртуальная функция должна быть определена только при вызове, или как если бы с (12.4), синтаксисом квалифицированного идентификатора (5.1).... [Примечание: Объявление функции не может предоставить как чисто-спецификатор, так и определение -end note]

§ 12.4/9 Деструктор может быть объявлен виртуальным (10.3) или чистым виртуальным (10.4); если в программе созданы какие-либо объекты этого класса или любого производного класса, должен быть определен деструктор.

Некоторые вопросы, требующие ответа, следующие:

  • Если чистой виртуальной функции не была предоставлена ​​реализация, не должно ли это быть ошибкой компилятора или компоновщика?

  • Если чистой виртуальной функции была предоставлена ​​реализация, почему она не может быть корректно определена в этом случае для вызова этой функции?

Ответ 1

Поскольку виртуальный вызов НИКОГДА не может вызвать чистую виртуальную функцию - единственный способ вызова чистой виртуальной функции - это явный (квалифицированный) вызов.

Теперь за пределами конструкторов или деструкторов это обеспечивается тем фактом, что вы никогда не можете иметь объекты абстрактного класса. Вместо этого вы должны иметь объект некоторого не абстрактного производного класса, который переопределяет чистую виртуальную функцию (если она не переопределяет ее, класс будет абстрактным). Однако, хотя работает конструктор или деструктор, у вас может быть объект промежуточного состояния. Но поскольку в стандарте говорится, что попытка вызывать чистую виртуальную функцию практически в этом состоянии приводит к поведению undefined, компилятор волен не иметь особых случаев, чтобы сделать это правильно, предоставляя гораздо большую гибкость для реализации чистых виртуальных функций. В частности, компилятор может свободно реализовывать чистые виртуальные машины так же, как он реализует нечистые виртуальные машины (нет необходимости в специальном случае), а также сбои или другие сбои, если вы вызываете чистый виртуальный объект из ctor/dtor.

Ответ 2

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

(BTW, когда я говорю "компилятор", я действительно имею в виду "компилятор и компоновщик". Извиняюсь за любую путаницу.)

struct Abstract {
    virtual void pure() = 0;
    virtual void foo() {
        pure();
    }
    Abstract() {
        foo();
    }
    ~Abstract() {
        foo();
    }
};

struct X : public Abstract {
    virtual void pure() { cout << " X :: pure() " << endl; }
    virtual void impure() { cout << " X :: impure() " << endl; }
};
int main() {
    X x;
}

Если конструктор Abstract, непосредственно вызванный pure(), это, очевидно, будет проблемой, и компилятор может легко увидеть, что не существует Abstract::pure() для вызова, а g++ дает предупреждение. Но в этом примере конструктор вызывает foo(), а foo() - нечистая виртуальная функция. Поэтому для компилятора или компоновщика нет простой основы для предупреждения или ошибки.

Как наблюдатели, мы можем видеть, что foo является проблемой, если вызывается из конструктора Abstract. Abstract::foo() сам определяется, но он пытается вызвать Abstract::pure, и этого не существует.

На этом этапе вы можете подумать, что компилятор должен выпустить предупреждение/ошибку о foo на том основании, что он вызывает чистую виртуальную функцию. Но вместо этого вы должны рассмотреть производный не-абстрактный класс, где pure была предоставлена ​​реализация. Если вы вызовете foo в этом классе после построения (и предположим, что вы не переопределили foo), вы получите четко определенное поведение. Поэтому снова нет оснований для предупреждения о foo. foo четко определен, если он не вызывается в конструкторе Abstract.

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

  • Полностью определенный: он и все методы, которые он вызывает, полностью определены на каждом уровне иерархии объектов
  • Defined-после строительства. Функция типа foo, которая имеет реализацию, но которая может иметь неприятные последствия в зависимости от состояния вызываемых ею методов.
  • Чистый виртуальный.

Много работы ожидать от компилятора и компоновщика, чтобы отслеживать все это, и, следовательно, стандарт позволяет компиляторам скомпилировать его чисто, но дать поведение undefined.

(я не упомянул о том, что возможно реализовать реализации для чистых виртуальных методов.Это для меня новичок. Является ли это правильно, или это просто расширение для компилятора? void Abstract :: pure() { })

Итак, это не просто undefined ', потому что стандарт так говорит. Вы должны спросить себя: "Какое поведение вы бы определили для вышеуказанного кода?". Единственным разумным ответом является либо оставить его undefined, либо запросить ошибку во время выполнения. Компилятор и компоновщик не будут легко анализировать все эти зависимости.

И чтобы усугубить ситуацию, подумайте о функциях-указателях-членах! Компилятор или компоновщик не могут действительно сказать, будут ли когда-либо называться "проблемные" методы - это может зависеть от целой нагрузки других вещей, которые происходят во время выполнения. Если компилятор видит (this->*mem_fun)() в конструкторе, нельзя ожидать, что он будет хорошо определен mem_fun.

Ответ 3

Это способ построения и уничтожения классов.

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

При разрушении сначала Derived уничтожается, затем Base. Итак, еще раз в деструкторе базы нет объекта Derived для вызова функции, только Base.

Кстати, это только undefined, где функция по-прежнему является чистой виртуальной. Итак, это четко определено:

struct Base
{
virtual ~Base() { /* calling foo here would be undefined */}
  virtual void foo() = 0;
};

struct Derived : public Base
{
  ~Derived() { foo(); }
  virtual void foo() { }
};

Обсуждение перешло к предложению альтернатив, которые:

  • Это может привести к ошибке компилятора, так же как и попытка создать экземпляр абстрактного класса.

Пример кода, несомненно, будет выглядеть примерно так:   класс Base   {     // другие вещи     virtual void init() = 0;     virtual void cleanup() = 0;   };

Base::Base()
{
    init(); // pure virtual function
}

Base::~Base()
{
   cleanup(); // which is a pure virtual function. You can't do that! shouts the compiler.
}

Здесь ясно, что вы делаете, это приведет вас в неприятности. Хороший компилятор может выдать предупреждение.

  • может возникнуть ошибка связи

Альтернативой является поиск определения Base::init() и Base::cleanup() и вызов того, что если он существует, в противном случае вызывается ошибка ссылки, то есть очистка очистки как не виртуальная для целей конструкторов и деструкторов.

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

class Base
{
   void init();
   void cleanup(); 
  // other stuff. Assume access given as appropriate in examples
  virtual ~Base();
  virtual void doinit() = 0;
  virtual void docleanup() = 0;
};

Base::Base()
{
    init(); // non-virtual function
}

Base::~Base()
{
   cleanup();      
}

void Base::init()
{
   doinit();
}

void Base::cleanup()
{
   docleanup();
}

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

Это невозможно для компилятора или компоновщика.

Поэтому стандарт должен разрешать компиляцию и ссылку и отмечать это как "поведение undefined".

Конечно, если реализация существует, компилятор может использовать ее, если это возможно. Поведение undefined не означает, что он должен упасть. Просто, что стандарт не говорит, что он должен его использовать.

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

Base::~Base()
{
   someCollection.removeMe( this );
}

void CollectionType::removeMe( Base* base )
{
    base->cleanup(); // ouch
}

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

Последнее, что вы должны помнить о Base::init() и Base::cleanup() здесь, состоит в том, что даже если у них есть реализации, они никогда не вызываются через механизм виртуальных функций (v-table). Их можно было бы назвать явно (используя полную квалификацию класса), что означает, что на самом деле они не являются виртуальными. То, что вам разрешено давать им реализации, возможно, вводит в заблуждение, вероятно, не было хорошей идеей, и если бы вы хотели такую ​​функцию, которую можно было бы вызвать через производные классы, возможно, ее лучше защищать и не виртуально.

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

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

Ответ 4

Прежде чем обсуждать, почему это undefined, сначала уточните, о чем идет речь.

#include<iostream>
using namespace std;

struct Abstract {
        virtual void pure() = 0;
        virtual void impure() { cout << " Abstract :: impure() " << endl; }
        Abstract() {
                impure();
                // pure(); // would be undefined
        }
        ~Abstract() {
                impure();
                // pure(); // would be undefined
        }
};
struct X : public Abstract {
        virtual void pure() { cout << " X :: pure() " << endl; }
        virtual void impure() { cout << " X :: impure() " << endl; }
};
int main() {
        X x;
        x.pure();
        x.impure();
}

Результат этого:

Abstract :: impure()  // called while x is being constructed
X :: pure()           // x.pure();
X :: impure()         // x.impure();
Abstract :: impure()  // called while x is being destructed.

Вторая и третья строки легко понять; методы изначально были определены в Abstract, но переопределения в X взяли верх. Этот результат был бы таким же, даже если x был ссылкой или указателем абстрактного типа вместо типа X.

Но это интересно, что происходит внутри конструктора и деструктора X. Вызов impure() в конструкторе вызывает Abstract::impure(), а не X::impure(), хотя построенный объект имеет тип x. То же самое происходит в деструкторе.

Когда объект типа x строится, первое, что строится, является просто объектом Abstract и, что важно, оно не знает, что в конечном итоге это будет объект x. Тот же процесс происходит в обратном порядке для разрушения.

Теперь, предполагая, что вы это понимаете, понятно, почему поведение должно быть undefined. Существует не метод Abstract :: pure, который может быть вызван конструктором или деструктором, и поэтому было бы бессмысленно пытаться определить это поведение (за исключением, возможно, как ошибки компиляции.)

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

struct Abstract {
    virtual void pure() = 0;
};
void Abstract :: pure() { cout << "How can I be called?!" << endl; }

Никогда не будет объекта, динамический тип которого является абстрактным, поэтому вы никогда не сможете выполнить этот код с обычным вызовом abs.pure(); или что-то в этом роде. Итак, в чем смысл такого определения?

Смотрите эту демонстрацию. Компилятор дает предупреждения, но теперь метод Abstract::pure() может быть вызван из конструктора. Это единственный способ, с помощью которого можно вызвать Abstract::pure().

Но это технически undefined. Другой компилятор имеет право игнорировать реализацию Abstract::pure или даже делать другие сумасшедшие вещи. Я не знаю, почему это не определено, но я написал это, чтобы попытаться помочь решить вопрос.