Когда использовать std :: forward для пересылки аргументов?

155

C ++ 0x показывает пример использования std::forward:

template<class T>
void foo(T&& arg) 
{
  bar(std::forward<T>(arg));
}

Когда выгодно использовать std::forwardвсегда?

Кроме того, он требует использования &&в декларации параметров, действительно ли это во всех случаях? Я думал, что вам нужно передать временные функции в функцию, если она была объявлена &&в ней, так может ли foo вызываться с любым параметром?

Наконец, если у меня есть вызов функции, такой как этот:

template<int val, typename... Params>
void doSomething(Params... args) {
  doSomethingElse<val, Params...>(args...);
}

Должен ли я использовать это вместо:

template<int val, typename... Params>
void doSomething(Params&&... args) {
  doSomethingElse<val, Params...>(std::forward<Params>(args)...);
}

Кроме того, целесообразно ли использовать параметры в функции дважды, т. Е. Перенаправлять их одновременно двум функциям std::forward? Не std::forwardпреобразует ли одно и то же во временное дважды, перемещая память и делая ее недействительной для повторного использования? Будет ли следующий код в порядке:

template<int val, typename... Params>
void doSomething(Params&&... args) {
  doSomethingElse<val, Params...>(std::forward<Params>(args)...);
  doSomethingWeird<val, Params...>(std::forward<Params>(args)...);
}

Я немного сбит с толку std::forward, и я бы с удовольствием воспользовался некоторыми разъяснениями.

coyotte508
источник

Ответы:

124

Используйте это как ваш первый пример:

template <typename T> void f(T && x)
{
  g(std::forward<T>(x));
}

template <typename ...Args> void f(Args && ...args)
{
  g(std::forward<Args>(args)...);
}

Это из-за правил свертывания ссылок : если T = U&, то T&& = U&, но если T = U&&, тогда T&& = U&&, так что вы всегда получаете правильный тип внутри тела функции. Наконец, вам нужно forwardпревратить lvalue-turn x(потому что у него теперь есть имя!) Обратно в ссылку на rvalue, если она была изначально.

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

Керрек С.Б.
источник
Я думал, что это было Args...&& args?
Щенок
5
@DeadMG: Это всегда тот, который правильно, а не тот, который я неправильно запомнил :-) ... хотя в этом случае я, кажется, неправильно запомнил это!
Керрек С.Б.
1
Но как объявляется g для универсального типа T?
МК.
@MK. g объявлена ​​как обычная функция с параметрами, которые вы хотите.
CoffeDeveloper
1
@cmdLP: Вы правы в том, что он хорошо определен для многократной пересылки, но редко бывает семантически правильным для вашей программы. Однако, взятие членов выражений вперед полезно. Я обновлю ответ.
Kerrek SB
4

Ответ Керрека очень полезен, но он не полностью отвечает на вопрос из заголовка:

Когда использовать std :: forward для пересылки аргументов?

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

template<typename T>
void f(T&& param);

имейте в виду, что paramэто не ссылка на значение (как можно предположить), а универсальная ссылка *. Универсальные ссылки характеризуются очень ограниченной формой (просто T&&, без const или аналогичных квалификаторов) и с помощью дедукции типа - тип Tбудет выведен при fвызове. В двух словах, универсальные ссылки соответствуют ссылкам rvalue, если они инициализируются с помощью значений rvalue, и ссылкам lvalue, если они инициализируются с помощью значений lvalue.

Теперь относительно легко ответить на исходный вопрос - обратиться std::forwardк:

  • универсальная ссылка в последний раз, когда она используется в функции
  • универсальная ссылка, возвращаемая из функций, возвращаемых по значению

Пример для первого случая:

template<typename T>
void foo(T&& prop) {
    other.set(prop); // use prop, but don't modify it because we still need it
    bar(std::forward<T>(prop)); // final use -> std::forward
}

В приведенном выше коде мы не хотим propиметь какое-то неизвестное значение после other.set(..)его завершения, поэтому пересылка здесь не происходит. Тем не менее, когда barмы звоним, мы продвигаемся, propкак мы закончили с этим, и barможем делать с ним все, что захотим (например, переместить его).

Пример для второго случая:

template<typename T>
Widget transform(T&& prop) {
   prop.transform();
   return std::forward<T>(prop);
}

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

* если быть точным, универсальная ссылка - это концепция получения rvalue-ссылки на cv-неквалифицированный параметр шаблона.

Мильен Микич
источник
0

Этот пример помогает? Я изо всех сил пытался найти полезный не универсальный пример std :: forward, но натолкнулся на пример банковского счета, на который мы передаем наличные деньги для депонирования в качестве аргумента.

Поэтому, если у нас есть постоянная версия учетной записи, мы должны ожидать, когда передадим ее в наш шаблон депозита <>, что вызывается функция const; и это затем вызывает исключение (идея заключается в том, что это была заблокированная учетная запись!)

Если у нас есть неконстантный аккаунт, мы сможем изменить его.

#include <iostream>
#include <string>
#include <sstream> // std::stringstream
#include <algorithm> // std::move
#include <utility>
#include <iostream>
#include <functional>

template<class T> class BankAccount {
private:
    const T no_cash {};
    T cash {};
public:
    BankAccount<T> () {
        std::cout << "default constructor " << to_string() << std::endl;
    }
    BankAccount<T> (T cash) : cash (cash) {
        std::cout << "new cash " << to_string() << std::endl;
    }
    BankAccount<T> (const BankAccount& o) {
        std::cout << "copy cash constructor called for " << o.to_string() << std::endl;
        cash = o.cash;
        std::cout << "copy cash constructor result is  " << to_string() << std::endl;
    }
    // Transfer of funds?
    BankAccount<T> (BankAccount<T>&& o) {
        std::cout << "move cash called for " << o.to_string() << std::endl;
        cash = o.cash;
        o.cash = no_cash;
        std::cout << "move cash result is  " << to_string() << std::endl;
    }
    ~BankAccount<T> () {
        std::cout << "delete account " << to_string() << std::endl;
    }
    void deposit (const T& deposit) {
        cash += deposit;
        std::cout << "deposit cash called " << to_string() << std::endl;
    }
    friend int deposit (int cash, const BankAccount<int> &&account) {
        throw std::string("tried to write to a locked (const) account");
    }
    friend int deposit (int cash, const BankAccount<int> &account) {
        throw std::string("tried to write to a locked (const) account");
    }
    friend int deposit (int cash, BankAccount<int> &account) {
        account.deposit(cash);
        return account.cash;
    }
    friend std::ostream& operator<<(std::ostream &os, const BankAccount<T>& o) {
        os << "$" << std::to_string(o.cash);
        return os;
    }
    std::string to_string (void) const {
        auto address = static_cast<const void*>(this);
        std::stringstream ss;
        ss << address;
        return "BankAccount(" + ss.str() + ", cash $" + std::to_string(cash) + ")";
    }
};

template<typename T, typename Account>
int process_deposit(T cash, Account&& b) {
    return deposit(cash, std::forward<Account>(b));
}

int main(int, char**)
{
    try {
        // create account1 and try to deposit into it
        auto account1 = BankAccount<int>(0);
        process_deposit<int>(100, account1);
        std::cout << account1.to_string() << std::endl;
        std::cout << "SUCCESS: account1 deposit succeeded!" << std::endl;
    } catch (const std::string &e) {
        std::cerr << "FAILED: account1 deposit failed!: " << e << std::endl;
    }

    try {
        // create locked account2 and try to deposit into it; this should fail
        const auto account2 = BankAccount<int>(0);
        process_deposit<int>(100, account2);
        std::cout << account2.to_string() << std::endl;
        std::cout << "SUCCESS: account2 deposit succeeded!" << std::endl;
    } catch (const std::string &e) {
        std::cerr << "FAILED: account2 deposit failed!: " << e << std::endl;
    }

    try {
        // create locked account3 and try to deposit into it; this should fail
        auto account3 = BankAccount<int>(0);
        process_deposit<int>(100, std::move(account3));
        std::cout << account3.to_string() << std::endl;
        std::cout << "SUCCESS: account3 deposit succeeded!" << std::endl;
    } catch (const std::string &e) {
        std::cerr << "FAILED: account3 deposit failed!: " << e << std::endl;
    }
}

Строить:

cd std_forward
rm -f *.o example
c++ -std=c++2a -Werror -g -ggdb3 -Wall -c -o main.o main.cpp
c++ main.o  -o example
./example

Ожидаемый результат:

# create account1 and try to deposit into it
new cash BankAccount(0x7ffee68d96b0, cash $0)
deposit cash called BankAccount(0x7ffee68d96b0, cash $100)
BankAccount(0x7ffee68d96b0, cash $100)
# SUCCESS: account1 deposit succeeded!
delete account BankAccount(0x7ffee68d96b0, cash $100)

# create locked account2 and try to deposit into it; this should fail
new cash BankAccount(0x7ffee68d9670, cash $0)
delete account BankAccount(0x7ffee68d9670, cash $0)
# FAILED: account2 deposit failed!: tried to write to a locked (const) account

# create locked account3 and try to deposit into it; this should fail
new cash BankAccount(0x7ffee68d9630, cash $0)
delete account BankAccount(0x7ffee68d9630, cash $0)
# FAILED: account3 deposit failed!: tried to write to a locked (const) account
Нил МакГилл
источник