Почему "универсальные ссылки" имеют тот же синтаксис, что и ссылки rvalue?
Я только что немного исследовал эти (совершенно) новые функции, и мне интересно, почему Комитет С++ решил ввести для них один и тот же синтаксис? Похоже, разработчикам не нужно тратить время, чтобы понять, как это работает, и одно решение позволяет подумать о дальнейших проблемах. В моем случае это началось с проблемы, которая может быть упрощена до этого:
#include <iostream>
template <typename T>
void f(T& a)
{
std::cout << "f(T& a) for lvalues\n";
}
template <typename T>
void f(T&& a)
{
std::cout << "f(T&& a) for rvalues\n";
}
int main()
{
int a;
f(a);
f(int());
return 0;
}
Я скомпилировал его во-первых на VS2013, и он работал так, как я ожидал, с такими результатами:
f(T& a) for lvalues
f(T&& a) for rvalues
Но была одна подозрительная вещь: intellisense подчеркнула f (a). Я сделал некоторые исследования, и я понял, что это потому, что тип рушится (универсальные ссылки, как Скотт Мейерс назвал его), поэтому я подумал о том, что g++ думает об этом. Конечно, он не был составлен. Очень приятно, что Microsoft внедрила свой компилятор для работы более интуитивно понятным способом, но я не уверен, соответствует ли он стандарту и должна ли быть такая разница в IDE (компилятор против intellisense, но на самом деле это может в этом есть смысл). Хорошо, вернемся к проблеме. Я решил это так:
template <typename T>
void f(T& a)
{
std::cout << "f(T& a) for lvalues\n";
}
template <typename T>
void f(const T&& a)
{
std::cout << "f(T&& a) for rvalues\n";
}
Теперь не было никакого коллапсинга типа, просто нормальная перегрузка для (r/l) значений. Он составлен на g++, intellisense перестала жаловаться, и я был почти доволен. Почти, потому что я думал о том, что, если я захочу что-то изменить в состоянии объекта, которое передается ссылкой rvalue? Я мог бы описать некоторую ситуацию, когда это может понадобиться, но это описание слишком длинное, чтобы представить его здесь. Я решил это так:
template <typename T>
void f(T&& a, std::true_type)
{
std::cout << "f(T&& a) for rvalues\n";
}
template <typename T>
void f(T&& a, std::false_type)
{
std::cout << "f(T&& a) for lvalues\n";
}
template <typename T>
void f(T&& a)
{
f(std::forward<T>(a), std::is_rvalue_reference<T&&>());
}
Теперь он компилируется на всех тестируемых компиляторах и позволяет мне изменять состояние объекта в реализации ссылочной ссылки rvalue, но это выглядит не очень хорошо, и это связано с тем же синтаксисом для универсальных ссылок и ссылок на значения rvalue. Поэтому мой вопрос: почему комитет С++ не представил другого синтаксиса для универсальных ссылок? Я думаю, что эта функция должна сигнализироваться, например, T?, auto?, или что-то подобное, но не как T && и авто && которые просто сталкиваются с ссылками rvalue. Используя этот подход, моя первая реализация была бы совершенно правильной не только для MS-компилятора. Может ли кто-нибудь объяснить решение Комитета?
Ответы
Ответ 1
Я думаю, это случилось наоборот. Первоначальная идея состояла в том, чтобы ввести rvalue-ссылки в язык, что означает, что "код, предоставляющий ссылку с двумя амперсандами, не заботится о том, что произойдет с указанным объектом". Это позволяет перемещать семантику. Это приятно.
Теперь. Стандарт запрещает конструировать ссылку на ссылку, но это всегда было возможно. Рассмотрим:
template<typename T>
void my_func(T, T&) { /* ... */ }
// ...
my_func<int&>(a, b);
В этом случае тип второго параметра должен быть int & &
, но это явно запрещено в стандарте. Поэтому ссылки должны быть свернуты даже на С++ 98. В С++ 98 был только один вид ссылки, поэтому правило сбрасывания было простым:
& & -> &
Теперь у нас есть два типа ссылок, где &&
означает "Меня не волнует, что может случиться с объектом", а &
означает "Я могу заботиться о том, что может случиться с объектом, поэтому вам лучше посмотреть, что вы делаете". Имея это в виду, правила сворачивания естественно протекают: С++ должен свернуть referecnces до &&
, только если никто не заботится о том, что происходит с объектом:
& & -> &
& && -> &
&& & -> &
&& && -> &&
С учетом этих правил я думаю, что Скотт Мейерс заметил, что это подмножество правил:
& && -> &
&& && -> &&
Показывает, что &&
является нейтральным по отношению к сбрасыванию ссылки, и, когда происходит дедукция типа, конструкцию T&&
можно использовать для соответствия любому типу ссылок и придумал термин "универсальная ссылка" для этих Рекомендации. Комитет не является тем, что было придумано Комитетом. Это только побочный эффект других правил, а не дизайн Комитета.
И поэтому этот термин был введен для различения REAL rvalue-ссылок, когда не происходит декомпозиции типа, которые гарантируются как &&
, и эти универсальные ссылки, выводимые по типу, которые не гарантируют остаться &&
при времени специализации шаблона.
Ответ 2
Другие уже упомянули, что правила коллапса ссылок являются ключевыми для универсальных ссылок на работу, но есть другой (возможно) не менее важный аспект: вывод аргумента шаблона, когда параметр шаблона имеет форму T&&
.
Собственно, в связи с вопросом:
Почему "универсальные ссылки" имеют тот же синтаксис, что и ссылки на rvalue?
по-моему, форма параметра шаблона важнее, потому что это все о синтаксисе. В С++ 03 не было способа для функции шаблона знать категорию значения (rvalue или lvalue) переданного объекта. С++ 11 изменил вывод аргумента шаблона, чтобы учесть это: 14.8.2.1 [temp.deduct.call]/p3
[...] Если P
является ссылкой rvalue на параметр cv-unqualified template, а аргумент является lvalue, вместо A
используется тип "lvalue reference to A
" для типа вычет.
Это немного сложнее, чем первоначально предложенная формулировка (данная n1770):
Если P
является ссылочным типом rvalue формы cv T&&
, где T
является параметром типа шаблона, а аргумент является lvalue, значение аргумента выводимого шаблона для T
составляет A&
. [Пример:
template<typename T> int f(T&&);
int i;
int j = f(i); // calls f<int&>(i)
--- конец примера]
Более подробно, вызов выше запускает экземпляр f<int&>(int& &&)
, который после применения обратного коллапса становится f<int&>(int&)
. С другой стороны, f(0)
создает экземпляр f<int>(int&&)
. (Обратите внимание: нет &
внутри < ... >
.)
Никакая другая форма объявления не выводит T
в int&
и не запускает экземпляр f<int&>( ... )
. (Обратите внимание, что &
может отображаться между ( ... )
, но не между < ... >
.)
Таким образом, при выполнении вывода типа синтаксическая форма T&&
- это то, что позволяет использовать категорию значений исходного объекта внутри тела шаблона функции.
В связи с этим, обратите внимание, что нужно использовать std::forward<T>(arg)
, а не std::forward(arg)
именно потому, что он T
(not arg
), который записывает категорию значений исходного объекта. (В качестве меры предосторожности определение std::forward
"искусственно" заставляет последнюю компиляцию не помешать программистам совершить эту ошибку.)
Вернуться к исходному вопросу: "Почему комитет решил использовать форму T&&
, а не выбрать новый синтаксис?"
Я не могу сказать правду, но я могу рассуждать. Сначала это обратно совместимо с С++ 03. Второе и, самое главное, это было очень простое решение для состояния в Стандарте (изменение одного абзаца) и для реализации компиляторами. Пожалуйста, не пойми меня неправильно. Я не говорю, что члены комитета ленивы (они, конечно же, нет). Я просто говорю, что они минимизировали риск сопутствующего ущерба.
Ответ 3
Вы ответили на свой собственный вопрос: "универсальная ссылка" - это просто имя для ссылочного примера ссылочного обращения. Если для ссылочного свертывания потребовался другой синтаксис, это не было бы ссылкой на рушится. Смещение ссылок просто применяет ссылочный квалификатор к ссылочному типу.
поэтому я подумал о том, что g++ думает об этом. Конечно, он не скомпилировался.
Ваш первый пример хорошо сформирован. GCC 4.9 компилирует его без жалобы, и результат согласуется с MSVC.
Почти, потому что я думал о том, что, если я захочу что-то изменить в состоянии объекта, которое проходит по ссылке rvalue?
Ссылки Rvalue не применяются семантика const
; вы всегда можете изменить состояние объекта, прошедшего через move
. Для их цели необходима взаимная совместимость. Хотя есть такая вещь, как const &&
, вам никогда не понадобится.
Ответ 4
Универсальные ссылки работают из-за правил сбрасывания ссылок в С++ 11. если у вас есть
template <typename T> func (T & t)
Ссылка на развал все еще происходит, но она не будет работать с временным, поэтому ссылка не является "универсальной". Универсальная ссылка называется "универсальной", потому что она может принимать lvals и rvals (также сохраняет другие квалификаторы). T & t
не является универсальным, поскольку он не может принимать rvals.
Итак, чтобы подвести итог, универсальные ссылки - это продукт ссылочного коллапсинга, а универсальная ссылка называется как таковая, потому что она универсальна, что она может быть
Ответ 5
В первую очередь, причина, по которой первый пример не был скомпилирован с gcc 4.8, заключается в том, что это ошибка в gcc 4.8. (Я расскажу об этом позже). Первый пример компилирует, запускает и производит тот же вывод, что и VS2013, с пост-4.8 версиями gcc, clang 3.3 и более поздних версий и компилятором С++ на основе Apple LLVM.
Об универсальных ссылках:
Одна из причин, по которой Скотт Мейер придумал термин "универсальные ссылки", состоит в том, что T&&
как аргумент шаблона функции соответствует lvalues, а также rvalues. Универсальность T&&
в шаблонах можно увидеть, удалив первую функцию из первого примера в вопросе:
// Example 1, sans f(T&):
#include <iostream>
#include <type_traits>
template <typename T>
void f(T&&) {
std::cout << "f(T&&) for universal references\n";
std::cout << "T&& is an rvalue reference: "
<< std::boolalpha
<< std::is_rvalue_reference<T&&>::value
<< '\n';
}
int main() {
int a;
const int b = 42;
f(a);
f(b);
f(0);
}
Выше компилируется и выполняется на всех вышеперечисленных компиляторах, а также на gcc 4.8. Эта одна функция универсально принимает значения lvalues и rvalues в качестве аргументов. В случае вызовов f(a)
и f(b)
функция сообщает, что T&&
не является ссылкой на rvalue. Призывы к f(a)
, f(b)
и f(0)
соответственно становятся вызовами функций f<int&>(int&)
, f<const int&>(const int&)
, f<int&&>(int&&)
. Только в случае f(0)
есть ли T&&
значение rvalue. Поскольку аргумент T&& foo
может быть или не быть ссылкой rvalue в случае шаблона функции, лучше назвать это чем-то другим. Мейерс решил назвать их "универсальными ссылками".
Почему это ошибка в gcc 4.8:
В первом примере кода в вопросе шаблоны функций template <typename T> void f(T&)
и template <typename T> void f(T&&)
становятся f<int>(int&)
и f<int&>(int&)
в отношении вызова f(a)
, последний благодаря правилам свертывания ссылок С++ 11. Эти две функции имеют одну и ту же подпись, поэтому, возможно, gcc 4.8 верна, что вызов f(a)
неоднозначен. Это не так.
Причина, по которой вызов не является двусмысленным, заключается в том, что template <typename T> void f(T&)
более специализирован, чем template <typename T> void f(T&&)
в соответствии с правилами разделов 13.3 (Разрешение перегрузки), 14.5.6.2 (Частичное упорядочение шаблонов функций) и 14.8.2.4 ( Вывод шаблонных аргументов при частичном упорядочении). При сравнении template <typename T> void f(T&&)
с template <typename T> void f(T&)
с T=int&
, оба продукта f(int&)
. Здесь не может быть никакого различия. Однако при сравнении template<typename T> void f(T&)
с template <typename T> void f(T&&)
с T=int
первая более специализирована, потому что теперь мы имеем f(int&)
по сравнению с f(int&&)
. В пункте 14.8.2.4 пункта 9 "если тип из шаблона аргумента был ссылкой lvalue и тип из шаблона параметра не был, тип аргумента считается более специализированным, чем другой".