Как спецификация исключения влияет на переопределение виртуального деструктора?

В стандарте С++ указано следующее о виртуальных функциях, имеющих спецификации исключений:

Если виртуальная функция имеет спецификацию исключения, все декларации, включая определение любой функции, которая переопределяет эту виртуальную функцию в любом производном классе, должны разрешать исключения, допускаемые спецификацией исключения виртуальной функции базового класса (С++ 03 § 15.4/3).

Таким образом, следующее плохо сформировано:

struct B {
    virtual void f() throw() { } // allows no exceptions
};
struct D : B {
    virtual void f() { }         // allows all exceptions
};

(1) Используется ли это правило для деструкторов? То есть следующие хорошо сформированные?

struct B {
    virtual ~B() throw() { }
};
struct D : B {
    virtual ~D() { }
};

(2) Как это правило применяется к неявно объявленному деструктору? То есть, правильно ли сформирован?

struct B {
    virtual ~B() throw() { }
};
struct D : B { 
    // ~D() implicitly declared
};

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

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

Ответы

Ответ 1

(1) Используется ли это правило для деструкторов?

Да, это правило применяется к деструкторам (исключение из правила для деструкторов), поэтому этот пример плохо сформирован. Чтобы сделать его хорошо сформированным, спецификация исключения ~D() должна быть совместима с спецификацией ~B(), например,

struct B {
    virtual ~B() throw() { }
};
struct D : B {
    virtual ~D() throw() { }
};

(2) Как это правило применяется к неявно объявленной специальной функции-члену?

В стандарте С++ говорится о неявно объявленных специальных функциях-членах:

Неявно объявленная специальная функция-член должна иметь спецификацию исключения.

Если f - неявно объявленный конструктор по умолчанию, оператор-конструктор копирования, деструктор или копирование, его неявная спецификация исключения указывает тип-идентификатор T тогда и только тогда, когда исключение T спецификация функции, непосредственно вызываемой f s неявным определение;

f должен допускать все исключения, если любая функция, которую он вызывает напрямую, разрешает все исключения, а f не допускает исключений, если каждая функция, которую он вызывает напрямую, не допускает исключений (С++ 03 § 15.4/13).

Какие функции непосредственно вызывают неявно объявленный деструктор?

После выполнения тела деструктора и уничтожения любых автоматических объектов, выделенных в теле, деструктор для класса X вызывает

  • деструкторы для X s прямых членов,
  • деструкторы для прямых базовых классов X s и
  • Если X - тип самого производного класса, его деструктор вызывает деструкторы для <базовых классов X s

(С++ 03 §12.4/6, переформатирован для упрощения чтения).

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

struct B {
    virtual ~B() throw() { }
};
struct D : B { 
    // ~D() implicitly declared
};

Единственным деструктором, вызываемым неявным объявлением ~D(), является ~B(). Поскольку ~B() не допускает исключений, ~D() не допускает никаких исключений и как будто он был объявлен virtual ~D() throw().

Эта спецификация исключений, очевидно, совместима с ~B(), поэтому этот пример хорошо сформирован.


В качестве практического примера того, почему это имеет значение, рассмотрим следующее:

struct my_exception : std::exception {
    std::string message_;
};

~string() разрешает все исключения, поэтому неявно объявленный ~my_exception() разрешает все исключения. Деструктор базового класса ~exception() является виртуальным и не позволяет исключений, поэтому деструктор производного класса несовместим с деструктором базового класса, и это плохо сформировано.

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

struct my_exception : std::exception {
    virtual ~my_exception() throw() { }
    std::string message_;
};

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