Copy ctor вызывается вместо move ctor

Почему при возврате из bar вызывается конструктор копирования вместо конструктора перемещения?

#include <iostream>

using namespace std;

class Alpha {
public:
  Alpha() { cout << "ctor" << endl; }
  Alpha(Alpha &) { cout << "copy ctor" << endl; }
  Alpha(Alpha &&) { cout << "move ctor" << endl; }
  Alpha &operator=(Alpha &) { cout << "copy asgn op" << endl; }
  Alpha &operator=(Alpha &&) { cout << "move asgn op" << endl; }
};

Alpha foo(Alpha a) {
  return a; // Move ctor is called (expected).
}

Alpha bar(Alpha &&a) {
  return a; // Copy ctor is called (unexpected).
}

int main() {
  Alpha a, b;
  a = foo(a);
  a = foo(Alpha());
  a = bar(Alpha());
  b = a;
  return 0;
}

Если bar выполняет return move(a), то поведение соответствует ожидаемому. Я не понимаю, зачем нужен вызов std::move, учитывая, что foo при возврате вызывает конструктор перемещения.


person Jacob Pollack    schedule 22.08.2017    source источник
comment
ответил здесь, но не уверен, что это обман.   -  person NathanOliver    schedule 22.08.2017
comment
@NathanOliver этот пост дал мне необходимое понимание. Последующий ответ будет предоставлен в ближайшее время.   -  person Jacob Pollack    schedule 22.08.2017
comment
В foo объект, обозначенный a, является локальным для функции, поэтому гарантируется, что срок действия объекта истекает и его можно безопасно перемещать. В bar, nothing is known about the object designated by a`, поэтому вы не хотите изменять его молча. Помните, что ссылки rvalue — это еще один вид ссылок; основной язык не имеет мнения о том, для чего вы его используете, и не имеет никаких ожиданий относительно времени жизни или псевдонимов.   -  person Kerrek SB    schedule 22.08.2017
comment
@KerrekSB Является ли перемещение a в foo поведением реализации? Мне не удалось найти какое-либо соответствующее описание этого поведения в стандарте.   -  person songyuanyao    schedule 22.08.2017
comment
@songyuanyao это часть стандарта. Если RVO не происходит, то при выходе из функции вызывается конструктор копирования или перемещения. Поскольку я определил конструктор перемещения, и a гарантированно истечет, как только foo выйдет за пределы области действия, вызывается конструктор перемещения.   -  person Jacob Pollack    schedule 22.08.2017
comment
@JacobPollack В качестве именованного параметра afoo) является lvalue, тогда следует вызвать конструктор копирования; это все, что я нашел в стандарте. Я не могу найти никаких цитат о том, что если срок действия a истекает, то вместо этого можно использовать конструктор перемещения. Вот почему я предполагаю, что это поведение реализации, не гарантированное стандартом.   -  person songyuanyao    schedule 22.08.2017
comment
@songyuanyao см. N4296.12.8.32.   -  person Jacob Pollack    schedule 22.08.2017
comment
@JacobPollack: переезд - это не реально. Это просто разговорное сокращение для определенных типов общих стратегий реализации. Что касается языка, то есть только привязка значений к ссылкам.   -  person Kerrek SB    schedule 22.08.2017


Ответы (4)


В этой ситуации нужно понять 2 вещи:

  1. a в bar(Alpha &&a) является именованной ссылкой rvalue; поэтому рассматривается как lvalue.
  2. a по-прежнему является ссылкой.

Часть 1

Поскольку a в bar(Alpha &&a) является именованной ссылкой на rvalue, она обрабатывается как lvalue. Мотивация обращения с именованными ссылками rvalue как lvalues ​​заключается в обеспечении безопасности. Рассмотрим следующее,

Alpha bar(Alpha &&a) {
  baz(a);
  qux(a);
  return a;
}

Если baz(a) считает a значением r, то можно свободно вызывать конструктор перемещения, а qux(a) может быть недопустимым. Стандарт позволяет избежать этой проблемы, рассматривая именованные ссылки rvalue как lvalue.

Часть 2

Поскольку a по-прежнему является ссылкой (и может ссылаться на объект вне области действия bar), bar при возврате вызывает конструктор копирования. Мотивация такого поведения заключается в обеспечении безопасности.

Ссылки

  1. SO Q&A - возврат по ссылке rvalue
  2. Комментарий от Kerrek SB
person Jacob Pollack    schedule 22.08.2017
comment
Этот ответ вводит в заблуждение, потому что он не делает абсолютно критического различия между типом и категорией значения. - person Nir Friedman; 23.08.2017
comment
ИМО, не очень разумно говорить, что именованная ссылка rvalue является lvalue. Вместо этого различайте сущности и выражения. Сущность a имеет тип Alpha&&, но выражение a является lvalue типа Alpha. Выражения никогда не имеют ссылочного типа, в лучшем случае бессмысленно говорить о том, что a является ссылкой в ​​контексте обсуждения того, как оно ведет себя в выражении. - person M.M; 23.08.2017

да, очень запутанно. Я хотел бы процитировать еще один пост SO здесь rvalue-para">неявный ход. где я нахожу следующие комментарии немного убедительными,

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

На самом деле «&&» уже указывает на отпускание, и в то время, когда вы делаете «возврат», достаточно безопасно делать движение.

возможно это просто выбор из стандартного комитета.

пункт 25 «эффективного современного С++» Скотта Мейерса также резюмировал это, не давая особых объяснений.

Alpha foo() {
  Alpha a
  return a; // RVO by decent compiler
}
Alpha foo(Alpha a) {
  return a; // implicit std::move by compiler
}

Alpha bar(Alpha &&a) {
  return a; // Copy ctor due to lvalue
}

Alpha bar(Alpha &&a) {
  return std:move(a); // has to be explicit by developer
}
person pepero    schedule 22.08.2017

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

int — это тип. int& - это другой тип. int&& — это еще один тип. Это все разные виды.

lvalue и rvalue — это вещи, называемые категориями значений. Пожалуйста, ознакомьтесь с фантастической диаграммой здесь: Что такое glvalues ​​и prvalues?. Вы можете видеть, что в дополнение к lvalue и rvalue у нас также есть prvalues ​​и glvalues ​​и xvalues, и они формируют различное отношение типа диаграммы Венна.

В C++ есть правила, согласно которым переменные различных типов могут связываться с выражениями. Однако ссылочный тип выражения отбрасывается (часто говорят, что выражения не имеют ссылочного типа). Вместо этого у выражения есть категория значения, которая определяет, какие переменные могут к нему привязываться.

Другими словами: ссылки rvalue и ссылки lvalue имеют непосредственное отношение только к левой части присваивания, когда переменная создается/привязывается. С правой стороны мы говорим о выражениях, а не о переменных, а ссылка на rvalue/lvalue актуальна только в контексте определения категории значений.

Очень простой пример для начала — это просто посмотреть на вещи чисто типа int. Переменная типа int как выражение является lvalue. Однако выражение, состоящее из вычисления функции, которая возвращает int, является rvalue. Это имеет интуитивно понятный смысл для большинства людей; ключевой момент, однако, состоит в том, чтобы отделить тип выражения (даже до того, как ссылки будут отброшены) и его категорию значения.

Это приводит к тому, что хотя переменные типа int&& могут связываться только со значениями r, это не означает, что все выражения с типом int&& являются значениями r. Фактически, согласно правилам на http://en.cppreference.com/w/cpp/language/value_category скажем, любое выражение, состоящее из имени переменной, всегда является lvalue, независимо от типа.

Вот почему вам нужно std::move для передачи ссылок rvalue в последующие функции, которые принимают ссылку rvalue. Это связано с тем, что ссылки rvalue не связываются с другими ссылками rvalue. Они связываются с rvalue. Если вы хотите получить конструктор перемещения, вам нужно дать ему rvalue для привязки, а именованная ссылка rvalue не является rvalue.

std::move — это функция, которая возвращает ссылку на значение rvalue. И какова ценностная категория такого выражения? Значение? Неа. Это xvalue. Это в основном rvalue с некоторыми дополнительными свойствами.

person Nir Friedman    schedule 22.08.2017

И в foo, и в bar выражение a является lvalue. Оператор return a; означает инициализацию объекта возвращаемого значения из инициализатора a и возврат этого объекта.

Разница между этими двумя случаями заключается в том, что разрешение перегрузки для этой инициализации выполняется по-разному в зависимости от того, объявлен ли a как энергонезависимый автоматический объект в самом внутреннем охватывающем блоке или как параметр функции.

Что это для foo, но не bar. (В bar a объявляется ссылкой). Таким образом, return a; в foo выбирает конструктор перемещения для инициализации возвращаемого значения, а return a; в bar выбирает конструктор копирования.

Полный текст C++14 [class.copy]/32:

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

где "соблюдены критерии исключения операции копирования/перемещения" относится к [class.copy]/31.1:

  • в операторе возврата в функции с типом возвращаемого значения класса, когда выражение является именем энергонезависимого автоматического объекта (кроме параметра функции или предложения catch) с тем же типом cv-unqualified, что и возвращаемый тип функции, операцию копирования/перемещения можно опустить, встроив автоматический объект непосредственно в возвращаемое значение функции.

Обратите внимание, что эти тексты изменятся для C++17.

person M.M    schedule 22.08.2017