เมื่อใดที่จะใช้ std :: forward เพื่อส่งต่อข้อโต้แย้ง?


155

C ++ 0x แสดงตัวอย่างการใช้std::forward:

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

เมื่อใดจึงจะใช้ประโยชน์std::forwardเสมอ

นอกจากนี้ยังจำเป็นต้องใช้&&ในการประกาศพารามิเตอร์มันถูกต้องในทุกกรณี? ฉันคิดว่าคุณจะต้องส่งผ่านฟังก์ชั่นชั่วคราวหากฟังก์ชั่นนั้นถูกประกาศไว้&&ในนั้นดังนั้นฟูสามารถถูกเรียกด้วยพารามิเตอร์ใด ๆ ได้หรือไม่?

สุดท้ายถ้าฉันมีฟังก์ชั่นการโทรเช่นนี้

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: มันเป็นสิ่งที่ถูกต้องเสมอไม่ใช่ของที่ฉันพิมพ์ผิด :-) ... ถึงแม้ว่าในกรณีนี้ฉันดูเหมือนจะได้รับการแก้ไขผิดอย่างถูกต้อง!
Kerrek SB

1
แต่ g ประกาศสำหรับประเภททั่วไปอย่างไร
MK

@MK g ถูกประกาศเป็นฟังก์ชันปกติพร้อมพารามิเตอร์ที่คุณต้องการ
CoffeDeveloper

1
@cmdLP: คุณถูกต้องว่ามันถูกกำหนดเป็นอย่างดีเพื่อส่งต่อซ้ำ ๆ แต่ไม่ค่อยถูกต้องทางความหมายสำหรับโปรแกรมของคุณ การเป็นสมาชิกของการแสดงออกไปข้างหน้าเป็นกรณีที่มีประโยชน์ ฉันจะอัปเดตคำตอบ
Kerrek SB

4

คำตอบของ Kerrek นั้นมีประโยชน์มาก แต่ก็ไม่ได้ตอบคำถามทั้งหมดจากชื่อ:

เมื่อใดที่จะใช้ std :: forward เพื่อส่งต่อข้อโต้แย้ง?

เพื่อที่จะตอบว่าเราควรแนะนำความคิดของการอ้างอิงสากล สก็อตต์เมเยอร์สให้ชื่อนี้และทุกวันนี้พวกเขามักถูกเรียกว่าการอ้างอิงการส่งต่อ โดยทั่วไปเมื่อคุณเห็นสิ่งนี้:

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

จำไว้ว่าparamไม่ใช่การอ้างอิงที่คุ้มค่า (อาจถูกล่อลวงให้สรุป) แต่เป็นการอ้างอิงสากล * การอ้างอิงสากลนั้นมีรูปแบบที่ จำกัด มาก (เพียง แต่T&&ไม่มีตัวคั่นหรือตัวระบุที่คล้ายกัน) และโดยการลดชนิด - ประเภท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

* เพื่อความแม่นยำอย่างเต็มที่การอ้างอิงสากลเป็นแนวคิดของการอ้างอิงค่า rvalue กับพารามิเตอร์เทมเพลต cv-unqualified


0

ตัวอย่างนี้ช่วยหรือไม่ ฉันพยายามหาตัวอย่างที่มีประโยชน์ทั่วไปของ std :: ไปข้างหน้า แต่พบกับตัวอย่างของบัญชีธนาคารที่เราส่งผ่านเงินสดเพื่อนำไปฝากเป็นอาร์กิวเมนต์

ดังนั้นหากเรามีบัญชีรุ่น const เราควรคาดหวังว่าเมื่อเราส่งมันไปยังเทมเพลตการฝากเงินของเรา <> ว่าฟังก์ชัน const นั้นถูกเรียกว่า และสิ่งนี้จะทำให้เกิดข้อยกเว้น (ความคิดที่ว่านี่คือบัญชีที่ถูกล็อค!)

หากเรามีบัญชีที่ไม่ใช่ 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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.