скопировать elision возвращаемых значений и noexcept

У меня есть такой шаблон функции:

template <typename T>
constexpr auto myfunc() noexcept
{
    return T{};
}

Гарантируется ли, что этот шаблон функции будет исключен из-за исключения копии? Если внутри конструктора генерируется исключение, это происходит внутри или вне функции?


person Martin Fehrs    schedule 21.04.2018    source источник


Ответы (3)


Все, что делает копирование, — это устраняет фактическую копию или перемещение. Все происходит «как будто» без копирования-элизиона (за исключением самого копирования, конечно).

Конструкция происходит внутри функции. Copy elision этого не меняет. Все, что он делает, это устраняет фактическое копирование/перемещение (я повторяюсь?) в результате того, что возвращаемое значение функции возвращается обратно в вызывающую программу.

Итак, если конструктор класса по умолчанию выдает исключение, noexcept уничтожает все это с высокой орбиты.

Если конструктор копирования/перемещения выдает исключение, поскольку копирование/перемещение не происходит, все продолжает работать.

С gcc 7.3.1, скомпилированным с использованием -std=c++17:

template <typename T>
constexpr auto myfunc() noexcept
{
    return T{};
}

class xx {
public:

    xx() { throw "Foo"; }
};

int main()
{
    try {
        myfunc<xx>();
    } catch (...) {
    }
}

Результат:

terminate called after throwing an instance of 'char const*'

Теперь давайте смешаем это и создадим исключение как в конструкторах копирования, так и в конструкторах перемещения:

class xx {
public:

    xx() { }

    xx(xx &&) { throw "Foo"; }

    xx(const xx &) { throw "Baz"; }
};

Это работает без исключения.

person Sam Varshavchik    schedule 21.04.2018

Инициализация возвращаемого значения происходит в контексте вызываемого объекта (функции, содержащей оператор return). То есть, если вы хотите оставить открытой возможность обработки исключения, создаваемого конструктором по умолчанию T, вам не следует не объявлять myfunc с noexcept.

Я понимаю источник путаницы: согласно таксономии категорий значений в С++ 17 и более поздних версиях, prvalue — это рецепт создания объекта, а не сам объект. Рассмотрим следующий код:

T foo() {
    return {};
}
T t = foo();

В C++14 оператор return и инициализация t представляют собой два отдельных шага, хотя исключение разрешено в качестве оптимизации. На первом этапе возвращаемый объект (он же foo()) инициализируется копированием из {}. На втором этапе t инициализируется копированием из этого возвращаемого объекта. Очевидно, что первый шаг происходит в контексте вызываемого объекта, а второй — в контексте вызывающего.

Таким образом, вы можете подумать, что в C++17 происходит аналогичный двухэтапный процесс, только с пересмотренной концепцией prvalue: а именно, поскольку foo() является prvalue, вы можете подумать, что оператор return просто создает рецепт (который может быть концептуально представлен как [](void* p) { new (p) T{}; }), и указанный рецепт создается в контексте вызываемого объекта, в то время как выполнение этого рецепта для создания t будет происходить в контексте вызывающего объекта. Если бы это было так, то фактический вызов конструктора по умолчанию T происходил бы в контексте вызывающего объекта, и, таким образом, любое выброшенное им исключение не встречало бы внешней скобки вызываемого объекта.

Однако в стандарте есть явный язык, который отрицает такую ​​интерпретацию:

оператор return инициализирует объект результата glvalue или prvalue вызова функции (явного или неявного) путем инициализации копирования [...] из операнда.

То есть инициализация t выполняется самим оператором return. Это означает, что t полностью инициализируется до того, как самый внешний блок вызываемого объекта будет фактически оставлен. Например, если в вызываемом объекте есть какие-либо локальные переменные, которые необходимо уничтожить, это фактически происходит после того, как t уже инициализирован (поэтому такое поведение потенциально отличается от поведения C++14). Точно так же, как ясно, что уничтожение таких локальных переменных происходит в контексте вызываемого объекта (и, следовательно, поиск обработчика, если при этом будет выдано исключение, натолкнется на самый внешний блок foo), также ясно, что инициализация t происходит в контексте вызываемого объекта.

person Brian Bi    schedule 31.01.2021

Сделай это так:

template <typename T> constexpr 
auto myfunc() noexcept(std::is_nothrow_default_constructible_v<T>)
{
    return T{};
}
person Jive Dadson    schedule 21.04.2018