Подтвердить что ты не робот

Почему "универсальные ссылки" имеют тот же синтаксис, что и ссылки 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-компилятора. Может ли кто-нибудь объяснить решение Комитета?

4b9b3361

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