Почему & quot; универсальные ссылки & quot; имеют тот же синтаксис, что и ссылки rvalue?

Я только что провел исследование об этих (вполне) новых функциях, и мне интересно, почему Комитет 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. Кто-нибудь может объяснить решение комитета?

32

Решение

Я думаю, что это произошло наоборот. Первоначальная идея состояла в том, чтобы ввести rvalue-ссылки в язык, что означает, что «код, предоставляющий ссылку с двойным амперсандом, не заботится о том, что произойдет с объектом, на который делается ссылка». Это позволяет переместить семантику. Это мило.

Сейчас. Стандарт запрещает создавать ссылку на ссылку, но это всегда было возможно. Рассматривать:

template<typename T>
void my_func(T, T&) { /* ... */ }

// ...

my_func<int&>(a, b);

В этом случае тип второго параметра должен быть int & &, но это явно запрещено в стандарте. Поэтому ссылки должны быть свернуты даже в C ++ 98. В C ++ 98 был только один вид ссылок, поэтому правило свертывания было простым:

& & -> &

Теперь у нас есть два вида ссылок, где && означает «мне все равно, что может случиться с объектом», и & что означает «меня может волновать, что может случиться с объектом, поэтому вам лучше посмотреть, что вы делаете». Имея это в виду, сворачивающиеся правила текут естественным образом: C ++ должен свернуть ссылки на && только если никто не заботится о том, что происходит с объектом:

& & -> &
& && -> &
&& & -> &
&& && -> &&

С этими правилами я думаю, что Скотт Мейерс заметил, что это подмножество правил:

& && -> &
&& && -> &&

Показывает, что && является нейтральным справа по отношению к эталонному коллапсу, и, когда происходит вывод типа, T&& Конструкция может использоваться, чтобы соответствовать любому типу ссылки, и придумал термин «Универсальная ссылка» для этих ссылок. Это не то, что было изобретено Комитетом. Это всего лишь побочный эффект других правил, а не дизайн комитета.

И поэтому был введен термин, чтобы различать РЕАЛЬНЫЕ rvalue-ссылки, когда не происходит дедукции типа, которые гарантированно будут &&и те выведенные типом УНИВЕРСАЛЬНЫЕ ссылки, которые не гарантированно останутся && во время специализации шаблона.

36

Другие решения

Другие уже упоминали, что правила свертывания ссылок являются ключевыми для универсальных ссылок на работу, но есть еще один (возможно) не менее важный аспект: выведение аргумента шаблона, когда параметр шаблона имеет форму 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. Во-вторых, и, самое главное, это было очень простое решение для утверждения в Стандарте (с изменением одного абзаца) и для реализации компиляторами. Пожалуйста, не поймите меня неправильно. Я не говорю, что члены комитета ленивы (они, конечно, нет). Я просто говорю, что они минимизировали риск побочного ущерба.

7

Вы ответили на свой собственный вопрос: «универсальная ссылка» — это просто название для случая ссылки на rvalue. Если бы для свертывания ссылок требовался другой синтаксис, это не было бы свертыванием ссылок. Свертывание ссылок — это просто применение спецификатора ссылки к типу ссылки.

поэтому мне было интересно, что думает об этом g ++. Конечно, это не скомпилировано.

Ваш первый пример правильно сформирован. GCC 4.9 компилирует его без жалоб, и выходные данные согласуются с MSVC.

Почти, потому что я подумал о том, что если я захочу что-то изменить в состоянии объекта, которое передается по ссылке rvalue?

Rvalue ссылки не применяются const семантика; Вы всегда можете изменить состояние объекта, переданного move, Изменчивость необходима для их цели. Хотя есть такая вещь, как const &&Вам это никогда не нужно.

6

Универсальные ссылки работают из-за правил свертывания ссылок в c ++ 11. если у вас есть

template <typename T> func (T & t)

Свертывание ссылок все еще происходит, но оно не будет работать с временным, поэтому ссылка не является «универсальной». Универсальная ссылка называется «универсальной», потому что она может принимать lvals и rvals (также сохраняет другие квалификаторы). T & t не универсален, так как не может принимать rvals.

Итак, чтобы подвести итог, универсальные ссылки являются продуктом свертывания ссылок, и универсальная ссылка называется таковой, потому что она универсальна, какой она может быть

0

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

0
По вопросам рекламы [email protected]