Прямая передача (англ. Perfect Forwarding) — идиоматический механизм передачи параметров-ссылок из одной функции в другую с сохранением константности (временная/обычная/константная) в шаблонах языка C++. Он был стандартизирован в редакции стандарта C++11 с помощью функциональности библиотеки STL и синтаксиса передаваемыx ссылок (англ. forwarding references), а также унифицирован для применения совместно с вариативными шаблонами[1][2].
Прямая передача используется в тех случаях, когда от функций и процедур обобщённого кода требуется оставлять неизменными фундаментальные свойства своих параметризованных аргументов, то есть[1]:
- константный объект должен передаваться по константной ссылке,
- модифицируемый объект — по обычной,
- временный — по временной ссылке.
Практическое воплощение прямой передачи в стандарте языка реализовано с помощью функции std::forward
из заголовочного файла <utility>
[3][4], вследствие чего комбинация специальных правил вывода для &&
-ссылок и их свёртки позволяет создать функциональный шаблон, который принимает произвольные аргументы с фиксацией их типов и основных свойств (rvalue или lvalue). Сохранение этой информации предопределяет возможность передавать данные аргументы при вызове других функций и методов[5].
Предпосылки
правитьОсобое поведение параметров — временных ссылок
правитьРассмотрим простейший объект с двумя конструкторами — один копирует поле из std::string, второй перемещает.
class Obj {
public:
Obj(const std::string& x) : field(x) {}
Obj(std::string&& x) : field(std::move(x)) {} // std::move нужен!!
private:
std::string field;
}
Первая перегрузка конструктора — самая обычная из Си++03. А во второй std::move, и вот почему.
Параметр string&&
снаружи — временная (rvalue) ссылка, и передача именованного (lvalue) объекта невозможна. А внутри функции этот параметр именованный (lvalue), то есть string&
. Сделано это для безопасности: если в функции, принимающей string&&
, идут сложные манипуляции с данными, потерять в производительности, не перенеся string&
, предпочтительнее, чем преждевременно опустошить string&&
.
Вопросы начинаются, когда параметров много — приходится делать 4, 8, 16… конструкторов.
class Obj2 {
public:
Obj2(const std::string& x1, const std::string& x2) : field1(x1), field2(x2) {}
Obj2(const std::string& x1, std::string&& x2) : field1(x1), field2(std::move(x2)) {}
// …и ещё две перегрузки
private:
std::string field1, field2;
}
Существуют два способа не множить сущности: идиома «by-value+move» и метапрограммирование. Чтобы метапрограммирование работало, в Си++ добавлена…
Склейка ссылок
правитьСклейку (свёртку, коллапсирование) ссылок (англ. reference collapsing) лучше всего объяснит такой код.
using One = int&&;
using Two = One&; // тогда Two = int&
При переходе к передаваемым ссылкам выясняется не только тип переданного в функцию параметра, но также даётся оценка, является ли он rvalue или lvalue. Если переданный в функцию параметр является lvalue, то подставляемое значение тоже будет ссылкой на lvalue. При этом, отмечается, что объявление типа параметра шаблона в виде &&
-ссылки может иметь интересные побочные эффекты. Например, проявляется необходимость явного указания инициализаторов для всех локальных переменных данного типа, так как при их использовании с lvalue-параметрами вывод типа после инстанцирования шаблона присвоит им значение lvalue-ссылки, которая по требованию языка обязана иметь инициализатор[6].
Склейка ссылок позволяет использовать такие шаблоны:
class Obj {
public:
template <class T>
Obj(T&& x) : field(std::forward<T>(x)) {} // забежали вперёд и сделали правильно
private: // ниже объясним, почему без явной функции forward нельзя
std::string field;
}
Для таких временных ссылок в компиляторах добавлены специальные правила[7], из-за чего…
- если T=string, будет
Obj(string&&)
- если T=string&, будет
Obj(string&)
- если T=const string&, будет
Obj(const string&)
Следствие: невозможно автоматически узнать, временная ли ссылка
правитьВернёмся к шаблонному конструктору Obj::Obj. Если не рассматривать посторонние типы, а только string, возможны три варианта.
- T=string, инстанцируется в
Obj(string&&)
, внутри x=string&. - T=string&, инстанцируется в
Obj(string&)
, внутри x=string&. - T=const string&, инстанцируется в
Obj(const string&)
, внутри x=const string&.
С третьим вариантом всё в порядке, но простым выведением типов невозможно отличить первый вариант от второго. А ведь в первом варианте для максимальной производительности нужен std::move, во втором он опасен: присваивание с перемещением опустошит строку, которая, возможно, ещё пригодится.
Решение: std::forward
правитьВернёмся к нашему шаблонному конструктору.
template <class T>
Obj(T&& x) : field(std::forward<T>(x)) {}
Шаблон std::forward
используется только в шаблонах (в нешаблонном коде хватает std::move
). Он требует, чтобы тип был явно указан (иначе не отличишь Obj(string&&)
от Obj(string&)
), и либо ничего не делает, либо разворачивается в std::move
.
Идиома «by-value + move»
правитьВторой способ не множить сущности: параметр принимается по значению и передаётся дальше через std::move
.
class Obj {
public:
Obj(std::string x) : field(std::move(x)) {}
private:
std::string field;
}
Используется для небольших легко перемещаемых объектов, обычно в нешаблонном коде.
Примечания
править- ↑ 1 2 Вандевурд, 2018, 6.1 Прямая передача, с. 125.
- ↑ Horton, 2014, Perfect Forwarding, p. 373.
- ↑ std::forward Архивная копия от 19 января 2019 на Wayback Machine C++ reference
- ↑ Вандевурд, 2018, 15.6.3 Прямая передача, с. 333.
- ↑ Вандевурд, 2018, 15.6.3 Прямая передача, с. 332.
- ↑ Вандевурд, 2018, 15.6.2 Передаваемые ссылки, с. 331.
- ↑ Вандевурд, 2018, 6.1 Прямая передача, с. 127.
Источники
править- Д. Вандевурд, Н. Джосаттис, Д. Грегор. Шаблоны C++. Справочник разработчика = C++ Templates. The Complete Guide. — 2-е. — СПб. : «Альфа-книга», 2018. — 848 с. — ISBN 978-5-9500296-8-4.
- I. Horton. Ivor Horton’s Beginning Visual C++ ® 2013. — John Wiley & Sons, Inc., 2014. — ISBN 978-1-118-84577-6.
Ссылки
править- Eli Bendersky Perfect forwarding and universal references in C++, 03 November 2014