Я использую gcc 12.2 и обнаружил, что следующий код компилируется и дает странные результаты попробуйте в Godbolt. (P.S. Переключение на clang показывает тот же результат)
#include <iostream>
void global() { /*std::cout << "global()" << std::endl;*/ }
template <typename T> // universal reference
void invoke(T&& func, const std::string& tag) {
if constexpr (std::is_rvalue_reference_v<T>) {
std::cout << "rvalue reference " << tag << std::endl;
}
else if constexpr (std::is_lvalue_reference_v<T>) {
std::cout << "lvalue reference " << tag << std::endl;
}
else {
std::cout << "non-reference " << tag << std::endl;
}
func();
}
template <typename T>
struct Test {
Test(T&& x, const std::string& tag) // rvalue reference, not universal reference
{
if constexpr (std::is_rvalue_reference_v<T>) {
std::cout << "rvalue reference " << tag << std::endl;
}
else if constexpr (std::is_lvalue_reference_v<T>) {
std::cout << "lvalue reference " << tag << std::endl;
}
else {
std::cout << "non-reference " << tag << std::endl;
}
}
};
int main() {
int && x = 3;
using RRef = void (&&)();
RRef rref = global;
using LRef = void (&)();
LRef lref = global;
std::cout << "RRef is rvalue reference " << std::is_rvalue_reference_v<RRef> << std::endl;
std::cout << "rref is rvalue reference " << std::is_rvalue_reference_v<decltype(rref)> << std::endl;
std::cout << "move(rref) is rvalue reference " << std::is_rvalue_reference_v<decltype(std::move(rref))> << std::endl;
std::cout << "x is rvalue reference " << std::is_rvalue_reference_v<decltype(x)> << std::endl;
std::cout << "move(x) is rvalue reference " << std::is_rvalue_reference_v<decltype(std::move(x))> << std::endl;
std::cout << "==== invoke ==== " << std::endl;
invoke(global, "global");
invoke(rref, "rref");
invoke(std::move(rref), "rref2");
invoke(lref, "lref");
invoke(std::move(lref), "lref2");
std::cout << "==== Test ==== " << std::endl;
Test(global, "global");
Test(rref, "rref");
Test(std::move(rref), "rref2");
Test(lref, "lref");
Test(std::move(lref), "lref2");
std::cout << "==== Test int ==== " << std::endl;
// Test(x, "x"); // cannot bind lvalue to rvalue-reference
Test(std::move(x), "move(x)");
}
Вывод для gcc 12.2 выглядит следующим образом:
RRef is rvalue reference 1
rref is rvalue reference 1
move(rref) is rvalue reference 0 // why is this no longer rvalue reference
x is rvalue reference 1
move(x) is rvalue reference 1
==== invoke ====
lvalue reference global // why are they all lvalue reference
lvalue reference rref
lvalue reference rref2
lvalue reference lref
lvalue reference lref2
==== Test ====
non-reference global // why they are non-reference
non-reference rref
non-reference rref2
non-reference lref
non-reference lref2
==== Test int ====
non-reference move(x)
Не могли бы вы объяснить, почему мы получаем следующий вывод:
std::move(rref)
— ссылка на lvalue, а std::move(x)
— ссылка на rvalue.invoke
, который принимает универсальную ссылку, все выведенные типы являются lvalue-reference, что указывает на то, что ссылки lvalue передаются в invoke
Test
, который принимает только ссылки на rvalue, принимаются все различные входные данные, что указывает на то, что все они являются ссылками на rvalue.Передача ссылки int
ведет себя нормально, а передача ссылки функции ведет себя довольно странно.
Несмотря на то, что std::move(x)
является ссылкой на rvalue, std::move(rref)
является lvalue, потому что это особый случай в стандарте. Из стандарта C++14 [expr.call/p10]:
Вызов функции является lvalue, если тип результата является ссылочным типом lvalue или ссылкой rvalue на тип функции, xvalue, если тип результата является ссылкой rvalue на тип объекта, и prvalue в противном случае.
Это подробно объясняется в этом ответе.
При передаче значений в invoke()
вы передаете их либо
invoke(global, "global");
invoke(rref, "rref");
std::move(ref-to-function)
как здесь:
invoke(std::move(rref), "rref2");
Это также lvalue из-за особого случая, упомянутого в (1).Таким образом, аргумент шаблона привязывается к lvalue во всех случаях.
Обратите внимание, что ваш конструктор Test
выводит информацию о типе аргумента шаблона T
, а не о типе параметра x
(то есть T&&
). Вот почему вы видите «не ссылка» там, где ожидаете «ссылку на rvalue». Если мы заменим код для вывода информации о decltype(x)
вместо T
, мы получим ожидаемый результат «ссылка на rvalue».
Что касается того, почему ссылка на rvalue в конструкторе Test
принимает входные данные lvalue. Это снова особый случай в стандарте, который разрешает привязку ссылок rvalue к lvalues функции. Из стандарта C++14 [over.ics.ref/3]:
За исключением неявного параметра объекта, для которого см. [over.match.funcs], стандартная последовательность преобразования не может быть сформирована, если она требует привязки ссылки lvalue, отличной от ссылки на неизменяемый тип const, к rvalue или привязки rvalue ссылка на lvalue, отличную от функции lvalue.
В любом случае, использование rvalue-reference-to-function довольно редко и неясно, обычно ссылки lvalue достаточно для большинства нужд (хотя здесь — один из примеров, когда нужна ссылка rvalue).
@user17732522 user17732522 Я думаю, что ваш комментарий делает ответ полным. Из первых двух пунктов ответа я понял, что все пять случаев global/lref/...
— это lvalue. Тогда я не могу понять, «почему они могут быть привязаны к параметру конструктора Test(T&&)
, который принимает только ссылку rvalue», пока не прочитаю ваш комментарий. Не могли бы вы указать источник/ссылку на это исключение?
@ vvv444 спасибо за ваш ответ, который очень полезен. Теперь я понял, что все параметры, переданные invoke
, являются ссылками на lvalue. Не могли бы вы также добавить некоторые пояснения, почему эти ссылки lvalue принимаются конструктором Test
, который должен быть ссылкой rvalue?
Спасибо. Добавленная цитата указывает, что «константная ссылка lvalue может быть привязана к ссылке rvalue» и «ссылка rvalue может быть привязана к функции lvalue».
@doraemon Теперь в ответе содержится цитата из стандарта, которая разрешает успешное разрешение перегрузки в этих обстоятельствах. Тогда вам нужна только часть «or function lvalue» из eel.is/c++draft/dcl.init.ref#5.3.1, чтобы фактически определить привязку ссылки, и поведение будет полностью объяснено.
И для 3. также важно, что есть еще одно исключение для типов функций в привязке ссылок, которое позволяет lvalues привязываться к ссылкам rvalue.