auto && บอกอะไรเรา


168

ถ้าคุณอ่านรหัสเช่น

auto&& var = foo();

ซึ่งfooเป็นฟังก์ชั่นใด ๆ Tกลับมาจากค่าของชนิด จากนั้นvarเป็น lvalue อ้างอิงประเภท rvalue Tไป แต่สิ่งนี้บ่งบอกถึงvarอะไร? หมายความว่าเราได้รับอนุญาตให้ขโมยทรัพยากรของvarหรือไม่ มีสถานการณ์ที่เหมาะสมเมื่อคุณควรใช้auto&&เพื่อบอกผู้อ่านโค้ดของคุณว่าคุณทำอย่างไรเมื่อคุณกลับมาunique_ptr<>เพื่อบอกว่าคุณมีความเป็นเจ้าของโดยเฉพาะ? และตัวอย่างเช่นT&&เมื่อTเป็นประเภทชั้นเรียน?

ฉันแค่ต้องการที่จะเข้าใจหากมีกรณีการใช้งานอื่น ๆ ที่auto&&นอกเหนือจากในการเขียนโปรแกรมแม่แบบ; เหมือนที่กล่าวไว้ในตัวอย่างในบทความนี้Universal Referencesโดย Scott Meyers


1
ฉันสงสัยในสิ่งเดียวกัน ฉันเข้าใจวิธีการหักชนิดทำงาน แต่สิ่งที่เป็นรหัสของฉันบอกว่าเมื่อฉันใช้auto&&? ฉันเคยคิดถึงว่าทำไมช่วงที่ใช้ลูปเพื่อขยายจึงใช้auto&&เป็นตัวอย่าง แต่ไม่ได้รับรอบ บางทีใครก็ตามที่คำตอบสามารถอธิบายได้
Joseph Mansfield

1
มันถูกกฎหมายหรือไม่ ฉันหมายถึง instnce ของ T ถูกทำลายทันทีหลังจากfooกลับมาเก็บค่าอ้างอิง rvalue ให้ดูเหมือนว่า UB ไปทางทิศตะวันออกเฉียงเหนือ

1
@aleguna สิ่งนี้ถูกกฎหมายอย่างสมบูรณ์ ฉันไม่ต้องการคืนค่าการอ้างอิงหรือตัวชี้ไปยังตัวแปรโลคอล แต่เป็นค่า ฟังก์ชันfooอาจมีลักษณะเช่น: int foo(){return 1;}.
MWid

9
@aleguna การอ้างอิงถึงขมับทำการขยายอายุการใช้งานเช่นเดียวกับใน C ++ 98
ecatmur

5
@aleguna ส่วนขยายอายุการใช้งานใช้งานได้เฉพาะกับขมับในท้องถิ่นไม่ใช่ฟังก์ชันที่ส่งคืนการอ้างอิง ดูstackoverflow.com/a/2784304/567292
ecatmur

คำตอบ:


232

โดยการใช้auto&& var = <initializer>ที่คุณกำลังพูดว่า: ฉันจะยอมรับการเริ่มต้นใด ๆ ไม่ว่าจะเป็น lvalue หรือ rvalue แสดงออกและเราจะรักษา constness โดยทั่วไปจะใช้สำหรับการส่งต่อ (โดยปกติจะมีT&&) ด้วยเหตุนี้การทำงานเป็นเพราะ "การอ้างอิงสากล" auto&&หรือT&&จะผูกกับอะไร

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

ยกตัวอย่างเช่นลองนึกภาพว่าคุณต้องการได้รับstd::vectorเอา iterator ไปที่องค์ประกอบแรกและแก้ไขค่าที่ iterator ชี้ไปในทางใดทางหนึ่ง:

auto&& vec = some_expression_that_may_be_rvalue_or_lvalue;
auto i = std::begin(vec);
(*i)++;

รหัสนี้จะรวบรวมได้ดีโดยไม่คำนึงถึงการแสดงออกเริ่มต้น ทางเลือกที่จะauto&&ล้มเหลวด้วยวิธีการดังต่อไปนี้:

auto         => will copy the vector, but we wanted a reference
auto&        => will only bind to modifiable lvalues
const auto&  => will bind to anything but make it const, giving us const_iterator
const auto&& => will bind only to rvalues

ดังนั้นสำหรับสิ่งนี้auto&&ทำงานได้อย่างสมบูรณ์แบบ! ตัวอย่างของการใช้auto&&สิ่งนี้อยู่ในforลูปที่อิงตามช่วง ดูคำถามอื่นของฉันสำหรับรายละเอียดเพิ่มเติม

หากคุณใช้std::forwardในauto&&การอ้างอิงของคุณเพื่อรักษาความจริงที่ว่าเดิมเป็น lvalue หรือ rvalue รหัสของคุณบอกว่า: ตอนนี้ฉันได้รับวัตถุของคุณจากนิพจน์ lvalue หรือ rvalue ทั้งสองฉันต้องการที่จะรักษาคุณค่าอันใดอันหนึ่งไว้ มีดังนั้นฉันสามารถใช้มันได้อย่างมีประสิทธิภาพมากที่สุด - นี่อาจเป็นโมฆะ ในขณะที่:

auto&& var = some_expression_that_may_be_rvalue_or_lvalue;
// var was initialized with either an lvalue or rvalue, but var itself
// is an lvalue because named rvalues are lvalues
use_it_elsewhere(std::forward<decltype(var)>(var));

สิ่งนี้ทำให้use_it_elsewhereสามารถดึงความกล้าออกมาเพื่อผลการปฏิบัติงาน (หลีกเลี่ยงการทำสำเนา) เมื่อ initializer ดั้งเดิมเป็น rvalue ที่แก้ไขได้

สิ่งนี้หมายความว่าเราสามารถหรือเมื่อเราสามารถขโมยทรัพยากรได้varหรือไม่ เนื่องจากความauto&&ต้องการจะผูกมัดกับสิ่งใดก็ตามเราไม่สามารถพยายามที่จะดึงvarความกล้าออกมาได้ - มันอาจจะเป็น lvalue หรือ const อย่างไรก็ตามเราสามารถstd::forwardใช้ฟังก์ชั่นอื่น ๆ ที่อาจทำลายภายในของมันโดยสิ้นเชิง ทันทีที่เราทำเช่นนี้เราควรพิจารณาvarให้อยู่ในสถานะที่ไม่ถูกต้อง

ทีนี้เราจะนำสิ่งนี้ไปใช้กับกรณีของauto&& var = foo();ตามที่กำหนดไว้ในคำถามของคุณโดยที่ foo คืนTค่าโดย ในกรณีนี้เราทราบว่าประเภทของจะอนุมานได้ว่าเป็นvar T&&เนื่องจากเราทราบได้อย่างแน่นอนว่ามันเป็นค่าใช้จ่ายเราจึงไม่ต้องการการstd::forwardอนุญาตให้ขโมยทรัพยากร ในกรณีเฉพาะนี้โดยรู้ว่าfooผลตอบแทนตามมูลค่าผู้อ่านควรอ่านมันเป็น: ฉันกำลังอ้างอิงค่า rvalue กับค่าที่ส่งคืนจากชั่วคราวfooดังนั้นฉันจึงสามารถย้ายจากมันได้อย่างมีความสุข


ในฐานะที่เป็นภาคผนวกฉันคิดว่ามันคุ้มค่าที่จะกล่าวถึงเมื่อนิพจน์เช่นsome_expression_that_may_be_rvalue_or_lvalueอาจเปิดขึ้นนอกเหนือจากสถานการณ์ "รหัสของคุณอาจมีการเปลี่ยนแปลง" ดังนั้นนี่คือตัวอย่างที่วางแผนไว้:

std::vector<int> global_vec{1, 2, 3, 4};

template <typename T>
T get_vector()
{
  return global_vec;
}

template <typename T>
void foo()
{
  auto&& vec = get_vector<T>();
  auto i = std::begin(vec);
  (*i)++;
  std::cout << vec[0] << std::endl;
}

นี่get_vector<T>()คือการแสดงออกที่น่ารักที่สามารถเป็นได้ทั้ง lvalue หรือ rvalue Tขึ้นอยู่กับประเภททั่วไป เราเป็นหลักเปลี่ยนประเภทการกลับมาของผ่านพารามิเตอร์แม่แบบของget_vectorfoo

เมื่อเราเรียกfoo<std::vector<int>>มันget_vectorจะส่งคืนglobal_vecค่าที่ให้นิพจน์ rvalue อีกวิธีหนึ่งคือเมื่อเราเรียกfoo<std::vector<int>&>, get_vectorจะกลับglobal_vecโดยการอ้างอิงผลในการแสดงออก lvalue

ถ้าเราทำ:

foo<std::vector<int>>();
std::cout << global_vec[0] << std::endl;
foo<std::vector<int>&>();
std::cout << global_vec[0] << std::endl;

เราได้ผลลัพธ์ต่อไปนี้ตามที่คาดไว้:

2
1
2
2

หากคุณกำลังจะเปลี่ยนauto&&ในรหัสใด ๆauto, auto&, const auto&หรือconst auto&&แล้วเราจะไม่ได้รับผลที่เราต้องการ


อีกทางเลือกหนึ่งในการเปลี่ยนตรรกะของโปรแกรมโดยอ้างอิงว่าauto&&การอ้างอิงของคุณถูกกำหนดค่าเริ่มต้นด้วยการแสดงออก lvalue หรือ rvalue คือการใช้ลักษณะของประเภท:

if (std::is_lvalue_reference<decltype(var)>::value) {
  // var was initialised with an lvalue expression
} else if (std::is_rvalue_reference<decltype(var)>::value) {
  // var was initialised with an rvalue expression
}

2
เราพูดT vec = get_vector<T>();ในฟังก์ชั่น foo ไม่ได้เหรอ? หรือฉันจะทำให้มันง่ายขึ้นถึงระดับไร้สาระ :)
Asterisk

@Asterisk ไม่สามารถมอบหมาย bcoz T vec ให้กับ lvalue ในกรณีของ std :: vector <int &> และถ้า T คือ std :: vector <int> เราจะใช้การเรียกตามค่าที่ไม่มีประสิทธิภาพ
Kapil

1
อัตโนมัติและให้ผลลัพธ์เหมือนกัน ฉันใช้ MSVC 2015 และ GCC สร้างข้อผิดพลาด
Sergey Podobry

ที่นี่ฉันใช้ MSVC 2015 auto & ให้ผลลัพธ์เหมือนกับ auto &&
Kehe CAI

ทำไมฉันถึง int; auto && j = i; อนุญาต แต่ int i; int && j = i; ไม่ใช่ ?
SeventhSon84

14

ก่อนอื่นฉันขอแนะนำให้อ่านคำตอบของฉันนี้เพื่ออ่านคำอธิบายทีละขั้นตอนเกี่ยวกับวิธีการหักล้างอาร์กิวเมนต์เทมเพลตสำหรับการอ้างอิงสากล

หมายความว่าเราได้รับอนุญาตให้ขโมยทรัพยากรของvarหรือไม่

ไม่จำเป็น. จะเกิดอะไรขึ้นหากfoo()การอ้างอิงกลับมาโดยฉับพลันหรือคุณเปลี่ยนการโทร แต่ลืมที่จะอัพเดทการใช้งานvar? หรือถ้าคุณอยู่ในรหัสทั่วไปและประเภทผลตอบแทนfoo()อาจเปลี่ยนแปลงขึ้นอยู่กับพารามิเตอร์ของคุณ?

คิดว่ามันauto&&จะเหมือนกับT&&ในtemplate<class T> void f(T&& v);เพราะ (เกือบ ) อย่างนั้น คุณทำอะไรกับการอ้างอิงสากลในฟังก์ชั่นเมื่อคุณต้องการส่งต่อหรือใช้พวกเขาในทางใดทางหนึ่ง? คุณใช้std::forward<T>(v)เพื่อรับหมวดหมู่ค่าดั้งเดิมกลับมา ถ้ามันเป็น lvalue ก่อนที่จะถูกส่งผ่านไปยังการทำงานของคุณจะอยู่ lvalue std::forwardหลังจากที่ถูกส่งผ่าน หากเป็น rvalue จะกลายเป็น rvalue อีกครั้ง (จำไว้ว่าการอ้างอิง rvalue ที่มีชื่อคือ lvalue)

ดังนั้นคุณจะใช้varอย่างถูกต้องในแบบทั่วไปได้อย่างไร std::forward<decltype(var)>(var)ใช้ สิ่งนี้จะทำงานเหมือนกับstd::forward<T>(v)ในแม่แบบฟังก์ชั่นด้านบน ถ้าvarเป็นT&&คุณจะได้รับค่าย้อนกลับและถ้าเป็นT&คุณจะได้รับค่ากลับคืน

กลับไปที่หัวข้อ: อะไรauto&& v = f();และstd::forward<decltype(v)>(v)ใน codebase บอกอะไรเราบ้าง? พวกเขาบอกเราว่าvจะได้รับและส่งต่ออย่างมีประสิทธิภาพมากที่สุด อย่างไรก็ตามโปรดจำไว้ว่าหลังจากที่ส่งต่อตัวแปรดังกล่าวไปแล้วอาจเป็นไปได้ว่ามันถูกย้ายจากดังนั้นมันจึงไม่ถูกต้องที่จะใช้งานเพิ่มเติมโดยไม่ต้องรีเซ็ต

ส่วนตัวผมใช้auto&&ในรหัสทั่วไปเมื่อฉันต้องmodifyableตัวแปร การส่งต่อค่าที่สมบูรณ์แบบกำลังแก้ไขเนื่องจากการดำเนินการย้ายอาจขโมยความกล้า ถ้าฉันเพียงแค่ต้องการที่จะขี้เกียจ (คือไม่ได้สะกดชื่อประเภทแม้ว่าฉันรู้ว่ามัน) และไม่จำเป็นต้องปรับเปลี่ยน (เช่นเมื่อพิมพ์เพียงองค์ประกอบของช่วง) auto const&ฉันจะติด


autoอยู่ในเพื่อให้ห่างไกลแตกต่างกันที่auto v = {1,2,3};จะทำให้ขณะที่จะเป็นความล้มเหลวหักvstd::initializer_listf({1,2,3})


ในส่วนแรกของคำตอบของคุณ: ฉันหมายความว่าหากfoo()ส่งคืนค่าประเภทTแล้วvar(นิพจน์นี้) จะเป็น lvalue และประเภท (ของนิพจน์นี้) จะเป็นการอ้างอิงค่า rvalue กับT(เช่นT&&)
MWid

@MWid: เหมาะสมแล้วนำส่วนแรกออก
Xeo

3

พิจารณาบางประเภทTที่มีตัวสร้างการย้ายและสมมติ

T t( foo() );

ใช้ตัวสร้างการย้ายนั้น

ตอนนี้เรามาใช้การอ้างอิงระดับกลางเพื่อจับภาพผลตอบแทนจากfoo:

auto const &ref = foo();

กฎนี้ใช้ตัวสร้างการย้ายดังนั้นค่าส่งคืนจะต้องคัดลอกแทนการย้าย (แม้ว่าเราจะใช้std::moveที่นี่เราไม่สามารถย้ายไปอ้างอิงกลุ่มอ้างอิงได้)

T t(std::move(ref));   // invokes T::T(T const&)

อย่างไรก็ตามถ้าเราใช้

auto &&rvref = foo();
// ...
T t(std::move(rvref)); // invokes T::T(T &&)

ตัวสร้างการย้ายยังคงมีอยู่


และเพื่อตอบคำถามอื่น ๆ ของคุณ:

... มีสถานการณ์ใดที่สมเหตุสมผลเมื่อคุณควรใช้ auto && เพื่อบอกผู้อ่านถึงโค้ดของคุณ ...

อย่างแรกที่ Xeo บอกว่าโดยพื้นฐานแล้วฉันกำลังผ่าน X อย่างมีประสิทธิภาพเท่าที่จะเป็นไปได้ไม่ว่าประเภท X คืออะไร ดังนั้นการเห็นโค้ดที่ใช้auto&&ภายในควรสื่อสารว่ามันจะใช้ซีแมนติกส์ของการย้ายภายในตามความเหมาะสม

... เหมือนที่คุณทำเมื่อคุณส่งคืน unique_ptr <> เพื่อบอกว่าคุณมีกรรมสิทธิ์เฉพาะ ...

เมื่อเท็มเพลตฟังก์ชั่นใช้อาร์กิวเมนต์ชนิดT&&มันอาจบอกว่ามันอาจย้ายวัตถุที่คุณส่งผ่านกลับมาunique_ptrอย่างชัดเจนให้ความเป็นเจ้าของแก่ผู้โทร; การยอมรับT&&อาจลบความเป็นเจ้าของออกจากผู้โทร (ถ้ามี ctor ย้ายอยู่ ฯลฯ )


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

3
นี่เป็นสิ่งที่ผิด ในทั้งสองกรณีตัวสร้างสำเนาถูกเรียกใช้เนื่องจากrefและrvrefเป็นทั้งค่า T t(std::move(rvref))หากคุณต้องการคอนสตรัคย้ายแล้วคุณจะต้องเขียน
MWid

คุณหมายถึงโทษ const ในตัวอย่างแรกของคุณ: auto const &?
PiotrNycz

@aleguna - คุณและ MWid นั้นถูกต้องขอบคุณ ฉันแก้ไขคำตอบของฉันแล้ว
ไร้ประโยชน์

1
@ ไม่มีประโยชน์คุณพูดถูก แต่นี่ไม่ได้ตอบคำถามของฉัน คุณจะใช้เมื่อใดauto&&และคุณบอกผู้อ่านเกี่ยวกับรหัสของคุณโดยใช้auto&&อย่างไร
MWid

-3

auto &&ไวยากรณ์ใช้สองคุณสมบัติใหม่ของ C ++ 11:

  1. autoส่วนช่วยให้คอมไพเลอร์อนุมานชนิดขึ้นอยู่กับบริบท (ค่าตอบแทนในกรณีนี้) นี้โดยไม่ต้องมีคุณสมบัติอ้างอิงใด ๆ (ช่วยให้คุณสามารถระบุว่าคุณต้องการT, T &หรือT &&สำหรับชนิดอนุมานT)

  2. &&เป็นความหมายใหม่ย้าย ประเภทที่สนับสนุนซีแมนทิกส์การย้ายใช้คอนสตรัคเตอร์T(T && other)ที่จะย้ายเนื้อหาในรูปแบบใหม่ได้อย่างเหมาะสมที่สุด สิ่งนี้อนุญาตให้วัตถุสลับการแทนค่าภายในแทนการทำสำเนาแบบลึก

สิ่งนี้ช่วยให้คุณมีสิ่งต่อไปนี้:

std::vector<std::string> foo();

ดังนั้น:

auto var = foo();

จะทำสำเนาเวกเตอร์ที่ส่งคืน (แพง) แต่:

auto &&var = foo();

จะสลับการแทนค่าภายในของเวกเตอร์ (เวกเตอร์จากfooและเวกเตอร์เปล่าจากvar) ดังนั้นจะเร็วขึ้น

ใช้ในไวยากรณ์ for-loop ใหม่:

for (auto &item : foo())
    std::cout << item << std::endl;

ไหนสำหรับวงมีการถือครองauto &&เพื่อค่าตอบแทนจากfooและมีการอ้างอิงถึงแต่ละค่าในitemfoo


สิ่งนี้ไม่ถูกต้อง auto&&จะไม่ย้ายอะไรมันแค่ทำการอ้างอิง ไม่ว่าจะเป็นการอ้างอิง lvalue หรือ rvalue ขึ้นอยู่กับนิพจน์ที่ใช้ในการเริ่มต้น
Joseph Mansfield

ในทั้งสองกรณีตัวสร้างการย้ายจะถูกเรียกใช้เนื่องจากstd::vectorและstd::stringเป็นตัวสร้างการเคลื่อนย้าย varนี้มีอะไรจะทำอย่างไรกับชนิดของ
MWid

1
@MWid: จริง ๆ แล้วการเรียกไปที่ตัวสร้างการคัดลอก / ย้ายอาจถูกนำมารวมกันกับ RVO
Matthieu M.

@MatthieuM คุณพูดถูก แต่ฉันคิดว่าในตัวอย่างด้านบนตัวคัดลอก cnstructor จะไม่ถูกเรียกใช้เนื่องจากทุกอย่างสามารถเคลื่อนย้ายได้
MWid

1
@MWid: จุดของฉันคือว่าแม้กระทั่งตัวสร้างการเคลื่อนย้ายสามารถ elided Elision สำคัญกว่าการเคลื่อนไหว (มันถูกกว่า)
Matthieu M.
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.