Reference collapsing rule

Еще до С++11 ввели правило, что ссылка на ссылку - это ссылка без вложенности. В связи с введением rvalue-ссылок правило пришлось дополнить.

(A&)& -> A&
(A&)&& -> A&
(A&&)& -> A&
(A&&)&& -> A&&

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

Важный момент! Помните, что

int a = 42;

int& b = a; // lvalue-ссылка, имеющая lvalue категорию
const int& b = a; // то же самое

int&& f() {
    return 42;
}

f(); // rvalue-ссылка, имеющая xvalue категорию
int&& b = std::move(a); // rvalue-ссылка, имеющая lvalue категорию

int g(int&& a) { // a - rvalue-ссылка, имеющая lvalue категорию
    return a;
}

Универсальная ссылка

В C++11 правила вывода шаблонных параметров были определены специальным образом, который позволил сохранять информацию о том, ссылка какого типа в функцию передавалась.

template <typename T>
void g(T&& a) {
    f(a);
}

int main() {
    g(42); // rvalue: T -> int, void g(int&&)
    int a;
    g(a); // lvalue: T -> int&, void g(int&)
}

Шаблонная "rvalue"-ссылка ведет себя по-разному в зависимости от того, что в нее передали - она становится либо lvalue-ссылкой, либо rvalue-ссылкой.

Реализуется компилятором это тривиально: создаются обе версии, если нужно.

На примере выше можно передавать в g(T&&) любой тип, и он прикастуется к ссылке определенного типа. Но есть подвох: как ни крути тип ссылки в рантайме мы все-таки не знаем, а в вызовах f(A&&) из g(T&&) вообще будет присутствовать только версия, принимающая lvalue-ссылку.

Почему? Так как T&& a - именованная ссылка, значит она имеет категорию lvalue, значит тип аргумента будет (T&&)& -> T&, либо (T&)& -> T& по правилу схлопывания ссылок. Для того, чтобы сохранять информацию о типе ссылки на уровне компиляции, придумали std::forward.

std::forward

Использование Perfect forwarding позволяет сохранять тип ссылки на уровне компиляции.


void bar(int& v) {
    std::cout << "lvalue";
}

void bar(int&& v) {
    std::cout << "rvalue";
}

template <typename T>
void foo(T&& v) {
    bar(v);
}

template <typename T>
void foo2(T&& v) {
    bar(std::forward<T>(v));
}

int a = 42;
foo(a);  // out: lvalue
foo(42); // out: lvalue

foo2(a);  // out: lvalue
foo2(42); // out: rvalue

Часто std::forward применяется вместе с variadic templates.

template<typename... Args>
void f(Args&&... args) {
    g(std::forward<Args>(args)...);
}