Когда компилятор по умолчанию по умолчанию генерирует конструктор как noexcept?

Похоже, что в современных версиях хотя бы некоторых компиляторов (GCC 5.2 и Visual С++ 2015 Update 1) некорректно генерировать конструкторы по умолчанию noexcept, когда есть инициализированные члены класса:

#include <memory>
#include <exception>
#include <iostream>

struct E {};

struct A
{
    A()
    {
        throw E();
    }
};

struct B
{
    A a;
};

struct C
{
    std::shared_ptr<B> b{ std::make_shared<B>() };
        //C() {}  // uncomment to fix
};

int main()
{
    try
    {
        new C;
    }
    catch (const E &)
    {
        std::cout << "Exception caught\n";
    }
    std::cout << "Exiting...\n";
}

Запуск этого кода вызывает вызов std::terminate (вместо вызова блока catch) в GCC 5.2 (режим С++ 14) и обновление Visual С++ 2015 1.

Живой пример: http://coliru.stacked-crooked.com/view?id=16efc34ec173aca7

Uncommenting пустой конструктор исправляет этот код для Visual С++, но не для GCC. Clang 3.6 правильно (я полагаю?) Вызывает блокировку catch в любом случае.

Существуют ли какие-либо правила в стандарте, которые указывают, когда созданный по умолчанию конструктор должен быть помечен как noexcept?

Ответ 1

Я собираюсь здесь на С++ 14; Я не знаю, изменились ли изменения в пост-С++ 14, уточнив ситуацию.

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

f разрешает все исключения, если какая-либо функция, которую он вызывает напрямую, разрешает все исключения, а f имеет спецификацию исключения noexcept(true), если каждая функция, которую он вызывает напрямую, не позволяет исключений.

Тем не менее, "непосредственное обращение" явно не определено в стандарте и не является очевидным, когда дело доходит до инициализаторов в классе. Ваш класс C вызывает std::make_shared<B> (который, очевидно, может бросать независимо от спецификации исключения B, поскольку он выделяет память) и конструктор копирования std::shared_ptr<B> (что не является исключением) в его инициализаторе, но делают ли эти подсчеты "непосредственно вызванными" "или рассчитывается только конструктор копирования?

Вполне возможно, что именно здесь компиляторы отличаются интерпретацией. Кажется, что Clang считает make_shared, а другие компиляторы, по-видимому, этого не делают.

Предоставление B конструктор по умолчанию ничего не должен менять, поскольку этот конструктор вызывается только из make_shared и поэтому определенно не в стороне от компилятора; если он что-то меняет, там что-то серьезно не так.

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

Ответ 2

Кажется, что это ошибка компилятора, связанная с инициализаторами элементов по умолчанию. Обратите внимание на следующие исправления для GCC:

struct C
{
    std::shared_ptr<B> b;
    C() : b{std::make_shared<B>()} {}
};

Демо

, тогда как все это все еще падает (обратите внимание, что я явно использовал noexecpt(false)).

struct A
{
    A() noexcept(false)
    {
        throw E();
    }
};

struct B
{
    A a;
    B() noexcept(false) {}
};

struct C
{
    std::shared_ptr<B> b{ std::make_shared<B>() };
    C() noexcept(false) {}
};

Демо

тем не менее, сохраняя инициализатор по умолчанию, но переопределяя его определенным, также фиксирует вещи:

struct C
{
    std::shared_ptr<B> b{ std::make_shared<B>() };
    C() : b(std::make_shared<B>()) {}
};

Демо

Так что мне определенно кажется ошибкой в ​​GCC, по крайней мере.