Почему при возврате совместимого типа требуется явно std:: move?

Я смотрю " Не помогите компилятору" поговорить с STL, где у него есть аналогичный пример на слайде 26:

struct A
{
  A() = default;
  A(const A&) { std::cout << "copied" << std::endl; }
  A(A&&) { std::cout << "moved" << std::endl; }
};

std::pair<A, A> get_pair()
{
  std::pair<A, A> p;
  return p;
}

std::tuple<A, A> get_tuple()
{
  std::pair<A, A> p;
  return p;
}

std::tuple<A, A> get_tuple_moved()
{
  std::pair<A, A> p;
  return std::move(p);
}

При этом следующий вызов:

get_pair();
get_tuple();
get_tuple_moved();

Производит этот вывод:

moved
moved
copied
copied
moved
moved

См. MCVE в действии.

Результат get_pair построен по ходу движения, что и ожидалось. Движение также может быть полностью отменено NRVO, но оно не соответствует теме настоящего вопроса.

Результат get_tuple_moved также построен по ходу, что явно указано так. Однако результат get_tuple копируется, что совершенно неочевидно для меня.

Я думал, что любое выражение, переданное в оператор return, может считаться неявным move на нем, поскольку компилятор знает, что он все равно выйдет за рамки. Кажется, я ошибаюсь. Может кто-то уточнить, что здесь происходит?

См. также связанный, но другой вопрос: Когда следует использовать std:: move для возвращаемого значения функции?

Ответ 1

Оператор return в get_tuple() должен быть инициализирован с помощью move-constructor, но поскольку тип возвращаемого выражения и тип возвращаемого значения не совпадают, вместо этого выбирается copy-constructor. В С++ 14 произошли изменения, где теперь начальная фаза разрешения перегрузки обрабатывает оператор return как rvalue, когда это просто автоматическая переменная, объявленная в теле.

Соответствующую формулировку можно найти в [class.copy]/p32:

Когда выполняются критерии для исключения операции копирования/перемещения, [..], или когда выражение в операторе return является (возможно, в скобках) id-выражением, которое называет объект с автоматической продолжительностью хранения объявленный в теле [..], разрешение перегрузки для выбора конструктора для копии сначала выполняется так, как если бы объект был обозначен rvalue.

Итак, в С++ 14 все выходные данные должны поступать от конструктора move A.

Варианты брандмауэров clang и gcc уже реализуют это изменение. Чтобы получить такое же поведение в режиме С++ 11, вам нужно будет использовать явный std:: move() в операторе return.