Зачем нужны правила сворачивания ссылок

Все могут знать, что мы используем rvalue-ссылки в сочетании с правилами свертывания ссылок для создания идеальных функций пересылки, например так

template<typename T>
void f(T&& arg) {
    otherfunc(std::forward<T>(arg));
}

f(4);

Правила свертывания ссылок похожи на

+------+-----+--------+
| T    | Use | Result |
|------|--------------|
| X&   | T&  | X&     |
| X&   | T&& | X&     |
| X&&  | T&  | X&     |
| X&&  | T&& | X&&    |
+------+-----+--------+

Итак, в моем примере f, T — это int&&, а T&& — это int&& &&, которое сворачивается в int&&.

Мой вопрос: зачем нам эти правила, если T уже выведено в int&&? Почему будет

template<typename T>
void f(T arg);

f(4);

превратиться в void f(int) вместо void f(int&&), если T будет int&&? Если T на самом деле int, а T&& - это то, что превращает его в int&& и, следовательно, void f(int&&), то зачем нам нужны правила свертывания ссылок, если кажется, что они никогда не применяются? Это единственные два варианта, которые я могу сказать из своих ограниченных знаний, поэтому, очевидно, есть правило, о котором я не знаю.

Также было бы полезно увидеть цитату из стандарта по этому поводу.


person Kal    schedule 05.10.2013    source источник
comment
Стандарт содержит правила, но чаще всего эти правила не обоснованы.   -  person David Rodríguez - dribeas    schedule 05.10.2013
comment
@DavidRodríguez-dribeas, даже если нет причины, мне просто нужно знать, почему T иногда используется как int, а иногда как int&&, и когда это так, я могу предсказать поведение своего кода.   -  person Kal    schedule 05.10.2013
comment
@Kal: Не поймите меня неправильно, есть причины, просто вы не найдете это в стандарте (то есть Было бы также полезно увидеть цитату из стандарта об этом ну стандарт не столько объясняет причины, сколько устанавливает правила)   -  person David Rodríguez - dribeas    schedule 05.10.2013


Ответы (2)


Мой вопрос: зачем нам нужны эти правила, если T уже выведен в int&&?

Это не совсем так. Правила вывода типа не позволяют сделать вывод, что аргумент является ссылкой. То есть в:

template <typename T>
void f(T);

И выражения:

X g();
X& h();
X a;
f(g());        // argument is an rvalue, cannot be bound by lvalue-ref
f(h());        // argument is an lvalue
f(a);          // argument is an lvalue

Выведенный тип будет X в последних двух случаях, и он не сможет скомпилироваться в первом. Выведенный тип будет типом value, а не ссылочным типом.

Следующий шаг — выяснить, каким будет выведенный тип, если шаблон принимает аргумент по ссылке lvalue или rvalue. В случае ссылок lvalue параметры ясны, с измененным f:

template <typename T>
void f(T &);

f(g());       // only const& can bind an rvalue: f(const X&), T == const int
f(h());       // f(X&)
f(a);         // f(X&)

До сих пор это уже было определено в предыдущей версии стандарта. Теперь вопрос в том, какими должны быть выведенные типы, если шаблон принимает rvalue-ссылки. Это то, что было добавлено в C++11. Рассмотрим теперь:

template <typename T>
void f(T &&);

И rvalue будет связываться только с rvalue и никогда с lvalue. Это означает, что при использовании тех же простых правил, что и для lvalue-ссылок (какой тип T вызовет компиляцию), второй и третий вызовы не компилируются:

f(g());     // Fine, and rvalue-reference binds the rvalue
f(h());     // an rvalue-reference cannot bind an lvalue!
f(a);       // an rvalue-reference cannot bind an lvalue!

Без правил свертывания ссылок пользователь должен был бы предоставить две перегрузки для шаблона: одну, которая принимает ссылку на rvalue, и другую, которая принимает ссылку на lvalue. Проблема в том, что по мере увеличения количества аргументов количество альтернатив растет экспоненциально, и реализация идеальной переадресации становится почти такой же сложной в C++03 (с единственным преимуществом в возможности обнаружения rvalue с помощью rvalue-ссылки).

Поэтому нужно сделать что-то другое, и это свертывание ссылок, которое на самом деле является способом описания желаемой семантики. Другой способ их описания состоит в том, что когда вы вводите && с помощью аргумента шаблона, вы на самом деле не запрашиваете ссылку-rvalue-reference, так как это не позволило бы вызвать с lvalue, но вы скорее просите компилятор дать вам лучший тип сопоставления ссылок.

person David Rodríguez - dribeas    schedule 05.10.2013
comment
Для вашего примера f(g()); // only const& can bind an rvalue: f(const X&), T == const int, который у меня не компилируется - person Kal; 05.10.2013
comment
Итак, вы говорите, что нет единого правила, которое заботится обо всех случаях, а есть разные правила для каждого случая: T, T& и T&&, верно? - person Kal; 05.10.2013
comment
@Kal: Это не правила, а попытка объяснить, почему эти правила необходимы/полезны. Без свертывания ссылок вы не сможете реализовать идеальную пересылку без экспоненциального взрыва. - person David Rodríguez - dribeas; 05.10.2013
comment
Я уже знаю, почему правила полезны, но я не знаю правил, которые делают эти правила необходимыми, или правил, которые используют правила (разрушения ссылок). Это мой вопрос. - person Kal; 05.10.2013
comment
Итак, если вам нужны дополнительные разъяснения, с позывным f(4) почему T = int с void f(T);, а T = int&& с void f(T&&), хотя место вызова выглядит точно так же? В обоих случаях они вызываются со значением x/r. Почему вычитание T отличается, хотя в первом случае с void f(T), если поставить T = int&& будет нормально и законно? - person Kal; 05.10.2013
comment
@Kal: Если бы все было так, как вы говорите, как бы вы написали шаблон, который принимает аргумент по значению? Прочтите начало ответа, правила вывода аргументов требуют, чтобы выводимый тип для f(1) в template <typename T> void f(T); был int, а не int&&. Я думаю, что вы основываете свой аргумент на неверном предположении, что выведенный тип равен int&&, когда он равен int. Только если вы добавите &&, вы можете получить ссылку rvalue (или ссылку lvalue из-за свертывания ссылки). - person David Rodríguez - dribeas; 06.10.2013
comment
Да, это была моя проблема, я думал, что T может быть int&& с f(4). Спасибо. - person Kal; 06.10.2013

В вашем примере T это int, а не int&&. Вариант void f(T arg) всегда будет принимать параметр по значению, создавая копию. Это, конечно, лишает смысла идеальную пересылку: otherfunc вполне может брать свой параметр по ссылке, избегая копирования; вы могли бы даже вызвать его с классом, который нельзя копировать.

С другой стороны, представьте, что вы вызываете f(), передавая lvalue, скажем, класса C. Затем T превращается в C&, а T&& становится C& &&, сворачиваясь в C&. Таким образом, идеальная переадресация сохраняет, так сказать, «ценность». Вот для чего нужны разрушающиеся правила.

Рассмотреть возможность:

#include <utility>
#include <iostream>
using namespace std;

class C {
public:
    C() : x(0) {}
    int x;
private:
    C(const C&);
};

void otherfunc(C& c) { c.x = 1; }

template<typename T>
void f(T&& arg) {
    otherfunc(std::forward<T>(arg));
}

template<typename T>
void g(T arg) {
    otherfunc(std::forward<T>(arg));
}

int main() {
    C c;
    f(c);  // OK
//  g(c);  // Error: copy constructor inaccessible

    cout << c.x;  // prints 1
    return 0;
}
person Igor Tandetnik    schedule 05.10.2013
comment
Ну 4 - это rvalue, не так ли? Итак, как вы говорите, если T это int, а не int&&, то T&& это просто int&& и правила свертывания ссылок не вступают в игру. - person Kal; 05.10.2013
comment
В случае с f(4) действительно нет. В моем примере f(c) да. Обратите внимание, что оба раза это одна и та же функция, отличается только форма вызова. В этом суть идеальной переадресации — вы можете написать единый шаблон функции, который принимает и пересылает все виды аргументов, сохраняя их важные характеристики, такие как константность или lvalue-ность. - person Igor Tandetnik; 05.10.2013
comment
Итак, тогда в f(4) T есть int, а в f(c) int есть &, в каком случае T int&&? Что бы вы сказали в случае f(std::move(y))? Тогда почему это отличается от f(4)? - person Kal; 05.10.2013
comment
Ни в коем случае T никогда не выводится как int&&. Вы могли бы, я полагаю, явно вызвать f<int&&>(4), если бы вы были так склонны. Почему что отличается от f(4)? Я не понимаю этот вопрос. - person Igor Tandetnik; 05.10.2013
comment
Я думаю, что T можно вывести из int&&, иначе правило T&& && никогда не применялось бы. - person Kal; 05.10.2013
comment
@Kal T не может считаться int&&. Это правило можно применять вне вывода аргументов шаблона: using foo = int&&; using bar = foo&&. Происходит свертывание ссылок, и bar равно int&&. - person Simple; 06.10.2013
comment
@ Простой ах, я вижу, если вы напишете это как ответ, я приму это. - person Kal; 06.10.2013