Преимущества использования форварда

331

В идеальной пересылке std::forward используется для преобразования названных ссылок rvalue t1 и t2 в неназванные ссылки rvalue. Для чего это нужно? Как это повлияет на вызываемую функцию inner, если оставить t1 и t2 как lvalues?

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
Теги:
c++11
rvalue-reference
perfect-forwarding
c++-faq

6 ответов

684

Вам нужно понять проблему пересылки. Вы можете прочитать всю проблему подробнее, но я подведу итог.

В принципе, учитывая выражение E(a, b, ... , c), мы хотим, чтобы выражение f(a, b, ... , c) было эквивалентным. В С++ 03 это невозможно. Есть много попыток, но в любом случае все они терпят неудачу.


Простейшим является использование ссылки lvalue:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
    E(a, b, c);
}

Но это не позволяет обрабатывать временные значения: f(1, 2, 3);, поскольку они не могут быть привязаны к lvalue-reference.

Следующая попытка может быть следующей:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(a, b, c);
}

Что исправляет вышеуказанную проблему, но flips flops. Теперь он не позволяет E иметь неконстантные аргументы:

int i = 1, j = 2, k = 3;
void E(int&, int&, int&); f(i, j, k); // oops! E cannot modify these

Третья попытка принимает const-ссылки, но затем const_cast const прочь:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}

Это принимает все значения, может передавать все значения, но потенциально приводит к поведению undefined:

const int i = 1, j = 2, k = 3;
E(int&, int&, int&); f(i, j, k); // ouch! E can modify a const object!

Окончательное решение обрабатывает все правильно... ценой невозможности поддерживать. Вы предоставляете перегрузки f со всеми комбинациями const и non-const:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);

N аргументов требуют 2 N комбинаций, кошмар. Мы хотели бы сделать это автоматически.

(Это фактически то, что мы получаем для компилятора для нас в С++ 11.)


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

Решение состоит в том, чтобы вместо этого использовать вновь добавленные rvalue-ссылки; мы можем ввести новые правила при выводе типов rvalue-reference и создать любой желаемый результат. В конце концов, мы не можем сломать код сейчас.

Если задана ссылка на ссылку (ссылка на примечание - это охватывающий термин, означающий как T&, так и T&&), мы используем следующее правило для определения результирующего типа:

"[данный] тип TR, который является ссылкой на тип T, попытка создать тип" lvalue reference to cv TR "создает тип" lvalue reference to T ", в то время как попытка создать тип" Ссылка rvalue на cv TR "создает тип TR."

Или в табличной форме:

TR   R

T&   &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&   && -> T&  // rvalue reference to cv TR -> TR (lvalue reference to T)
T&&  &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&&  && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)

Далее, с выводом аргумента шаблона: если аргумент является lvalue A, мы приводим аргумент шаблона с ссылкой lvalue на A. В противном случае мы выводим обычно. Это дает так называемые универсальные ссылки (термин ссылка для пересылки является официальной).

Почему это полезно? Поскольку в сочетании мы сохраняем способность отслеживать категорию значений типа: если это значение lvalue, у нас есть параметр lvalue-reference, в противном случае у нас есть параметр rvalue-reference.

В коде:

template <typename T>
void deduce(T&& x); 

int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)

Последнее означает "переслать" категорию значений переменной. Имейте в виду, что когда-то внутри функции параметр может быть передан как значение l на что-либо:

void foo(int&);

template <typename T>
void deduce(T&& x)
{
    foo(x); // fine, foo can refer to x
}

deduce(1); // okay, foo operates on x which has a value of 1

Это нехорошо. E нужно получить ту же категорию ценностей, которую мы получили! Решение таково:

static_cast<T&&>(x);

Что это делает? Рассмотрим, что мы находимся внутри функции deduce, и нам передали lvalue. Это означает, что T является A&, поэтому целевой тип для статического литья A& && или просто A&. Поскольку x уже является A&, мы ничего не делаем и оставляем ссылку lvalue.

Когда нам передали rvalue, T - A, поэтому целевой тип для статического литья A&&. Листинг приводит к выражению rvalue, которое больше не может быть передано в ссылку lvalue. Мы сохранили категорию значений параметра.

Объединение этих данных дает нам "идеальную пересылку":

template <typename A>
void f(A&& a)
{
    E(static_cast<A&&>(a)); 
}

Когда f получает lvalue, E получает значение lvalue. Когда f получает rvalue, E получает значение r. Совершенная.


И, конечно, мы хотим избавиться от уродливых. static_cast<T&&> загадочно и странно запомнить; вместо этого сделайте служебную функцию с именем forward, которая делает то же самое:

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
  • 1
    Не будет ли функция f , а не выражение?
  • 0
    @Sbi: Ха-ха. :) <3 @mfukar: Это выражение вызова функции. Сам по себе f может быть чем-то «вызываемым»; функция, указатель функции или объект функции.
Показать ещё 16 комментариев
38

Я думаю, что концептуальный код, реализующий std :: forward, может добавить к обсуждению. Это слайд со слова Скотта Мейерса "Эффективный С++ 11/14 Sampler"

Изображение 1208

Функция move в коде std::move. В этом разговоре есть (рабочая) реализация. Я нашел фактическую реализацию std :: forward в libstdc++, в файле move.h, но это совсем не поучительно.

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

#include <iostream>
#include <string>
#include <utility>

void overloaded_function(std::string& param) {
  std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
  std::cout << "std::string&& version" << std::endl;
}

template<typename T>
void pass_through(T&& param) {
  overloaded_function(std::forward<T>(param));
}

int main() {
  std::string pes;
  pass_through(pes);
  pass_through(std::move(pes));
}

Конечно, он печатает

std::string& version
std::string&& version

Код основан на примере из вышеупомянутого разговора. Слайд 10, примерно в 15:00 с самого начала.

  • 1
    Ваша вторая ссылка в итоге указала куда-то совершенно другое.
16

В идеальной пересылке std:: forward используется для преобразования именованных rvalue-ссылок t1 и t2 в неназванный справочник rvalue. Для чего это нужно? Как это повлияет на вызванную функцию inner, если оставить t1 и t2 как lvalue?

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

Если вы используете ссылочную ссылку rvalue в выражении, это фактически значение lvalue (потому что вы ссылаетесь на объект по имени). Рассмотрим следующий пример:

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2

Теперь, если мы назовем outer следующим образом

outer(17,29);

мы хотели бы, чтобы 17 и 29 были перенаправлены на # 2, потому что 17 и 29 являются целыми литералами и как таковые значения r. Но так как t1 и t2 в выражении inner(t1,t2); являются lvalues, вы будете вызывать # 1 вместо # 2. Поэтому нам нужно вернуть ссылки в неназванные ссылки с помощью std::forward. Таким образом, t1 в outer всегда является выражением lvalue, а forward<T1>(t1) может быть выражением rvalue в зависимости от t1. Последний является только выражением lvalue, если t1 является ссылкой на lvalue. И t1 выводится только как ссылка lvalue в случае, если первый аргумент для внешнего является выражением lvalue.

  • 1
    Наконец, четкое объяснение :)
  • 0
    Это своего рода смягченное объяснение, но очень хорошо сделанное и функциональное объяснение. Люди должны сначала прочитать этот ответ, а затем, если хотите, глубже
11

Как это повлияет на вызываемую функцию inner, если оставить t1 и t2 как lvalue?

Если после экземпляра T1 имеет тип char, а T2 имеет класс, вы хотите передать T1 за копию и T2 за const ссылку. Ну, если inner() не принимает их за ссылку не const, то есть в этом случае вы тоже этого хотите.

Попробуйте написать набор функций outer(), которые реализуют это без ссылок rvalue, выведя правильный способ передать аргументы из типа inner(). Думаю, вам понадобится что-то 2 ^ 2 из них, довольно здоровенный шаблон-мета-материал, чтобы вывести аргументы, и много времени, чтобы получить это право для всех случаев.

И затем кто-то приходит с inner(), который принимает аргументы за указатель. Я думаю, что теперь он составляет 3 ^ 2. (Или 4 ^ 2. Черт, я не могу потрудиться, чтобы попытаться понять, имеет ли смысл указатель const.)

И тогда представьте, что вы хотите сделать это для пяти параметров. Или семь.

Теперь вы знаете, почему некоторые яркие мысли придумали "идеальную пересылку": это заставляет компилятор делать все это за вас.

2

Точка, которая не была прозрачной, - это то, что static_cast<T&&> правильно обрабатывает const T&.
Программа:

#include <iostream>

using namespace std;

void g(const int&)
{
    cout << "const int&\n";
}

void g(int&)
{
    cout << "int&\n";
}

void g(int&&)
{
    cout << "int&&\n";
}

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

int main()
{
    cout << "f(1)\n";
    f(1);
    int a = 2;
    cout << "f(a)\n";
    f(a);
    const int b = 3;
    cout << "f(const b)\n";
    f(b);
    cout << "f(a * b)\n";
    f(a * b);
}

Выдает:

f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&

Обратите внимание, что 'f' должно быть функцией шаблона. Если он определен как "void f (int & a), это не работает.

  • 0
    Хороший вопрос, поэтому T && в статическом приведении также следует правилам свертывания ссылок, верно?
1

Возможно, стоит подчеркнуть, что forward необходимо использовать в тандеме с внешним методом с пересылкой/универсальной ссылкой. Использование пересылки само по себе, поскольку допускаются следующие утверждения, но не делает ничего хорошего, кроме как путаницы. Стандартный комитет может захотеть отключить такую ​​гибкость, иначе почему бы нам просто не использовать static_cast?

     std::forward<int>(1);
     std::forward<std::string>("Hello");

По моему мнению, перемещение и форвард - это шаблоны проектирования, которые являются естественными результатами после введения ссылочного типа r-value. Мы не должны называть метод, предполагающий его правильное использование, если неправильное использование запрещено.

  • 0
    Я не думаю, что комитет по С ++ считает, что они обязаны «правильно» использовать языковые идиомы и даже не определять, что такое «правильное» использование (хотя они, безусловно, могут дать рекомендации). С этой целью, в то время как учителя, начальники и друзья человека могут быть обязаны так или иначе руководить ими, я считаю, что комитет C ++ (и, следовательно, стандарт) не имеет такой обязанности.
  • 0
    Да, я только что прочитал N2951, и я согласен, что комитет по стандартизации не обязан добавлять ненужные ограничения в отношении использования функции. Но имена этих двух шаблонов функций (перемещение и пересылка) действительно немного сбивают с толку, видя только их определения в файле библиотеки или стандартной документации (23.2.5 Помощники пересылки / перемещения). Примеры в стандарте, безусловно, помогают понять концепцию, но, возможно, было бы полезно добавить больше замечаний, чтобы сделать вещи более понятными.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню