Я только что провел исследование об этих (вполне) новых функциях, и мне интересно, почему Комитет C ++ решил ввести одинаковый синтаксис для них обоих? Кажется, что разработчикам не нужно тратить некоторое время, чтобы понять, как это работает, и одно решение позволяет подумать о дальнейших проблемах. В моем случае это началось с проблемы, которую можно упростить до этого:
#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. Поэтому мой вопрос: почему Комитет C ++ не ввел какой-то другой синтаксис для универсальных ссылок? Я думаю, что эта функция должна сигнализироваться, например, T ?, auto? Или что-то подобное, но не как T&& и авто&& которые просто сталкиваются с rvalue ссылками. Используя этот подход, моя первая реализация была бы совершенно правильной, не только для компилятора MS. Кто-нибудь может объяснить решение комитета?
Я думаю, что это произошло наоборот. Первоначальная идея состояла в том, чтобы ввести rvalue-ссылки в язык, что означает, что «код, предоставляющий ссылку с двойным амперсандом, не заботится о том, что произойдет с объектом, на который делается ссылка». Это позволяет переместить семантику. Это мило.
Сейчас. Стандарт запрещает создавать ссылку на ссылку, но это всегда было возможно. Рассматривать:
template<typename T>
void my_func(T, T&) { /* ... */ }
// ...
my_func<int&>(a, b);
В этом случае тип второго параметра должен быть int & &
, но это явно запрещено в стандарте. Поэтому ссылки должны быть свернуты даже в C ++ 98. В C ++ 98 был только один вид ссылок, поэтому правило свертывания было простым:
& & -> &
Теперь у нас есть два вида ссылок, где &&
означает «мне все равно, что может случиться с объектом», и &
что означает «меня может волновать, что может случиться с объектом, поэтому вам лучше посмотреть, что вы делаете». Имея это в виду, сворачивающиеся правила текут естественным образом: C ++ должен свернуть ссылки на &&
только если никто не заботится о том, что происходит с объектом:
& & -> &
& && -> &
&& & -> &
&& && -> &&
С этими правилами я думаю, что Скотт Мейерс заметил, что это подмножество правил:
& && -> &
&& && -> &&
Показывает, что &&
является нейтральным справа по отношению к эталонному коллапсу, и, когда происходит вывод типа, T&&
Конструкция может использоваться, чтобы соответствовать любому типу ссылки, и придумал термин «Универсальная ссылка» для этих ссылок. Это не то, что было изобретено Комитетом. Это всего лишь побочный эффект других правил, а не дизайн комитета.
И поэтому был введен термин, чтобы различать РЕАЛЬНЫЕ rvalue-ссылки, когда не происходит дедукции типа, которые гарантированно будут &&
и те выведенные типом УНИВЕРСАЛЬНЫЕ ссылки, которые не гарантированно останутся &&
во время специализации шаблона.
Другие уже упоминали, что правила свертывания ссылок являются ключевыми для универсальных ссылок на работу, но есть еще один (возможно) не менее важный аспект: выведение аргумента шаблона, когда параметр шаблона имеет форму T&&
,
Собственно, по отношению к вопросу:
Почему «универсальные ссылки» имеют тот же синтаксис, что и ссылки на rvalue?
на мой взгляд, форма параметра шаблона более важна, потому что это все о синтаксисе. В C ++ 03 не было способа для функции шаблона узнать категорию значения (rvalue или lvalue) переданного объекта. C ++ 11 изменил вычет аргумента шаблона, чтобы учесть это: 14.8.2.1 [temp.deduct.call] / p3
[…] ЕслиP
является rvalue ссылкой на неквалифицированный cv параметр шаблона, а аргумент является lvalue, тип «lvalue ссылка наA
«используется вместоA
для вывода типа.
Это немного сложнее, чем первоначально предложенная формулировка n1770):
Если
P
является rvalue-ссылочным типом формырезюме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
(не arg
) который записывает категорию значений исходного объекта. (В качестве меры предосторожности, определение std::forward
«искусственно» заставляет последнюю компиляцию провалиться, чтобы программисты не допустили эту ошибку.)
Вернуться к исходному вопросу: «Почему комитет решил использовать форму T&&
а не выбирать новый синтаксис?
Я не могу сказать настоящую причину, но могу предположить. Во-первых, это обратно совместимо с C ++ 03. Во-вторых, и, самое главное, это было очень простое решение для утверждения в Стандарте (с изменением одного абзаца) и для реализации компиляторами. Пожалуйста, не поймите меня неправильно. Я не говорю, что члены комитета ленивы (они, конечно, нет). Я просто говорю, что они минимизировали риск побочного ущерба.
Вы ответили на свой собственный вопрос: «универсальная ссылка» — это просто название для случая ссылки на rvalue. Если бы для свертывания ссылок требовался другой синтаксис, это не было бы свертыванием ссылок. Свертывание ссылок — это просто применение спецификатора ссылки к типу ссылки.
поэтому мне было интересно, что думает об этом g ++. Конечно, это не скомпилировано.
Ваш первый пример правильно сформирован. GCC 4.9 компилирует его без жалоб, и выходные данные согласуются с MSVC.
Почти, потому что я подумал о том, что если я захочу что-то изменить в состоянии объекта, которое передается по ссылке rvalue?
Rvalue ссылки не применяются const
семантика; Вы всегда можете изменить состояние объекта, переданного move
, Изменчивость необходима для их цели. Хотя есть такая вещь, как const &&
Вам это никогда не нужно.
Универсальные ссылки работают из-за правил свертывания ссылок в c ++ 11. если у вас есть
template <typename T> func (T & t)
Свертывание ссылок все еще происходит, но оно не будет работать с временным, поэтому ссылка не является «универсальной». Универсальная ссылка называется «универсальной», потому что она может принимать lvals и rvals (также сохраняет другие квалификаторы). T & t
не универсален, так как не может принимать rvals.
Итак, чтобы подвести итог, универсальные ссылки являются продуктом свертывания ссылок, и универсальная ссылка называется таковой, потому что она универсальна, какой она может быть
В первую очередь, Причина, по которой первый пример не скомпилирован с gcc 4.8, заключается в том, что это ошибка в gcc 4.8. (Я расскажу об этом позже). Первый пример компилирует, запускает и выдает тот же вывод, что и VS2013 с версиями gcc после 4.8, clang 3.3 и более поздними версиями и компилятором c ++ от 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. Эта единственная функция универсально принимает lvalue и rvalues в качестве аргументов. В случае звонков на f(a)
а также f(b)
функция сообщает, что T&&
не является ссылкой на значение. Призывы к f(a)
, f(b)
а также f(0)
соответственно становятся вызовами функций f<int&>(int&)
, f<const int&>(const int&)
, f<int&&>(int&&)
, Только в случае f(0)
это делает T&&
стать референтной ссылкой. С аргументом 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)
, последнее благодаря правилам свертывания ссылок C ++ 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&&)
, Согласно пункту 9 пункта 14.8.2.4, «если тип из шаблона аргумента был ссылкой на lvalue, а тип из шаблона параметра не был, тип аргумента считается более специализированным, чем другой».