“ การอ้างอิงค่าสำหรับ * นี้” คืออะไร?


238

มาข้ามข้อเสนอที่เรียกว่า "การอ้างอิง rvalue สำหรับ * นี้" ในเสียงดังกราวของC ++ หน้า

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

มีลิงก์ไปยังเอกสารข้อเสนอบนหน้า: N2439 (ขยายความหมายของการย้ายไปเป็น * สิ่งนี้) แต่ฉันก็ไม่ได้รับตัวอย่างมากมายจากที่นั่น

คุณสมบัตินี้เกี่ยวกับอะไร?

คำตอบ:


293

ก่อนอื่น "ref-qualifier สำหรับ * this" เป็นเพียง "งบการตลาด" ประเภทของการ*thisไม่เปลี่ยนแปลงดูด้านล่างของโพสต์นี้ เป็นวิธีที่ง่ายกว่าที่จะเข้าใจด้วยถ้อยคำนี้

ถัดไปรหัสต่อไปนี้เลือกฟังก์ชั่นที่จะเรียกว่าขึ้นอยู่กับref-qualifierของ "พารามิเตอร์วัตถุ implicit" ของฟังก์ชั่น :

// t.cpp
#include <iostream>

struct test{
  void f() &{ std::cout << "lvalue object\n"; }
  void f() &&{ std::cout << "rvalue object\n"; }
};

int main(){
  test t;
  t.f(); // lvalue
  test().f(); // rvalue
}

เอาท์พุท:

$ clang++ -std=c++0x -stdlib=libc++ -Wall -pedantic t.cpp
$ ./a.out
lvalue object
rvalue object

มีการทำสิ่งต่าง ๆ ทั้งหมดเพื่อให้คุณสามารถใช้ประโยชน์จากความจริงเมื่อวัตถุที่เรียกใช้ฟังก์ชันเป็นค่า rvalue (เช่นชื่อชั่วคราวเป็นต้น) ใช้รหัสต่อไปนี้เป็นตัวอย่างเพิ่มเติม:

struct test2{
  std::unique_ptr<int[]> heavy_resource;

  test2()
    : heavy_resource(new int[500]) {}

  operator std::unique_ptr<int[]>() const&{
    // lvalue object, deep copy
    std::unique_ptr<int[]> p(new int[500]);
    for(int i=0; i < 500; ++i)
      p[i] = heavy_resource[i];

    return p;
  }

  operator std::unique_ptr<int[]>() &&{
    // rvalue object
    // we are garbage anyways, just move resource
    return std::move(heavy_resource);
  }
};

นี่อาจเป็นสิ่งที่ถูกประดิษฐ์ขึ้นมา แต่คุณควรเข้าใจ

โปรดทราบว่าคุณสามารถรวมcv-qualifier ( constและvolatile) และref-qualifiers ( &และ&&)


หมายเหตุ: คำพูดมาตรฐานและคำอธิบายการแก้ปัญหาการโอเวอร์โหลดมากมายหลังจากที่นี่!

†เพื่อให้เข้าใจวิธีการทำงานและทำไมคำตอบของ @Nicol Bolas อย่างน้อยก็ส่วนหนึ่งเราต้องขุดในมาตรฐาน C ++ สักหน่อย (ส่วนที่อธิบายว่าทำไมคำตอบของ @ Nicol ผิดที่ด้านล่างถ้าคุณ สนใจเฉพาะในนั้น)

ฟังก์ชั่นซึ่งจะถูกเรียกว่าจะถูกกำหนดโดยกระบวนการที่เรียกว่าความละเอียดเกินพิกัด กระบวนการนี้ค่อนข้างซับซ้อนดังนั้นเราจะแตะต้องสิ่งที่สำคัญสำหรับเราเท่านั้น

ก่อนอื่นสิ่งสำคัญคือการดูว่าการแก้ไขการโอเวอร์โหลดสำหรับฟังก์ชันสมาชิกทำงานอย่างไร:

§13.3.1 [over.match.funcs]

p2 ชุดของฟังก์ชั่นผู้สมัครสามารถมีทั้งฟังก์ชั่นสมาชิกและไม่ใช่สมาชิกที่จะแก้ไขกับรายการอาร์กิวเมนต์เดียวกัน เพื่อให้การโต้แย้งและพารามิเตอร์รายการที่มีการเทียบเคียงภายในชุดที่แตกต่างกันนี้ฟังก์ชันสมาชิกถือว่ามีพารามิเตอร์พิเศษที่เรียกว่าพารามิเตอร์วัตถุโดยปริยายซึ่งหมายถึงวัตถุที่ฟังก์ชันสมาชิกที่ได้รับการเรียกว่า [ ... ]

p3 ในทำนองเดียวกันเมื่อเหมาะสมบริบทสามารถสร้างรายการอาร์กิวเมนต์ที่มีอาร์กิวเมนต์วัตถุโดยนัยเพื่อแสดงวัตถุที่จะดำเนินการ

ทำไมเราถึงต้องเปรียบเทียบฟังก์ชั่นสมาชิกกับผู้ที่ไม่ได้เป็นสมาชิก สาเหตุที่ทำให้เกิดการบรรทุกเกินพิกัด พิจารณาสิ่งนี้:

struct foo{
  foo& operator<<(void*); // implementation unimportant
};

foo& operator<<(foo&, char const*); // implementation unimportant

แน่นอนว่าคุณต้องการให้มีการเรียกใช้ฟังก์ชันฟรีต่อไปนี้ใช่ไหม

char const* s = "free foo!\n";
foo f;
f << s;

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

p4 สำหรับฟังก์ชั่นสมาชิกคงที่ประเภทของพารามิเตอร์วัตถุโดยนัยคือ

  • “ การอ้างอิง lvalue กับcv X ” สำหรับฟังก์ชั่นที่ประกาศโดยไม่มีตัวระบุคุณสมบัติหรือด้วยตัวระบุ& คุณสมบัติ

  • “ การอ้างอิง rvalue กับcv X ” สำหรับฟังก์ชั่นที่ประกาศด้วย&& ref-qualifier

โดยที่Xเป็นคลาสที่ฟังก์ชันเป็นสมาชิกและcvคือคุณสมบัติของ CV ในการประกาศฟังก์ชันสมาชิก [ ... ]

p5 ในระหว่างการแก้ปัญหาการโอเวอร์โหลด [... ] [t] เขาระบุพารามิเตอร์ของวัตถุ [... ] ยังคงมีเอกลักษณ์เนื่องจากการแปลงในอาร์กิวเมนต์ที่สอดคล้องกันจะต้องปฏิบัติตามกฎเพิ่มเติมเหล่านี้:

  • ไม่สามารถแนะนำวัตถุชั่วคราวให้ระงับอาร์กิวเมนต์สำหรับพารามิเตอร์วัตถุโดยนัยได้ และ

  • ไม่สามารถใช้การแปลงที่ผู้ใช้กำหนดเองเพื่อให้ได้ประเภทที่ตรงกัน

[ ... ]

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

ลองมาตัวอย่างแรกที่ด้านบนของโพสต์นี้ หลังจากการแปลงสภาพที่กล่าวมาแล้วชุดโหลดเกินมีลักษณะดังนี้:

void f1(test&); // will only match lvalues, linked to 'void test::f() &'
void f2(test&&); // will only match rvalues, linked to 'void test::f() &&'

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

// first call to 'f' in 'main'
test t;
f1(t); // 't' (lvalue) can match 'test&' (lvalue reference)
       // kept in overload-set
f2(t); // 't' not an rvalue, can't match 'test&&' (rvalue reference)
       // taken out of overload-set

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

// second call to 'f' in 'main'
f1(test()); // 'test()' not an lvalue, can't match 'test&' (lvalue reference)
            // taken out of overload-set
f2(test()); // 'test()' (rvalue) can match 'test&&' (rvalue reference)
            // kept in overload-set

อย่างไรก็ตามโปรดทราบว่าหากเราไม่ได้ให้ตัวระบุคุณสมบัติไว้ (และไม่ได้มีการใช้งานฟังก์ชันมากเกินไป) ซึ่งf1 จะตรงกับค่า rvalue (ยัง§13.3.1):

p5 [... ] สำหรับฟังก์ชั่นสมาชิกที่ไม่คงที่ที่ประกาศโดยไม่มีตัวระบุคุณสมบัติจะใช้กฎเพิ่มเติม

  • แม้ว่าพารามิเตอร์วัตถุ implicit ไม่ได้ผ่านการconstรับรองคุณสมบัติ rvalue สามารถผูกกับพารามิเตอร์ตราบใดที่ทุกประการอาร์กิวเมนต์สามารถแปลงเป็นชนิดของพารามิเตอร์วัตถุ implicit
struct test{
  void f() { std::cout << "lvalue or rvalue object\n"; }
};

int main(){
  test t;
  t.f(); // OK
  test().f(); // OK too
}

ทีนี้ทำไมคำตอบของ @ Nicol ถึงผิดอย่างน้อยส่วนหนึ่ง เขาพูดว่า:

*thisโปรดทราบว่าประกาศนี้การเปลี่ยนแปลงประเภทของ

นั่นเป็นสิ่งที่ผิด*thisมีค่า lvalue เสมอ :

§5.3.1 [expr.unary.op] p1

ตัวดำเนิน*การunary ดำเนินการทางอ้อม : นิพจน์ที่ใช้นั้นจะเป็นตัวชี้ไปยังชนิดของวัตถุหรือตัวชี้ไปยังชนิดของฟังก์ชันและผลลัพธ์คือ lvalue ที่อ้างถึงวัตถุหรือฟังก์ชันที่จุดแสดงออกนั้น

§9.3.2 [class.this] p1

ในเนื้อความของฟังก์ชันสมาชิกแบบไม่คงที่ (9.3) คำสำคัญthisคือนิพจน์ prvalue ซึ่งค่าคือที่อยู่ของวัตถุที่เรียกใช้ฟังก์ชัน ประเภทของthisในฟังก์ชั่นสมาชิกของชั้นเรียนคือX X*[ ... ]


ฉันเชื่อว่าประเภท paraneter หลังจากส่วน "หลังการเปลี่ยนแปลง" ควรเป็น 'foo' แทน 'test'
ryaner

@ryaner: หาดีขอบคุณ แม้ว่าจะไม่ใช่พารามิเตอร์ แต่ตัวระบุคลาสของฟังก์ชันนั้นไม่ถูกต้อง :)
Xeo

โอ๊ะขอโทษฉันลืมเกี่ยวกับการทดสอบชื่อชั้นของเล่นเมื่อฉันอ่านส่วนหนึ่งและคิดว่าฉอยู่ภายใน foo ดังนั้นความคิดเห็นของฉัน ..
ryaner

นี้สามารถทำได้ด้วยการก่อสร้าง: MyType(int a, double b) &&?
Germán Diago

2
"ประเภทของ * สิ่งนี้ไม่มีวันเปลี่ยนแปลง" คุณอาจจะชัดเจนกว่าเล็กน้อยว่ามันจะไม่เปลี่ยนแปลงตามคุณสมบัติ r / l-value แต่มันสามารถเปลี่ยนระหว่าง const / non-const
xaxxon

78

มีกรณีการใช้งานเพิ่มเติมสำหรับแบบฟอร์มการคัดเลือกผู้คัดกรอง lvalue C ++ 98 มีภาษาที่อนุญาตให้constฟังก์ชันที่ไม่ใช่สมาชิกถูกเรียกใช้สำหรับอินสแตนซ์ของคลาสที่ rvalues สิ่งนี้นำไปสู่ความแปลกประหลาดทุกประเภทที่ขัดกับแนวคิดเรื่องความมีค่าและความเบี่ยงเบนจากการทำงานของประเภทในตัว:

struct S {
  S& operator ++(); 
  S* operator &(); 
};
S() = S();      // rvalue as a left-hand-side of assignment!
S& foo = ++S(); // oops, dangling reference
&S();           // taking address of rvalue...

ตัวระบุผู้อ้างอิง Lvalue แก้ปัญหาเหล่านี้ได้:

struct S {
  S& operator ++() &;
  S* operator &() &;
  const S& operator =(const S&) &;
};

ตอนนี้ตัวดำเนินการทำงานเหมือนกับชนิดที่มีอยู่ภายในยอมรับเฉพาะค่า lvalues


28

สมมติว่าคุณมีสองฟังก์ชันในคลาสทั้งที่มีชื่อและลายเซ็นเหมือนกัน แต่หนึ่งในนั้นถูกประกาศconst:

void SomeFunc() const;
void SomeFunc();

ถ้าอินสแตนซ์ของคลาสไม่ใช่constความละเอียดเกินพิกัดจะเลือกเวอร์ชันที่ไม่ใช่ const หากอินสแตนซ์นั้นconstผู้ใช้สามารถเรียกconstรุ่นได้เท่านั้น และthisตัวชี้เป็นconstตัวชี้ดังนั้นจึงไม่สามารถเปลี่ยนอินสแตนซ์ได้

"การอ้างอิง r-value สำหรับสิ่งนี้` อนุญาตให้คุณเพิ่มทางเลือกอื่นได้:

void RValueFunc() &&;

สิ่งนี้ช่วยให้คุณมีฟังก์ชั่นที่สามารถเรียกใช้ได้เฉพาะเมื่อผู้ใช้เรียกมันผ่านค่า r ที่เหมาะสม ดังนั้นถ้าเป็นแบบนี้Object:

Object foo;
foo.RValueFunc(); //error: no `RValueFunc` version exists that takes `this` as l-value.
Object().RValueFunc(); //calls the non-const, && version.

ด้วยวิธีนี้คุณสามารถใช้ลักษณะการทำงานโดยขึ้นอยู่กับว่าวัตถุนั้นถูกเข้าถึงผ่าน r-value หรือไม่

โปรดทราบว่าคุณไม่ได้รับอนุญาตให้โอเวอร์โหลดระหว่างเวอร์ชันการอ้างอิง r-value และเวอร์ชันที่ไม่ใช่การอ้างอิง นั่นคือถ้าคุณมีชื่อฟังก์ชันสมาชิกเวอร์ชันทั้งหมดจะใช้ตัวระบุคุณสมบัติ l / r-value thisหรือไม่สามารถทำได้ คุณทำสิ่งนี้ไม่ได้:

void SomeFunc();
void SomeFunc() &&;

คุณต้องทำสิ่งนี้:

void SomeFunc() &;
void SomeFunc() &&;

*thisโปรดทราบว่าประกาศนี้การเปลี่ยนแปลงประเภทของ ซึ่งหมายความว่า&&เวอร์ชันสมาชิกเข้าถึงทั้งหมดเป็นการอ้างอิง r-value ดังนั้นจึงเป็นไปได้ที่จะย้ายจากภายในวัตถุได้อย่างง่ายดาย ตัวอย่างที่ระบุในข้อเสนอรุ่นแรกคือ (หมายเหตุ: ต่อไปนี้อาจไม่ถูกต้องกับรุ่นสุดท้ายของ C ++ 11 ซึ่งตรงจากข้อเสนอ "r-value จากจุดเริ่มต้น"):

class X {
   std::vector<char> data_;
public:
   // ...
   std::vector<char> const & data() const & { return data_; }
   std::vector<char> && data() && { return data_; }
};

X f();

// ...
X x;
std::vector<char> a = x.data(); // copy
std::vector<char> b = f().data(); // move

2
ฉันคิดว่าคุณต้องการstd::moveรุ่นที่สองใช่มั้ย นอกจากนี้ทำไมการอ้างอิงค่า rvalue ถึงกลับมา?
Xeo

1
@ Xeo: เพราะนั่นคือตัวอย่างที่อยู่ในข้อเสนอ; ฉันไม่รู้ว่ามันยังใช้ได้กับเวอร์ชั่นปัจจุบันหรือไม่ และเหตุผลสำหรับการอ้างอิงค่า r-value นั้นเป็นเพราะการเคลื่อนไหวควรขึ้นอยู่กับคนที่จับมัน มันยังไม่ควรเกิดขึ้นในกรณีที่เขาต้องการเก็บไว้ใน && แทนค่า
Nicol Bolas

3
ใช่ฉันคิดว่าเหตุผลสำหรับคำถามที่สองของฉัน ฉันสงสัยว่าจะมีการอ้างอิงค่าสมาชิกที่ยืดเยื้อชั่วคราวยืดอายุการใช้งานของชั่วคราวนั้นหรือสมาชิกของมัน? ฉันสาบานได้เลยว่าฉันเห็นคำถามเกี่ยวกับเรื่องนั้นดังนั้นเมื่อไม่นานมานี้ ...
Xeo

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

15
ฉันคิดว่าฉันอาจจะอธิบายหลังจากทั้งหมดฉันสร้างคุณลักษณะนี้สำหรับ C ++ 11;) Xeo ถูกต้องในการยืนยันว่าจะไม่เปลี่ยนประเภทของ*thisแต่ฉันสามารถเข้าใจที่มาจากความสับสน นี่คือเนื่องจาก ref-qualifier เปลี่ยนประเภทของพารามิเตอร์ฟังก์ชัน implicit (หรือ "hidden") ซึ่งเป็นวัตถุ "this" (อัญประกาศใส่จุดประสงค์ที่นี่!) ถูกผูกไว้ระหว่างการแก้ปัญหาโอเวอร์โหลดและการเรียกใช้ฟังก์ชัน ดังนั้นจึงไม่มีการเปลี่ยนแปลง*thisเนื่องจากสิ่งนี้ได้รับการแก้ไขตามที่ Xeo อธิบาย แทนที่จะเปลี่ยนของ "hiddden พารามิเตอร์" ที่จะทำให้มัน lvalue- หรือ rvalue อ้างอิงเช่นเดียวกับconstฟังก์ชั่นรอบคัดเลือกทำให้มันconstฯลฯ ..
bronekk
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.