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


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, і я б із задоволенням скористався очищенням.

Відповіді:


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-повернуту x(бо вона має ім'я зараз!) Назад в посилання rvalue, якщо воно було початковим.

Однак ви не повинні пересилати щось більше одного разу, тому що це зазвичай не має сенсу: Пересилання означає, що ви потенційно пересуваєте аргумент до остаточного абонента, і як тільки він буде переміщений, він піде, тож ви не зможете використовувати його знову (так, як ви, мабуть, мали намір).


Я думав, що це Args...&& args?
Щеня

5
@DeadMG: Це завжди той, хто є правильним, а не той, кого я згадав про помилку :-) ... хоча в цьому випадку я, здається, неправильно його зарахував!
Керрек СБ

1
Але як оголошено g для загального типу T?
МК.

@MK. g оголошується як звичайна функція з потрібними параметрами.
CoffeDeveloper

1
@cmdLP: Ви маєте рацію, що це чітко визначено для повторного переадресації, але це рідко семантично правильно для вашої програми. Прийняття членів прямих виразів є корисним випадком. Я оновлю відповідь.
Керрек СБ

4

Відповідь Керрека дуже корисна, але вона не повністю відповідає на запитання з назви:

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

Щоб відповісти на це, спершу слід ввести поняття універсальних довідок . Скотт Майєрс назвав це ім'я, і ​​сьогодні їх часто називають переадресаційними посиланнями. В основному, коли ви бачите щось подібне:

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

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

Зараз відповісти на початкове запитання відносно просто - зверніться 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 на параметр шаблону некваліфікованого cv.


0

Чи допомагає цей приклад? Я намагався знайти корисний не загальний приклад std :: вперед, але натрапив на приклад банківського рахунку, який ми передаємо готівкою для здачі на зберігання як аргумент.

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

Якщо у нас є обліковий запис non 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.