Почему виртуальный вызов чисто виртуальной функции из конструктора является UB, а стандарт не допускает вызов не чистой виртуальной функции? - PullRequest
16 голосов
/ 08 февраля 2012

От 10,4 Абстрактные классы параг.6 в стандарте:

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

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

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

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

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

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

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

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

Ответы [ 4 ]

11 голосов
/ 08 февраля 2012

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

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

3 голосов
/ 08 февраля 2012

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

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

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

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

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

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

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

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

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

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

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

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

Между прочим, это только неопределенно, когда функция все еще является чисто виртуальной. Так что это четко определено:

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

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

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

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

Код примера, несомненно, будет выглядеть примерно так: базовый класс { // другие вещи виртуальная пустота init () = 0; виртуальная очистка void () = 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 () вызывают чистые виртуальные функции, если вы не знаете из где они вызываются.

Компилятору или компоновщику совершенно невозможно это сделать.

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

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

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

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

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

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

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

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

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

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

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

#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 был ссылкой или указателем типа Abstract вместо типа X.

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

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

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

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

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

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

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

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

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