Странное поведение - компилятор игнорирует конструктор копирования и пытается вызвать удаленный конструктор перемещения

Рассмотрим следующий код (реальный код несколько сложнее и находится под NDA):

class NonMovable
{
  public:
  NonMovable() = default;
  NonMovable(const NonMovable&) noexcept = default;
  NonMovable(NonMovable&&) noexcept = delete;
  NonMovable& operator=(const NonMovable&) noexcept = default;
  NonMovable& operator=(NonMovable&&) noexcept = delete;
  // ...
};

NonMovable SomeFunction()
{
  NonMovable nonMovable;
  // ...
  return nonMovable;// Error, NonMovalbe(NonMovable&&) is deleted.
}

Я не могу понять, почему возникает эта ошибка. У NonMovable есть стандартный конструктор копирования, но компилятор пытается вызвать конструктор перемещения, который удален.

Является ли это ошибкой компилятора? Или, может быть, возможен еще какой-то фактор, который принуждает компилятор к такому решению?

Вот так это выглядит. Ошибка наблюдается на разных версиях Visual Studio и компиляторов. введите сюда описание изображения


Ответы (2 шт):

Автор решения: αλεχολυτ

В С++17 ввели правило, по которому для возвращаемых безымянных объектов конструктор копирования или перемещения не должен вызываться и даже не обязан быть объявлен. Это такая обязательная оптимизация (RVO - return value optimization), даже если в теле конструктора выполняется какой-то видимый код. Однако, в вашем случае объект не безымянный (не создан при вызове return), а значит, правило RVO не применимо. В данном случае может происходить оптимизация NRVO - Named RVO, но её исполнение не стандартизовано.

Для устранения ошибки в вашем случае можно просто обернуть имя возвращаемой переменной в фигурные скобки, вызвав тем самым конструктор копирования до return. Но а при return уже сработает обязательная оптимизация RVO:

return {nonMovable};

Похоже, что вариант с фигурными скобками (list-initialization) не работает должным образом в msvc (по-прежнему хочет вызвать конструктор перемещения), и нужно явно указать вызов копирующего конструктора.

return NonMovable(nonMovable);

Clang, GCC устраивают оба варианта.

Вообще, стоит заметить, что при наличии копирующего конструктора в классе, автоматической генерации перемещающего конструктора не происходит. Т.е. если убрать эти строки:

NonMovable(NonMovable&&) noexcept = delete;
NonMovable& operator=(NonMovable&&) noexcept = delete;

класс не станет вдруг перемещаемым, а останется по-прежнему "NonMovable". И в этой ситуации msvc будет вполне корректно себя вести даже с первичным вариантом кода.

→ Ссылка
Автор решения: Денис Пантюшев

Если не определить NonMovable(NonMovable&&) явно и не поставить NonMovable(NonMovable&&) = default, то конструктор перемещения не будет создан, а будет использоваться конструктор копирования. = delete применять к конструктору перемещения бессмысленно.

И даже если написать NonMovable(NonMovable&&) = default; то конструктор копирования по умолчанию будет представлять из себя побайтовое копирование данных.

Можно и нужно эту строку убрать.

→ Ссылка