Идеальная пересылка

Идеальная пересылка и все остальное

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

Что такое Perfect Forwarding

Идеальная пересылка позволяет шаблонной функции, которая принимает набор аргументов, пересылать эти аргументы другой функции, сохраняя при этом характер lvalue или rvalue исходных аргументов функции.

Идеальная пересылка сокращает чрезмерное копирование и упрощает код за счет уменьшения необходимости писать перегрузки для обработки lvalue и rvalue по отдельности.

Примечание. Функция, на которую перенаправляются аргументы, может быть обычной функцией, другой функцией-шаблоном или конструктором.

template<typename T> 
void OuterFunction(T& param)
{ 
    InnerFunction(param); 
}

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

Обратите внимание, что внешняя функция принимает ссылку lvalue, поэтому мы можем передать ей только lvalue, следующий код не будет компилироваться.

OuterFunction(5); // Wont work trying to pass an rvalue to lvalue reference

Чтобы исправить это, мы могли бы заставить внешнюю функцию принимать ссылку на const lvalue, чтобы мы могли передавать rvalue, но тогда внутренней функции не разрешалось бы изменять свой аргумент. Вместо этого нам пришлось бы написать перегрузку внешней функции, которая обрабатывает rvalue.

template<typename T> 
void OuterFunction(T&& param)
{ 
   InnerFunction(param); 
}

Что еще более важно, что, если наша внутренняя функция должна принимать ссылку на rvalue в качестве аргумента. Из предыдущей статьи мы знаем, что ссылка rvalue на самом деле обрабатывается как lvalue, поэтому даже если наша внешняя функция принимает ссылку rvalue, когда дело доходит до передачи этого аргумента внутренней функции, компилятор будет рассматривать ее как lvalue и поэтому не допускается.

Упомянутые выше проблемы, а также многие другие проблемы, которые могут возникнуть при написании кода шаблона, - это то, что решает идеальная пересылка. Однако, чтобы по-настоящему понять идеальную пересылку, сначала необходимо понять несколько других концепций.

Вычет за тип шаблона

void OuterFunction(T&& t) 
{ 
} 
… 
OuterFunction(x); 
OuterFunction(X());

Вывод типа шаблона относится к тому, как компилятор выводит тип T, переданный в функцию шаблона, когда параметр functions имеет тип T &&. Два правила определяют способ разрешения T, который зависит от того, является ли переданный аргумент lvalue или rvalue. Правила следующие:

  1. Если аргумент, переданный в T, является lvalue, то T выводится на ссылку lvalue, X &
  2. Если аргумент, переданный T, является r-значением, то T выводится к l-значению X.

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

OuterFunction(6);

Если бы был сделан вышеуказанный вызов, компилятор определил бы тип T как int, и, учитывая, что передается rvalue, значением выражения T будет lvalue, T = int.

OuterFunction(helloWorldString);

Если бы был выполнен вышеуказанный вызов, компилятор определил бы тип T как строку и, учитывая, что передается lvalue, значение выражения T будет ссылкой на lvalue. T = строка &.

Примечание: вывод типа происходит, когда тип неизвестен и, следовательно, должен быть выведен, например, в случае OuterFunction («Здравствуйте»); не будет компилироваться, поскольку функция исключает ссылку lvalue строки типа, с которой нельзя связать rvalue «Hello».

Ссылка сворачивается

Сворачивание ссылок - это набор правил в C ++ 11 для определения значения T аргумента функции шаблона при попытке получить ссылку на ссылку, которая является чем-то недопустимым в C ++. Взятие адреса из адреса не имеет никакого смысла, но иногда это может происходить при написании шаблонов.

template<typename T> 
void func(T t) 
{ 
   T& k = t; 
} 
... 
string hello = “Hello”; 
func(hello);

В приведенном выше примере тип T выводится как int &, а затем мы пытаемся сохранить ссылку на t в форме k, что означает, что мы пытаемся сделать (int &) & k = t. Пытаюсь взять ссылку на нашу справку.

Следовательно, правила сворачивания ссылок указывают, какое значение T зависит от типа ссылки, которая происходит:

  • Принятие ссылки на ссылку lvalue приводит к тому, что ссылка на lvalue X & & становится X &
  • Принятие ссылки rvalue ссылки lvalue является ссылкой lvalue X & && становится X &
  • Принятие ссылки lvalue ссылки rvalue является ссылкой lvalue X && & становится X &
  • Принятие ссылки rvalue ссылки rvalue является ссылкой rvalue X && && становится X &&

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

Пересылка с форвардом

Функция std :: forward требуется для решения проблемы идеальной пересылки с целью разрешения этого неудобного правила, в котором ссылки на rvalue обрабатываются как lvalue (дополнительную информацию см. В последней статье).

Проблема, вызываемая этим, заключается в том, что даже при передаче rvalue функции, которая принимает ссылку rvalue, учитывая, что параметр ссылки rvalue обрабатывается как ссылка lvalue, мы не можем затем передать это в функцию, которая принимает rvalue. Мы потеряли все преимущества, которые дает нам семантика перемещения.

Здесь на помощь приходит std :: forward, поскольку он выполняет две вещи, зависящие от значения, переданного функции:

  1. Если передан аргумент, не являющийся ссылкой на lvalue, например int & val, тогда он вернет ссылку на rvalue.
  2. Если аргумент передан в ссылке lvalue, функция возвращает ссылку lvalue, она ничего не делает с аргументом.

Это означает, что передача rvalue в шаблонную функцию, которая принимает ссылку rvalue, сможет перенаправить этот аргумент как rvalue любым внутренним функциям, и если будет передано lvalue, и у нас есть перегрузка для нашей внутренней функции, которая принимает lvalue, тогда это вместо этого вызывается функция.

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

Разница между универсальной ссылкой и ссылкой rvalue заключается в том, что, хотя она обозначается с помощью «&&», она не обязательно должна быть ссылкой rvalue, иногда она может означать «&», быть ссылкой на lvalue.

Как будет показано ниже, именно так мы можем передавать lvalue и rvalue в шаблон с параметром типа T && без жалоб компилятора. В предыдущей статье говорилось о том, как функция со ссылкой на rvalue может принимать только ссылки на rvalue, и это правда, потому что в контексте нормальной функции «&&» означает ссылку на rvalue. В шаблонных функциях T && может означать T && или может означать T &.

В общем, «&&» означает универсальную ссылку только тогда, когда задействовано определение типа шаблона, чего не происходит, если мы используем нешаблонную функцию. К сожалению, с появлением нового ссылочного типа необходимо усвоить больше правил:

  • Если выражение, инициализирующее универсальную ссылку, является lvalue, универсальная ссылка становится ссылкой lvalue.
  • Если выражение, инициализирующее универсальную ссылку, является rvalue, универсальная ссылка становится ссылкой rvalue.

Собираем все вместе, идеальная пересылка

Объединение всех вышеперечисленных правил дает нам то, что называется идеальной пересылкой. Набор правил и функций в C ++, которые предоставляют нам возможность передавать значение независимо от того, является ли оно lvalue или rvalue, и сохраняют это значение при передаче в функцию, вызываемую внутри функции шаблона.

Ниже приведен простой пример идеальной пересылки, когда у объекта типа A есть свой конструктор, вызываемый в функции шаблона, при этом конструктор перемещения или конструктор копирования вызывается в зависимости от значения, переданного в функцию шаблона.

class A
{
public:
A(std::string b) : b(b) {}
// Copy Constructor
A(const A& other) : b(b)
{
b = other.b;
std::cout << "Copy Constructor" << std::endl;
}
// Move Constructor
A(A&& other)
{
b = std::move(other.b); std::cout << "Move Constructor" << std::endl;
}
private:
std::string b;
};
//And a template function
…
template<typename T>
void OuterFunction(T&& param)
{
A a(std::forward(param));
}
…
// Passing an lvalue
A a = A("Hello");
OuterFunction(a);
// Passing an rvalue
OuterFunction(A("World"));

После исследования в Интернете я не смог найти ответа, который определенно объяснил бы, как работает std :: forward, но этот пост объясняет std :: forward в контексте идеальной пересылки. По сути, std :: forward возвращает static_cast (t), когда T явно определен и t является переданным параметром.

template<typename T>
void func(T&& t) 
{
   std::forward(t);
}

Если мы позволим параметру шаблона быть определенным как универсальная ссылка T &&, тогда std :: forward будет либо передавать ссылку lvalue, либо ссылку rvalue. Данный std :: forward в основном является static_cast (param), где T явно определен, а не выводится, тогда правила свертывания ссылок определяют возвращаемое значение, где статическое приведение выглядит как static_cast ‹A & &&› (param) или static_cast ‹A && &&› (param) .

Передача Lvalue

Глядя на приведенный выше пример, мы можем увидеть, как все, что мы обсуждали, претворяется в жизнь. Передача переменной «a» в OuterFunction, которая является lvalue, означает, что тип T выводится в A &.

Далее, учитывая, что параметр теперь выглядит как A & &, поскольку правило универсальной ссылки гласит, что если передается lvalue, T && обрабатывается как ссылка lvalue. Для param мы можем посмотреть на правила сворачивания ссылок, чтобы определить, что param A &, является ссылкой lvalue.

Наконец, param передается в std :: forward, а статическое приведение std :: forward возвращает ссылку lvalue из-за правил сворачивания ссылок.

В этом случае ссылка lvalue передается в «a» и, таким образом, вызывается конструктор копирования.

Передача значения R

Передача объекту A («World») выражения rvalue в OuterFunction приводит к тому, что T выводится в A.

Далее, учитывая, что параметр теперь выглядит как A &&, где универсальная ссылка обрабатывается как ссылка rvalue, поэтому мы просто получаем A &&.

Наконец, param передается в std :: forward, а статическое приведение std :: forward возвращает ссылку rvalue из-за правил сворачивания ссылок.

В этом случае ссылка rvalue передается в «a» и, таким образом, вызывается конструктор перемещения, и мы можем воспользоваться семантикой перемещения, избегая ненужной копии переменной-члена «b».

Пошаговое руководство

Подводя итог, для безупречной работы перенаправления требуется, чтобы функция шаблона принимала параметр типа T && и использовался вывод этого типа шаблона. Кроме того, этот std :: forward должен иметь явно определенный тип.

  1. Аргумент, переданный в шаблонную функцию, сначала определяется его типом, что приводит к lvalue или ссылке lvalue. T становится T & или T
  2. Поскольку используется универсальная ссылка, если выражение, переданное функции (аргумент), является lvalue, тогда T && param становится T & param, если это rvalue, он становится T && param, ссылаясь на ссылку rvalue.
  3. На этом этапе у нас есть либо (T & && param), либо T && param, и применяются правила сворачивания ссылок, поэтому параметр функции приводит либо к ссылке lvalue, либо к ссылке rvalue.
  4. Наконец, учитывая, что ссылка rvalue фактически обрабатывается как ссылка lvalue, std :: forward требуется для приведения параметра шаблона к правильному значению. Опять же, требуются правила сворачивания ссылок, которые определяют, есть ли у нас ссылка lvalue, тогда ссылка lvalue должна быть перенаправлена, и если у нас есть ссылка rvalue, наше значение должно быть приведено к ссылке rvalue.

Резюме

По общему признанию, когда я впервые начал писать эту статью, я не совсем понимал, насколько сложной была идеальная переадресация и сколько тонких поведений C ++ требовалось, чтобы полностью понять, что происходит. В конце концов, я до сих пор не совсем понимаю std :: forward и не могу гарантировать, что написанное мной на 100% правильно, но я думаю, что это имеет смысл. В любом случае, я надеюсь, что эта и две предыдущие статьи предоставили достаточно информации, чтобы подчеркнуть необходимость и практическое использование ссылок rvalue, семантики перемещения и идеальной пересылки в C ++ 11.

Как всегда, если у вас есть вопросы, напишите мне @gamedevunboxed :)

Дополнительные ресурсы