Запутанные сообщения об ошибках с именованными ссылками rvalue

Учтите следующее:

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 безусловно, lvalue, его тип по-прежнему 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; Я ищу объяснение того, что происходит "за кадром".

1 ответ

Рассмотрим эти три задания:

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 ​​являются узлом привязки для присваиваний и чтения из памяти с известной длительностью хранения.

Второе не скомпилируется - вы не можете назначить ссылку на результат (таким образом), вы должны использовать синтаксис назначения ссылок для псевдонима существующего lvalue. Впрочем, это прекрасно:

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 Проходя мимо, компилятор имеет разные подписи для разных ситуаций / контекстов / соглашений о обработке значений. Это своего рода сделка со ссылками.

Если ссылки на rvalue и lvalue всегда называются lvalue, в чем разница?

Одним словом: время жизни объекта.

Ссылка 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 - ссылки в качестве аргументов функции относятся к объектам, которые могут существовать дольше, чем вызов функции, и, таким образом, являются lvalue, даже если тип аргумента является ссылкой на rvalue.

Два случая:

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&&,

Другие вопросы по тегам