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

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

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

7 ответов

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

В основном, учитывая выражение E(a, b, ... , c)мы хотим выражение f(a, b, ... , c) быть эквивалентным. В 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-ссылке.

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

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

Что решает вышеуказанную проблему, но шлепает флопс. Теперь это не позволяет 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));
}

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

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 аргументов требуют 2N комбинаций, кошмар. Мы хотели бы сделать это автоматически.

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


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

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

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

"[задано] тип TR, который является ссылкой на тип T, попытка создать тип" lvalue ссылка на cv TR "создает тип" lvalue ссылка на T ", а попытка создать тип" rvalue ссылка на тип T " 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&&)

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

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 получает значение когда f получает значение, E получает значение. Отлично.


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

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);

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

концептуальный код, реализующий std::forward

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

С точки зрения пользователя, смысл в том, что std::forward является условным приведением к значению. Это может быть полезно, если я пишу функцию, которая ожидает 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 от начала.

В идеальной пересылке 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 являются целочисленными литералами и как таковые. Но с тех пор t1 а также t2 в выражении inner(t1,t2); являются lvalues, вы будете вызывать #1 вместо #2. Вот почему мы должны превратить ссылки обратно в неназванные ссылки с std::forward, Так, t1 в outer всегда выражение lvalue в то время как forward<T1>(t1) может быть выражением rvalue в зависимости от T1, Последнее является только выражением lvalue, если T1 является ссылкой на lvalue. А также T1 выводится как ссылка lvalue только в том случае, если первый аргумент для external был выражением lvalue.

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

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

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

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

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

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

Дело, которое не было сделано кристально ясно, заключается в том, что 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)', это не сработает.

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

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

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

From another viewpoint, when dealing with rvalues in a universal reference assignment, it may be desirable to preserve the type of a variable as it is. For example

      auto&& x = 2; // x is int&&
    
auto&& y = x; // But y is int&    
    
auto&& z = std::forward<decltype(x)>(x); // z is int&&

Using , we ensured exactly has the same type as .

Moreover, std::forward doesn't affect lvalue references:

      int i;

auto&& x = i; // x is int&

auto&& y = x; // y is int&

auto&& z = std::forward<decltype(x)>(x); // z is int&

Still has the same type as x.

So, back to your case, if the inner function has two overloads for int& and int&&, you want to pass variables like z assignment not y one.

The types in the example can be assessed via:

      std::cout<<is_same_v<int&,decltype(z)>;
std::cout<<is_same_v<int&&,decltype(z)>;

Другие вопросы по тегам