Учтите следующее:
struct my_type {};
my_type make_my_type() { return my_type{}; }
void func(my_type&& arg) {}
int main()
{
my_type&& ref = make_my_type();
func(ref);
}
Излишне говорить, что этот код не компилируется. Я понимаю, что мне нужно использовать std::move()
во втором вызове функции, но для целей понимания я хочу рассмотреть код таким, какой он есть.
Пытаясь скомпилировать вышесказанное, Clang 3.5 говорит мне:
ошибка: нет подходящей функции для вызова ‘func’
примечание: функция-кандидат недопустима: нет известного преобразования из my_type в my_type &&’для 1-го аргумента void func (my_type&&) {}
В то время как g ++ 4.9 говорит что-то почти идентичное:
ошибка: невозможно привязать значение my_type к значению my_type&&’
примечание: инициализация аргумента 1 ‘void func (my_type&&)»
Эти сообщения об ошибках меня запутали, потому что пока ref
это конечно именующий, его тип все еще my_type&&
… не так ли?
Я пытаюсь точно понять, что здесь происходит, поэтому мне интересно, какие (если таковые имеются) из следующего являются правдой:
Поскольку только rvalue могут быть связаны с rvalue ссылками, и ref
это значение, оно не может быть связано с arg
, Сообщения об ошибках от Clang и g ++ вводят в заблуждение, утверждая, что ref
является (не ссылка) my_type
что «не может быть преобразовано».
Потому что это lvalue, ref
рассматривается для разрешения перегрузки как нереференсный my_type
несмотря на его фактический тип my_type&&
, Сообщения об ошибках от Clang и g ++ вводят в заблуждение, потому что они отображают тип, используемый внутри для сопоставления функций, а не реальный тип ref
,
В теле main()
, тип ref
является гладкий my_type
несмотря на то, что я прямо написал my_type&&
, Так что сообщения об ошибках от компиляторов точны, и я ошибаюсь. Однако, похоже, это не так, поскольку
static_assert(std::is_same<decltype(ref), my_type&&>::value, "");
проходит.
Происходит какая-то другая магия, которую я не учел.
Просто повторюсь, я знаю, что решение заключается в использовании std::move()
преобразовать rref обратно в rvalue; Я ищу объяснение того, что происходит «за кадром».
Рассмотрим эти три задания:
my_type x = func_returning_my_type_byvalue();
my_type & y = func_returning_my_type_byvalue();
my_type && z = func_returning_my_type_byvalue();
Первое — у вас есть локальная переменная x
и он инициализируется в результате вызова функции (rvalue), так что можно использовать конструктор перемещения / присваивание или конструкцию x может быть полностью исключены (пропущены и x
построен на месте func_returning_my_type_byvalue
когда он генерирует свой результат).
Обратите внимание, что x
является lvalue — вы можете взять его адрес, поэтому это также тип ссылка сам. Технически все переменные, которые не являются ссылками, являются ссылками на себя. В этом отношении lvalues являются узлом привязки для присваиваний и чтения из памяти с известной длительностью хранения.
Второй не скомпилируется — вы не можете назначить ссылку на результат (таким образом), вы должны использовать синтаксис присвоения ссылки для псевдоним существующее значение Впрочем, это прекрасно:
my_type & y = func_returning_my_type_byreference();
// `y` will never use constructors or destructors
Вот почему существует третий, когда мы необходимость ссылка на то, что мы не могу создать ссылку на использование обычного синтаксиса. В чем-то вроде func
в первоначальном вопросе, время жизни arg
не сразу очевидно. Например, мы не можем сделать это без явного перемещения:
void func( my_type && arg ) {
my_type && save_arg = arg;
}
Причина, по которой это не разрешено, заключается в том, что arg
это ссылка на значение в первую очередь. Если хранение arg
ценность (то, что это имеет в виду) должны были быть короче чем это из save_arg
, затем save_arg
вызвал бы деструктор этого значения — фактически захватывая его. Это не тот случай, save_arg
сначала исчезнет, поэтому нет смысла переводить в него lvalue, что мы можем, после func
, все еще обратитесь к потенциально!
Учтите, что даже если вы мы использовать std:move
заставить это скомпилировать. Деструктор все равно не будет вызван func
потому что вы не создали новый объект, просто новый ссылка, и затем эта ссылка уничтожается до того, как сам исходный объект вышел из области видимости.
Для всех намерений и целей arg
ведет себя так, как будто это my_type&
Как и любые другие ссылки. Хитрость заключается в продолжительности хранения и семантике продления срока службы путем передачи ссылки. Это все обычные ссылки под капотом, здесь нет «типа значения».
Если это поможет, вспомните операторы увеличения / уменьшения. Существуют две перегрузки, а не два оператора. operator++(void)
(предварительно) и operator++(int)
(сообщение). Там никогда не бывает актуальным int
После этого, компилятор имеет разные подписи для разных ситуаций / контекстов / соглашений по обработке значений. Это своего рода сделка со ссылками.
Одним словом: время жизни объекта.
Ссылка lvalue всегда должна быть присвоена использованию чего-либо с дольше срок хранения, то, что уже построено. Таким образом, нет необходимости вызывать конструкторы или деструкторы для области видимости ссылочной переменной lvalue, потому что по определению мы получаем готовый объект и забываем о нем до того, как он должен быть уничтожен.
также важно, чтобы объекты неявно уничтожались в обратном порядке:
int a; // created first, destroyed last
int b; // created second, destroyed 2nd-last
int & c = b; // fine, `c` goes out of scope before `b` per above
int && d = std::move(a); // fine, `a` outlives `d`, same situation as `c`
Если мы присвоили ссылку на rvalue, что-то, что является ссылкой lvalue, то применимо то же правило: lvalue должно по определению иметь более длительное хранилище, поэтому нам не нужно вызывать конструкторы или деструкторы для c
или даже d
, Вы не можете обмануть компилятор с std::move
на этом, потому что он знает область объекта, который перемещается — d
является однозначно короче, чем указанная ссылка, мы просто заставляем компилятор использовать проверку / контекст типа rvalue, и это все, чего мы достигли.
Разница в том, что ссылки не имеют значения — например, выражения, где могут быть ссылки на них, но эти ссылки определенно недолговечны, возможно, короче, чем продолжительность локальной переменной. Подсказка Подсказка.
Когда мы присваиваем результат вызова функции или выражения для ссылки rvalue, мы создаем ссылку на временный объект, на который иначе нельзя было бы ссылаться. Благодаря этому мы находимся в форсирование Построение на месте переменной из результата выражения. Это вариант разрешения копирования / перемещения, когда у компилятора нет другого выбора, кроме как исключить временную конструкцию на месте:
int a = 2, b = 3; // lvalues
int && temp = a + b; // temp is constructed in-place using the result of operator+(int,int)
func
Это сводится к присвоению lvalue — ссылки как аргументы функции относятся к объектам, которые может существуют дольше, чем вызов функции, и поэтому являются lvalues даже когда тип аргумента является ссылкой на значение.
Два случая:
func( std::move( variable ) ); // case 1
func( my_type() + my_type() ); // case 2
func
Нельзя угадывать, в какой ситуации мы будем использовать его раньше времени (без оптимизации). Если бы мы не допустили случай 1, то была бы законная причина считать ссылочный параметр rvalue имеющим меньшую продолжительность хранения, чем вызов функции, но это также не имело бы смысла, поскольку или объект всегда убирается внутри func
или всегда вне его, и «неизвестная» продолжительность хранения во время компиляции не является удовлетворительной.
У компилятора нет другого выбора, кроме как предположить худшее в этом случае 1 может быть случаться в конце концов, в этом случае мы должны дать гарантии на срок хранения arg
дольше, чем вызов func
в общем случае. Как следствие этого — это arg
будет считаться, что существует дольше, чем призыв к func
немного времени, и это func
сгенерированный код должен работа в обоих случаях — arg
допустимое использование и предполагаемая продолжительность хранения соответствуют требованиям my_type&
и не my_type&&
,