อะไรคือวัตถุประสงค์หลักของการใช้ std :: forward และปัญหาอะไรที่แก้ได้?


438

ในการส่งต่อที่สมบูรณ์แบบstd::forwardใช้เพื่อแปลงการอ้างอิง rvalue ที่ระบุชื่อt1และt2การอ้างอิง rvalue ที่ไม่มีชื่อ จุดประสงค์ของการทำเช่นนั้นคืออะไร? สิ่งนั้นจะส่งผลกระทบต่อฟังก์ชั่นที่ถูกเรียกหรือไม่innerถ้าเราปล่อยให้t1& t2เป็นค่า?

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

คำตอบ:


789

คุณต้องเข้าใจปัญหาการส่งต่อ คุณสามารถอ่านรายละเอียดปัญหาทั้งหมดแต่ฉันจะสรุป

โดยพื้นฐานแล้วเมื่อได้รับนิพจน์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);
}

ซึ่งแก้ไขปัญหาข้างต้น แต่พลิก flops ตอนนี้ไม่อนุญาตให้Eมีอาร์กิวเมนต์ที่ไม่ใช่ const:

int i = 1, j = 2, k = 3;
void E(int&, int&, int&); f(i, j, k); // oops! E cannot modify these

ความพยายามที่สามยอมรับ const อ้างอิง แต่แล้วconst_cast's 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พร้อมชุดค่าผสมทั้งหมดและไม่ใช่ค่าคงที่:

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);

ไม่มีข้อโต้แย้งต้องมี 2 ชุดNเป็นฝันร้าย เราต้องการทำสิ่งนี้โดยอัตโนมัติ

(นี่คือสิ่งที่เราจะได้รับคอมไพเลอร์อย่างมีประสิทธิภาพสำหรับเราใน C ++ 11)


ใน C ++ 11 เราได้รับโอกาสแก้ไข วิธีการหนึ่งแก้ไขกฎการหักเงินของเทมเพลตสำหรับประเภทที่มีอยู่ แต่สิ่งนี้อาจทำลายรหัสได้มาก ดังนั้นเราต้องหาวิธีอื่น

การแก้ปัญหาคือแทนที่จะใช้ที่เพิ่มใหม่rvalue อ้างอิง ; เราสามารถนำกฎใหม่มาใช้เมื่อลดประเภทการอ้างอิงค่า rvalue และสร้างผลลัพธ์ที่ต้องการ ท้ายที่สุดเราไม่สามารถทำลายรหัสได้ในขณะนี้

หากได้รับการอ้างอิงถึงการอ้างอิง (การอ้างอิงโน้ตเป็นคำที่ครอบคลุมทั้งความหมายT&และT&&) เราจะใช้กฎต่อไปนี้เพื่อค้นหาประเภทผลลัพธ์:

"[รับ] ประเภท TR ที่อ้างอิงถึงประเภท T ความพยายามในการสร้างประเภท“ อ้างอิง lvalue กับ cv TR” สร้างประเภท“ อ้างอิง lvalue กับ T” ในขณะที่ความพยายามในการสร้างประเภท“ อ้างอิง rvalue เพื่อ 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รับ lvalue เมื่อfรับค่า rvalue Eจะได้ค่า rvalue สมบูรณ์


และแน่นอนเราต้องการกำจัดความน่าเกลียด static_cast<T&&>เป็นความลับและแปลกที่จะจำ; ลองสร้างฟังก์ชั่นยูทิลิตี้ที่เรียกว่าforwardซึ่งทำสิ่งเดียวกัน:

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

1
จะไม่fเป็นหน้าที่และไม่ใช่การแสดงออกใช่มั้ย
Michael Foukarakis

1
ความพยายามครั้งสุดท้ายของคุณไม่ถูกต้องเกี่ยวกับคำแถลงปัญหา: มันจะส่งต่อค่า const เป็น non-const ดังนั้นจึงไม่ส่งต่อเลย นอกจากนี้ยังทราบว่าในความพยายามครั้งแรกที่const int iจะได้รับการยอมรับ: จะอนุมานไปA const intความล้มเหลวสำหรับตัวอักษร rvalues นอกจากนี้โปรดทราบว่าสำหรับการเรียกไปที่deduced(1)x คือint&&ไม่ใช่int(การส่งต่อที่สมบูรณ์แบบไม่เคยทำการคัดลอกตามที่ควรทำหากxจะเป็นพารามิเตอร์ค่า) เพียงแค่เป็นT intเหตุผลที่xประเมินค่าเป็น lvalue ในตัวส่งต่อคือเนื่องจากการอ้างอิง rvalue ที่มีชื่อกลายเป็นนิพจน์ lvalue
Johannes Schaub - litb

5
มีความแตกต่างในการใช้งานforwardหรือmoveที่นี่? หรือมันเป็นเพียงความแตกต่างทางความหมาย?
0x499602D2

28
@David: std::moveควรเรียกใช้โดยไม่มีข้อโต้แย้งเทมเพลตที่ชัดเจนและจะส่งผลให้เกิดค่า rvalue เสมอในขณะที่std::forwardอาจสิ้นสุดด้วยเช่นกัน ใช้std::moveเมื่อคุณรู้ว่าคุณไม่ต้องการค่าอีกต่อไปและต้องการย้ายไปที่อื่นใช้std::forwardเพื่อทำตามค่าที่ส่งผ่านไปยังเทมเพลตฟังก์ชันของคุณ
GManNickG

5
ขอขอบคุณที่เริ่มต้นด้วยตัวอย่างที่เป็นรูปธรรมก่อนและกระตุ้นให้เกิดปัญหา มีประโยชน์มาก!
ShreevatsaR

61

ฉันคิดว่าจะมีรหัสแนวคิดการใช้ std :: ไปข้างหน้าสามารถเพิ่มการสนทนา นี่คือสไลด์จาก Scott Meyers พูดคุยตัวอย่างที่มีประสิทธิภาพ C ++ 11/14

โค้ดแนวคิดการใช้ std :: forward

ฟังก์ชั่นในรหัสคือmove std::moveมีการใช้งาน (ที่ใช้งาน) ก่อนหน้านี้ในการพูดคุยนั้น ฉันพบการใช้งาน std :: forward จริงใน libstdc ++ในไฟล์ move.h แต่ไม่แนะนำเลย

จากมุมมองของผู้ใช้ความหมายของมันstd::forwardคือการส่งแบบมีเงื่อนไขไปยังค่า rvalue มันจะมีประโยชน์ถ้าฉันกำลังเขียนฟังก์ชั่นซึ่งคาดว่าทั้ง 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 น. จากจุดเริ่มต้น


2
ลิงก์ที่สองของคุณได้รับการชี้ไปที่อื่นอย่างสมบูรณ์
Pharap

34

ในการส่งต่อที่สมบูรณ์ std :: forward ถูกใช้เพื่อแปลงการอ้างอิง rvalue ที่กำหนดชื่อ t1 และ t2 เป็นการอ้างอิง rvalue ที่ไม่มีชื่อ จุดประสงค์ของการทำเช่นนั้นคืออะไร? สิ่งนั้นจะส่งผลอย่างไรต่อฟังก์ชั่นที่เรียกว่าถ้าเราปล่อยให้ 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);เป็นค่าคุณจะต้องเรียกใช้ # 1 แทน # 2 std::forwardที่ว่าทำไมเราต้องเปิดการอ้างอิงกลับเข้ามาในการอ้างอิงชื่อด้วย ดังนั้นt1ในouterคือนิพจน์ lvalue เสมอในขณะที่forward<T1>(t1)อาจเป็นนิพจน์ rvalue ขึ้นอยู่กับT1ขึ้นอยู่กับ หลังเป็นเพียงการแสดงออก lvalue ถ้าT1เป็นการอ้างอิง lvalue และT1ถูกอนุมานว่าเป็นเพียงการอ้างอิง lvalue ในกรณีที่อาร์กิวเมนต์แรกไปยังด้านนอกเป็นนิพจน์ lvalue


นี่เป็นคำอธิบายที่ใช้ง่าย แต่เป็นคำอธิบายที่ทำได้ดีและใช้งานได้ดี ผู้คนควรอ่านคำตอบนี้ก่อนแล้วค่อยลงลึกกว่านี้หากต้องการ
NicoBerrogorry

@sellibitze อีกหนึ่งคำถามที่ถูกต้องเมื่ออนุมาน int a; f (a): "เนื่องจาก a คือ lvalue ดังนั้น int (T&&) จึงเท่ากับ int (int &&&)" หรือ "เพื่อทำให้ T&& ถือเป็น int &, ดังนั้น T ควร int & "? ฉันชอบอันหลังมากกว่า
จอห์น

11

มันจะส่งผลกระทบอย่างไรต่อฟังก์ชั่นที่เรียกว่าถ้าเราปล่อยให้ t1 & t2 เป็น lvalue

หากหลังจาก instantiating T1เป็นประเภทcharและT2เป็นคลาสคุณต้องการส่งt1ต่อสำเนาและt2ต่อconstการอ้างอิง ดีเว้นแต่inner()จะใช้พวกเขาต่อไม่ใช่constการอ้างอิงนั่นคือในกรณีที่คุณต้องการเช่นกัน

พยายามเขียนชุดของouter()ฟังก์ชันที่ใช้สิ่งนี้โดยไม่มีการอ้างอิง rvalue เพื่อลดวิธีที่ถูกต้องในการส่งผ่านอาร์กิวเมนต์inner()ชนิดของ ฉันคิดว่าคุณจะต้องมีบางอย่าง 2 ^ 2 ของพวกเขาสิ่งที่เทมเพลต - เมตาเทมเพลตสวยมากที่จะสรุปข้อโต้แย้งและเวลามากที่จะได้รับสิทธินี้สำหรับทุกกรณี

จากนั้นมีบางคนมาพร้อมกับinner()ข้อโต้แย้งต่อตัวชี้ ฉันคิดว่าตอนนี้ทำให้ 3 ^ 2 (หรือ 4 ^ 2. นรกฉันไม่อยากจะลองคิดดูconstตัวชี้จะสร้างความแตกต่างหรือไม่)

จากนั้นลองจินตนาการว่าคุณต้องการทำสิ่งนี้สำหรับพารามิเตอร์ห้าตัว หรือเจ็ด

ตอนนี้คุณรู้แล้วว่าทำไมจิตใจที่สดใสบางอย่างเกิดขึ้นกับ "การส่งต่อที่สมบูรณ์แบบ": มันทำให้คอมไพเลอร์ทำทุกอย่างให้คุณ


5

จุดที่ไม่ได้ทำให้ชัดคือการ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' ต้องเป็นฟังก์ชันเทมเพลต หากเป็นเพียงการกำหนดเป็น 'โมฆะ f (int && a)' สิ่งนี้จะไม่ทำงาน


จุดที่ดีดังนั้น T&& ในการส่งแบบสแตติกก็เป็นไปตามกฎการยุบตัวอ้างอิงด้วยใช่ไหม
บาร์นีย์

3

อาจเป็นการดีที่จะเน้นว่าการส่งต่อจะต้องใช้ควบคู่กับวิธีการด้านนอกพร้อมการอ้างอิงการส่งต่อ / สากล อนุญาตให้ใช้การส่งต่อด้วยตัวเองเนื่องจากอนุญาตให้ใช้ข้อความต่อไปนี้ แต่ไม่ดีเกินกว่าจะทำให้เกิดความสับสน คณะกรรมการมาตรฐานอาจต้องการปิดใช้งานความยืดหยุ่นเช่นนั้นทำไมเราไม่ใช้ static_cast แทน?

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

ในความคิดของฉันการเคลื่อนที่ไปข้างหน้าเป็นรูปแบบการออกแบบซึ่งเป็นผลลัพธ์ที่เป็นธรรมชาติหลังจากมีการแนะนำประเภทอ้างอิง r-value เราไม่ควรตั้งชื่อวิธีที่สมมติว่ามีการใช้อย่างถูกต้องยกเว้นว่ามีการห้ามใช้งานที่ไม่ถูกต้อง


ฉันไม่คิดว่าคณะกรรมการ C ++ รู้สึกว่าความรับผิดชอบของพวกเขาคือการใช้สำนวนภาษา "ถูกต้อง" หรือแม้แต่กำหนดว่าการใช้ "ถูกต้อง" คืออะไร (แม้ว่าพวกเขาจะสามารถให้แนวทางได้) ด้วยเหตุนี้ในขณะที่ครูผู้สอนผู้บังคับบัญชาและเพื่อน ๆ อาจมีหน้าที่นำพวกเขาไปทางใดทางหนึ่งผมเชื่อว่าคณะกรรมการ C ++ (และมาตรฐาน) จึงไม่มีหน้าที่นั้น
SirGuy

ใช่ฉันเพิ่งอ่านN2951และฉันยอมรับว่าคณะกรรมการมาตรฐานไม่มีภาระผูกพันในการเพิ่มข้อ จำกัด ที่ไม่จำเป็นเกี่ยวกับการใช้งานฟังก์ชั่น แต่ชื่อของเท็มเพลตฟังก์ชั่นทั้งสองนี้ (การเลื่อนไปข้างหน้า) เป็นการสร้างความสับสนเล็กน้อยเมื่อเห็นเฉพาะคำจำกัดความของพวกเขาในไฟล์ไลบรารีหรือเอกสารมาตรฐาน (23.2.5 ตัวช่วยการส่งต่อ / ย้าย) ตัวอย่างในมาตรฐานช่วยให้เข้าใจแนวคิดอย่างแน่นอน แต่อาจมีประโยชน์ในการเพิ่มคำพูดเพิ่มเติมเพื่อทำให้สิ่งต่าง ๆ ชัดเจนยิ่งขึ้น
ลิน
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.