Khi nào nên sử dụng std :: Forward để chuyển tiếp đối số?


155

C ++ 0x cho thấy một ví dụ về việc sử dụng std::forward:

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

Khi nào thì thuận lợi để sử dụng std::forward, luôn luôn?

Ngoài ra, nó yêu cầu sử dụng &&trong khai báo tham số, nó có hợp lệ trong mọi trường hợp không? Tôi nghĩ rằng bạn phải chuyển tạm thời cho một hàm nếu hàm được khai báo &&trong đó, vì vậy foo có thể được gọi với bất kỳ tham số nào không?

Cuối cùng, nếu tôi có một chức năng gọi như thế này:

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

Tôi có nên sử dụng cái này thay thế:

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

Ngoài ra, nếu sử dụng các tham số hai lần trong hàm, tức là chuyển tiếp đến hai hàm cùng một lúc, có nên sử dụng std::forwardkhông? Không std::forwardchuyển đổi điều tương tự thành tạm thời hai lần, di chuyển bộ nhớ và làm cho nó không hợp lệ cho lần sử dụng thứ hai? Các mã sau sẽ ổn chứ:

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

Tôi có một chút bối rối std::forward, và tôi sẵn sàng sử dụng một số thanh toán bù trừ.

Câu trả lời:


124

Sử dụng nó như ví dụ đầu tiên của bạn:

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

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

Đó là do các quy tắc thu gọn tham chiếu : Nếu T = U&, sau đó T&& = U&, nhưng nếu T = U&&, sau đó T&& = U&&, vì vậy bạn luôn kết thúc với loại chính xác bên trong thân hàm. Cuối cùng, bạn cần forwardbiến lvalue-Turn x(vì bây giờ nó đã có tên!) Trở thành tham chiếu giá trị nếu nó là một tham chiếu ban đầu.

Tuy nhiên, bạn không nên chuyển tiếp một cái gì đó nhiều hơn một lần, bởi vì điều đó thường không có ý nghĩa: Chuyển tiếp có nghĩa là bạn có khả năng chuyển cuộc tranh cãi đến người gọi cuối cùng và sau khi nó chuyển đi, vì vậy bạn không thể sử dụng nó một lần nữa (theo cách bạn có thể có nghĩa).


Tôi nghĩ đó là Args...&& args?
Cún con

5
@DeadMG: Luôn luôn là cái đúng, không phải là cái tôi đã đánh giá sai :-) ... mặc dù trong trường hợp này tôi dường như đã đánh giá sai nó một cách chính xác!
Kerrek SB

1
Nhưng làm thế nào g được khai báo cho loại T chung?
MK.

@MK. g được khai báo là hàm thông thường với các tham số bạn muốn.
CoffeDeveloper

1
@cmdLP: Bạn nói đúng là nó được xác định rõ để chuyển tiếp nhiều lần, nhưng hiếm khi chính xác về mặt ngữ nghĩa cho chương trình của bạn. Mặc dù vậy, việc đưa các thành viên của một biểu thức chuyển tiếp là một trường hợp hữu ích. Tôi sẽ cập nhật câu trả lời.
Kerrek SB

4

Câu trả lời của Kerrek rất hữu ích, nhưng nó không hoàn toàn trả lời câu hỏi từ tiêu đề:

Khi nào nên sử dụng std :: Forward để chuyển tiếp đối số?

Để trả lời nó, trước tiên chúng ta nên đưa ra một khái niệm về các tài liệu tham khảo phổ quát . Scott Meyers đã đặt tên này và ngày nay chúng thường được gọi là tài liệu tham khảo chuyển tiếp. Về cơ bản, khi bạn thấy một cái gì đó như thế này:

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

lưu ý rằng đó paramkhông phải là một tài liệu tham khảo giá trị (vì người ta có thể muốn kết luận), mà là một tài liệu tham khảo phổ quát *. Tài liệu tham khảo phổ quát được đặc trưng bởi một hình thức rất hạn chế (chỉ T&&, không có const hoặc vòng loại tương tự) và theo loại trừ - loại Tsẽ được suy ra khi fđược gọi. Tóm lại, các tham chiếu phổ quát tương ứng với các tham chiếu giá trị nếu chúng được khởi tạo với các giá trị và tham chiếu giá trị nếu chúng được khởi tạo với các giá trị.

Bây giờ thật dễ dàng để trả lời câu hỏi ban đầu - áp dụng std::forwardcho:

  • một tham chiếu phổ quát lần cuối cùng nó được sử dụng trong hàm
  • một tham chiếu phổ quát được trả về từ các hàm trả về theo giá trị

Một ví dụ cho trường hợp đầu tiên:

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
}

Trong đoạn mã trên, chúng tôi không muốn propcó một số giá trị không xác định sau khi other.set(..)kết thúc, vì vậy không có chuyển tiếp nào xảy ra ở đây. Tuy nhiên, khi gọi barchúng tôi chuyển tiếp propkhi chúng tôi hoàn thành nó và barcó thể làm bất cứ điều gì nó muốn với nó (ví dụ: di chuyển nó).

Một ví dụ cho trường hợp thứ hai:

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

Mẫu hàm này sẽ di chuyển propvào giá trị trả về nếu đó là giá trị và sao chép nó nếu đó là giá trị. Trong trường hợp std::forwardcuối cùng chúng tôi bỏ qua , chúng tôi sẽ luôn tạo một bản sao, đắt hơn khi proptình cờ là một giá trị.

* để hoàn toàn chính xác, một tham chiếu phổ quát là một khái niệm lấy tham chiếu giá trị cho tham số mẫu không đủ tiêu chuẩn cv.


0

Liệu ví dụ này có giúp được gì không? Tôi đã vật lộn để tìm một ví dụ không chung chung hữu ích của std :: Forward, nhưng đánh vào một ví dụ về tài khoản ngân hàng mà chúng tôi chuyển qua tiền mặt để gửi làm đối số.

Vì vậy, nếu chúng ta có một phiên bản const của một tài khoản, chúng ta nên mong đợi khi chúng ta chuyển nó vào mẫu tiền gửi của mình <> rằng hàm const được gọi; và điều này sau đó ném ra một ngoại lệ (ý tưởng cho rằng đây là một tài khoản bị khóa!)

Nếu chúng tôi có tài khoản không const thì chúng tôi sẽ có thể sửa đổi tài khoản.

#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;
    }
}

Để xây dựng:

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

Sản lượng dự kiến:

# 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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.