ความแตกต่างระหว่าง std :: reference_wrapper และ simple pointer?


104

ทำไมถึงต้องมีstd::reference_wrapper? ควรใช้ที่ไหน? ต่างจากพอยน์เตอร์ธรรมดาอย่างไร? ประสิทธิภาพของมันเปรียบเทียบกับตัวชี้ธรรมดาอย่างไร?


4
โดยทั่วไปแล้วจะเป็นตัวชี้ที่คุณใช้.แทน->
MM

5
@MM ไม่การใช้.ไม่ได้ผลตามที่คุณแนะนำ (เว้นแต่ในบางจุดจะมีการนำข้อเสนอจุดตัวดำเนินการมาใช้และรวมเข้าด้วยกัน :))
Columbo

3
เป็นคำถามเช่นนี้ที่ทำให้ฉันไม่มีความสุขเมื่อต้องทำงานกับ C ++ ใหม่
Nils

ในการติดตาม Columbo std :: reference_wrapper จะใช้กับget()ฟังก์ชันสมาชิกหรือด้วยการแปลงโดยนัยกลับไปเป็นประเภทพื้นฐาน
Max Barraclough

คำตอบ:


89

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

พิจารณาอัลกอริทึมใน STL ซึ่งคัดลอก functors: คุณสามารถหลีกเลี่ยงการคัดลอกนั้นได้โดยเพียงแค่ส่งกระดาษห่ออ้างอิงที่อ้างอิงถึง functor แทน functor เอง:

unsigned arr[10];
std::mt19937 myEngine;
std::generate_n( arr, 10, std::ref(myEngine) ); // Modifies myEngine's state

ได้ผลเพราะ ...

  • reference_wrappers โอเวอร์โหลดoperator()ดังนั้นจึงสามารถเรียกได้เช่นเดียวกับอ็อบเจ็กต์ฟังก์ชันที่อ้างถึง:

    std::ref(myEngine)() // Valid expression, modifies myEngines state
    
  • … (un) เหมือนกับการอ้างอิงทั่วไปการคัดลอก (และการกำหนด) reference_wrappersเพียงแค่กำหนดผู้ชี้

    int i, j;
    auto r = std::ref(i); // r refers to i
    r = std::ref(j); // Okay; r refers to j
    r = std::cref(j); // Error: Cannot bind reference_wrapper<int> to <const int>
    

การคัดลอก Wrapper อ้างอิงนั้นเทียบเท่ากับการคัดลอกตัวชี้ซึ่งมีราคาถูกพอ ๆ กับที่ได้รับ ฟังก์ชันทั้งหมดที่เรียกใช้โดยธรรมชาติในการใช้งาน (เช่นฟังก์ชัน to operator()) ควรอยู่ในแนวเดียวเนื่องจากเป็นแบบเส้นเดียว

reference_wrappers ถูกสร้างขึ้นผ่านstd::refและstd::cref :

int i;
auto r = std::ref(i); // r is of type std::reference_wrapper<int>
auto r2 = std::cref(i); // r is of type std::reference_wrapper<const int>

อาร์กิวเมนต์แม่แบบระบุประเภทและคุณสมบัติ CV ของวัตถุที่อ้างถึง r2อ้างถึงconst intและจะให้การอ้างอิงconst intเท่านั้น การเรียกใช้ wrapper ที่มีconstfunctors อยู่ในนั้นจะเรียกเฉพาะconstฟังก์ชัน member operator()s

Rvalue initializers ไม่ได้รับอนุญาตเนื่องจากการอนุญาตให้ทำอันตรายมากกว่าผลดี เนื่องจากค่า rvalues ​​จะถูกย้ายไป (และด้วยการรับประกันสำเนาถึงแม้ว่าจะหลีกเลี่ยงได้บางส่วนก็ตาม) เราจึงไม่ได้ปรับปรุงความหมาย เราสามารถแนะนำพอยน์เตอร์ห้อยได้เนื่องจากกระดาษห่อหุ้มอ้างอิงไม่ได้ยืดอายุการใช้งานของพอยน์ตี้

ปฏิสัมพันธ์ของห้องสมุด

ดังที่ได้กล่าวไว้ก่อนหน้านี้เราสามารถสั่งmake_tupleให้จัดเก็บข้อมูลอ้างอิงในผลลัพธ์tupleโดยส่งผ่านอาร์กิวเมนต์ที่เกี่ยวข้องผ่านreference_wrapper:

int i;
auto t1 = std::make_tuple(i); // Copies i. Type of t1 is tuple<int>
auto t2 = std::make_tuple(std::ref(i)); // Saves a reference to i.
                                        // Type of t2 is tuple<int&>

โปรดทราบว่าสิ่งนี้แตกต่างเล็กน้อยforward_as_tuple: ที่นี่ไม่อนุญาตให้ใช้ rvalues ​​เป็นอาร์กิวเมนต์

std::bindแสดงพฤติกรรมเดียวกัน: จะไม่คัดลอกอาร์กิวเมนต์ แต่เก็บข้อมูลอ้างอิงหากเป็นไฟล์reference_wrapper. มีประโยชน์หากไม่จำเป็นต้องคัดลอกอาร์กิวเมนต์นั้น (หรือ functor!) แต่อยู่ในขอบเขตขณะที่ใช้bind-functor

ความแตกต่างจากพอยน์เตอร์ธรรมดา

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

    int i;
    int& ref = std::ref(i); // Okay
    
  • reference_wrappers ไม่เหมือนพอยน์เตอร์ไม่มีสถานะว่าง พวกเขาจะต้องเริ่มต้นด้วยการอ้างอิงหรืออื่นreference_wrapper

    std::reference_wrapper<int> r; // Invalid
    
  • ความคล้ายคลึงกันคือความหมายของสำเนาตื้น: ตัวชี้และreference_wrappers สามารถกำหนดใหม่ได้


คือstd::make_tuple(std::ref(i));ดีกว่าstd::make_tuple(&i);ในบางวิธี?
Laurynas Lazauskas

6
@LaurynasLazauskas มันต่างกัน ส่วนหลังที่คุณแสดงจะบันทึกตัวชี้ไว้iไม่ใช่การอ้างอิงถึง
Columbo

อืม ... ฉันเดาว่าฉันยังไม่สามารถแยกความแตกต่างของสองคนนี้ได้ดีเท่าที่ฉันต้องการ ... อืมขอบคุณ
Laurynas Lazauskas

@Columbo อาร์เรย์ของการอ้างอิงที่เป็นไปได้อย่างไรหากไม่มีสถานะว่าง อาร์เรย์มักจะไม่เริ่มต้นด้วยองค์ประกอบทั้งหมดที่ตั้งค่าเป็นสถานะว่าง
anatolyg

2
@anatolyg อะไรขัดขวางไม่ให้คุณเริ่มต้นอาร์เรย์นั้น
Columbo

27

อย่างน้อยมีจุดประสงค์ที่สร้างแรงบันดาลใจสองประการstd::reference_wrapper<T>คือ:

  1. เป็นการให้ความหมายอ้างอิงกับวัตถุที่ส่งผ่านเป็นพารามิเตอร์ค่าไปยังเทมเพลตฟังก์ชัน ตัวอย่างเช่นคุณอาจมีอ็อบเจ็กต์ฟังก์ชันขนาดใหญ่ที่คุณต้องการส่งผ่านstd::for_each()ซึ่งใช้พารามิเตอร์อ็อบเจ็กต์ฟังก์ชันตามค่า เพื่อหลีกเลี่ยงการคัดลอกวัตถุคุณสามารถใช้

    std::for_each(begin, end, std::ref(fun));
    

    การส่งผ่านอาร์กิวเมนต์std::reference_wrapper<T>ไปยังstd::bind()นิพจน์เป็นเรื่องปกติที่จะผูกอาร์กิวเมนต์โดยการอ้างอิงมากกว่าตามค่า

  2. เมื่อใช้std::reference_wrapper<T>กับstd::make_tuple()องค์ประกอบทูเพิลที่เกี่ยวข้องจะกลายเป็นT&แทนที่จะเป็นT:

    T object;
    f(std::make_tuple(1, std::ref(object)));
    

คุณช่วยยกตัวอย่างโค้ดสำหรับกรณีแรกได้ไหม
user1708860

1
@ user1708860: คุณหมายถึงคนอื่นที่ให้มา ... ?
Dietmar Kühl

ฉันหมายถึงรหัสจริงที่มาพร้อมกับ std :: ref (fun) เพราะฉันไม่เข้าใจวิธีการใช้งาน (เว้นแต่ว่า fun เป็นวัตถุและไม่ใช่ฟังก์ชัน ... )
user1708860

2
@ user1708860: ใช่ส่วนใหญ่funเป็นวัตถุฟังก์ชัน (เช่นวัตถุของคลาสที่มีตัวดำเนินการเรียกฟังก์ชัน) และไม่ใช่ฟังก์ชัน: หากfunเป็นฟังก์ชันจริงstd::ref(fun)ไม่มีจุดประสงค์และอาจทำให้โค้ดช้าลง
Dietmar Kühl

23

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

vector<int*> a;                    // the int values might or might not be owned
vector<unique_ptr<int>> b;         // the int values are definitely owned
vector<reference_wrapper<int>> c;  // the int values are definitely not owned

3
เว้นแต่จะเป็นโค้ดก่อน c ++ 11 ตัวอย่างแรกควรบ่งบอกถึงค่าที่เป็นทางเลือกและไม่ได้เป็นเจ้าของตัวอย่างเช่นสำหรับการค้นหาแคชตามดัชนี คงจะดีไม่น้อยหาก std จัดเตรียมสิ่งที่เป็นมาตรฐานให้กับเราเพื่อแสดงถึงค่าที่ไม่เป็นค่าว่างและเป็นเจ้าของ (ตัวแปรที่ไม่ซ้ำกันและใช้ร่วมกัน)
Bwmat

มันอาจจะไม่สำคัญเท่าใน C ++ 11 ที่ซึ่งพอยน์เตอร์เปล่ามักจะเป็นค่าที่ยืมมา
Elling

reference_wrapperดีกว่าตัวชี้ดิบไม่เพียงเพราะชัดเจนว่าไม่ใช่ของเจ้าของ แต่ยังเป็นเพราะไม่สามารถเป็นได้nullptr(หากไม่มีเชนานิแกน) ด้วยเหตุนี้ผู้ใช้จึงรู้ว่าพวกเขาไม่สามารถผ่านได้nullptr(หากไม่มีเชนานิแกน) และคุณรู้ว่าคุณไม่จำเป็นต้อง ตรวจสอบมัน
underscore_d

20

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

std::vector<std::reference_wrapper<T>> vec; // OK - does what you want
std::vector<T&> vec2; // Nope! Will not compile

โดยพื้นฐานแล้วเป็นCopyAssignableเวอร์ชันของT&ไฟล์. เวลาที่คุณต้องการอ้างอิง แต่มันจะต้องมีการมอบหมายการใช้งานหรือการทำงานของผู้ช่วยstd::reference_wrapper<T> std::ref()หรือใช้ตัวชี้


นิสัยใจคออื่น ๆsizeof::

sizeof(std::reference_wrapper<T>) == sizeof(T*) // so 8 on a 64-bit box
sizeof(T&) == sizeof(T) // so, e.g., sizeof(vector<int>&) == 24

และการเปรียบเทียบ:

int i = 42;
assert(std::ref(i) == std::ref(i)); // ok

std::string s = "hello";
assert(std::ref(s) == std::ref(s)); // compile error

1
@LaurynasLazauskas หนึ่งสามารถเรียกวัตถุฟังก์ชันที่มีอยู่ในกระดาษห่อหุ้มได้โดยตรง ที่อธิบายไว้ในคำตอบของฉัน
Columbo

2
เนื่องจากการใช้งานอ้างอิงเป็นเพียงตัวชี้ภายในฉันจึงไม่เข้าใจว่าเหตุใด Wrapper จึงเพิ่มทิศทางหรือโทษประสิทธิภาพ
ริกา

4
ไม่ควรมีทิศทางใดมากไปกว่าการอ้างอิงง่ายๆเมื่อพูดถึงรหัสรุ่น
ริกา

3
ฉันคาดว่าคอมไพลเลอร์จะแทรกreference_wrapperโค้ดเล็กน้อยทำให้เหมือนกับโค้ดที่ใช้ตัวชี้หรือการอ้างอิง
David Stone

4
@LaurynasLazauskas: std::reference_wrapperมีการรับประกันว่าวัตถุจะไม่เป็นโมฆะ std::vector<T *>พิจารณาสมาชิกชั้นเรียน คุณต้องตรวจสอบโค้ดคลาสทั้งหมดเพื่อดูว่าอ็อบเจ็กต์นี้สามารถเก็บ a nullptrในเวกเตอร์ได้หรือไม่ในขณะที่std::reference_wrapper<T>คุณรับประกันว่ามีอ็อบเจ็กต์ที่ถูกต้อง
David Stone
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.