Прямая передача (C++)

Прямая передача (англ. 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&&.

Вопросы начинаются, когда параметров много — приходится делать 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» и метапрограммирование, и для последнего сделан второй механизм Си++11.

Склейка ссылок править

Склейку (свёртку, коллапсирование) ссылок (англ. 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. 1 2 Вандевурд, 2018, 6.1 Прямая передача, с. 125.
  2. Horton, 2014, Perfect Forwarding, p. 373.
  3. std::forward Архивная копия от 19 января 2019 на Wayback Machine C++ reference
  4. Вандевурд, 2018, 15.6.3 Прямая передача, с. 333.
  5. Вандевурд, 2018, 15.6.3 Прямая передача, с. 332.
  6. Вандевурд, 2018, 15.6.2 Передаваемые ссылки, с. 331.
  7. Вандевурд, 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.

Ссылки править