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



Вообще говоря, вся эта серия постов началась с просьбы нашего подписчика Сергея Нефедова объяснить зачем нужны универсальные ссылки. Дождались! 🤩



В предыдущей статье я сделал акцент:



Тип rvalue reference задаётся с помощью && перед именем класса.



ОДНО БОЛЬШОЕ НО! Вместо имени класса может быть установлен параметр-тип шаблона:

template<typename T>

void foo(T &&message)

{

...

}




Ожидается, что из него будет выведен тип rvalue reference, но это не всегда так. Такие ссылки позволяют с одной стороны определить поведения для работы с xvalue, а с другой, неожиданно, для lvalue.



В своё время Scott Meyers, придумал такой термин как универсальные ссылки, чтобы объяснить некоторые тонкости языка. Рассмотрим на примере вышеупомянутой foo:

std::string str = "blah blah blah";



// Передает lvalue

foo(str);



// Передает xvalue (rvalue reference)

foo(std::move(str));




Оба вызова функции foo будут корректны, если не брать во внимание реализацию foo. Живой пример



Универсальная ссылка (т.н. universal reference) — это переменная или параметр, которая имеет тип T&& для выведенного типа T. Из неё будет выведен тип rvalue reference, либо lvalue reference. Это так же касается auto переменных, т.к. их тип тоже выводится.



Расставляем точки над i вместе со Scott Meyers:

Widget &&var1 = someWidget;

// ~~^~~

// rvalue reference



auto &&var2 = var1;

// ~~^~~

// universal reference



template<typename T>

void f(std::vector<T> &&param);

// ~~^~~

// rvalue reference



template<typename T>

void f(T &&param);

// ~~^~~

// universal reference




В соответствии с этим маленьким нюансом поведение может меняться внутри функции foo. Банально, можно накодить тормозящее копирование вместо производительной передачи ресурса.



Я немного изменил предыдущий пример: https://compiler-explorer.com/z/EzddYhjdv. В зависимости от выведенного типа, строка будет либо скопирована, либо перемещена. Соответственно, в области видимости функции main объект либо выводит текст, либо нет (т.к. ресурс был передан другому объекту внутри foo).



Причем, это не работает, если T — параметр-тип шаблонного класса:

template<class T>

class mycontainer

{

public:

void push_back(T &&other) { ... }

~~~^~~~

rvalue reference

...

};




Пример: https://compiler-explorer.com/z/We4qzG5xG



Получается, что в универсальные ссылки заложен дуализм поведения. Зачем же так было сделано? А за тем, что существуют template parameter pack:

template<class... Ts>

void foo(Ts... args)

{

bar(args...);

}



foo(std::move(string), value);

~~~~^~~~ ~~^~~~

xvalue lvalue




Как мы видим, разные аргументы вызова foo могут относиться к разным категориям выражений.



Кстати, если не знать и не пытаться в эти тонкости, то можно вполне спокойно использовать стандартные структуры. Если говорить с натяжкой, то можно, конечно, сказать, что такая универсальность может снижать порог вхождения в C++. Не знаешь — пишешь просто рабочий код, а знаешь — пишешь ещё и эффективный.



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



Я думаю, что нам еще нужны посты на разбор этой темы, чтобы это в голове уложилось. А пока будем развивать тему в сторону move семантики. Не забываем об исключениях в перемещающем конструкторе, а так же про оптимизации RVO/NRVO.



#cppcore #memory #algorithm #hardcore