C ++ 11 rvalues ​​และย้ายความสับสนของซีแมนทิกส์ (คำสั่ง return)


435

ฉันพยายามเข้าใจการอ้างอิงค่าและย้ายซีแมนทิกส์ของ C ++ 11

อะไรคือความแตกต่างระหว่างตัวอย่างเหล่านี้และสิ่งใดที่จะไม่คัดลอกเวกเตอร์

ตัวอย่างแรก

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

ตัวอย่างที่สอง

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

ตัวอย่างที่สาม

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

51
โปรดอย่าส่งคืนตัวแปรโลคัลโดยอ้างอิง การอ้างอิง rvalue ยังคงเป็นการอ้างอิง
fredoverflow

63
เห็นได้ชัดว่ามีเจตนาเพื่อเข้าใจความแตกต่างทางความหมายระหว่างตัวอย่าง lol
Tarantula

@FredOverflow คำถามเก่า แต่ใช้เวลาสักครู่ในการเข้าใจความคิดเห็นของคุณ ฉันคิดว่าคำถามที่มี # 2 คือstd::move()สร้างสำเนา "ถาวร" หรือไม่
3 บันทึก

5
@DavidLively std::move(expression)ไม่ได้สร้างอะไรเลยมันแค่แปลงค่าไปเป็นค่า xvalue std::move(expression)ไม่มีวัตถุที่มีการคัดลอกหรือย้ายในขั้นตอนของการประเมิน
fredoverflow

คำตอบ:


563

ตัวอย่างแรก

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

rval_refตัวอย่างแรกผลตอบแทนชั่วคราวซึ่งถูกจับได้โดย ชั่วคราวนั้นจะยืดอายุการใช้งานเกินrval_refคำจำกัดความและคุณสามารถใช้มันราวกับว่าคุณได้รับคุณค่า คล้ายกันมากกับสิ่งต่อไปนี้:

const std::vector<int>& rval_ref = return_vector();

ยกเว้นว่าในการเขียนของฉันคุณเห็นได้ชัดว่าไม่สามารถใช้rval_refในลักษณะที่ไม่ใช่ const

ตัวอย่างที่สอง

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

ในตัวอย่างที่สองคุณได้สร้างข้อผิดพลาดรันไทม์ rval_refตอนนี้มีการอ้างอิงไปยัง destructed tmpภายในฟังก์ชั่น โชคใด ๆ รหัสนี้จะล้มเหลวทันที

ตัวอย่างที่สาม

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

ตัวอย่างที่สามของคุณเทียบเท่ากับตัวอย่างแรกของคุณ การstd::moveเปิดtmpไม่จำเป็นและจริง ๆ แล้วสามารถเป็นการลดประสิทธิภาพได้เนื่องจากจะยับยั้งการปรับค่าส่งคืน

วิธีที่ดีที่สุดในการเขียนโค้ดสิ่งที่คุณทำคือ:

ปฏิบัติที่ดีที่สุด

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

เช่นเดียวกับที่คุณทำใน C ++ 03 tmpจะถือว่าเป็นค่า rvalue ในคำสั่ง return มันอาจจะถูกส่งกลับผ่านทางผลตอบแทนคุ้มค่าการเพิ่มประสิทธิภาพ (คัดลอกไม่มีการย้าย) หรือถ้าคอมไพเลอร์ตัดสินใจที่จะไม่สามารถดำเนินการ RVO แล้วมันจะใช้คอนสตรัคย้ายของเวกเตอร์ที่จะทำผลตอบแทน เฉพาะในกรณีที่ไม่มีการทำ RVO และหากประเภทที่ส่งคืนไม่มีตัวสร้างการย้ายจะใช้ตัวสร้างการคัดลอกสำหรับการส่งคืน


65
คอมไพเลอร์จะ RVO เมื่อคุณคืนค่าโลคัลอ็อบเจ็กต์ตามค่าและชนิดของโลคัลและการส่งคืนของฟังก์ชันเหมือนกันและไม่มีคุณสมบัติ cv (ไม่ส่งคืนชนิด const) อยู่ห่างจากการกลับมาพร้อมกับคำสั่ง condition (:?) เนื่องจากสามารถยับยั้ง RVO อย่าล้อมโลคัลในฟังก์ชันอื่นที่ส่งคืนการอ้างอิงถึงโลคัล return my_local;เพียงแค่ คำสั่งการส่งคืนหลายรายการตกลงและจะไม่ยับยั้ง RVO
Howard Hinnant

27
มีข้อแม้คือ: เมื่อส่งคืนสมาชิกของวัตถุในพื้นที่การย้ายจะต้องชัดเจน
boycy

5
@NoSenseEtAl: ไม่มีการสร้างชั่วคราวในบรรทัดส่งคืน moveไม่ได้สร้างชั่วคราว มันจะใช้ค่า lvalue ให้กับ xvalue โดยไม่ทำสำเนาสร้างสิ่งใดทำลายสิ่งใด ตัวอย่างนั้นเป็นสถานการณ์เดียวกับที่คุณส่งคืนโดยการอ้างอิง lvalue และลบออกmoveจากบรรทัดที่ส่งคืน: วิธีใดก็ตามที่คุณได้รับการอ้างอิงห้อยไปที่ตัวแปรท้องถิ่นภายในฟังก์ชันและสิ่งที่ถูกทำลาย
Howard Hinnant

15
"คำสั่งการส่งคืนหลายรายการเป็น ok และจะไม่ยับยั้ง RVO": เฉพาะในกรณีที่พวกเขาส่งกลับตัวแปรเดียวกัน
Deduplicator

5
@Dupuplicator: คุณถูกต้อง ฉันพูดไม่ถูกต้องตามที่ตั้งใจ ฉันหมายถึงว่าข้อความสั่งคืนหลายรายการไม่ได้ห้ามคอมไพเลอร์จาก RVO (แม้ว่าจะเป็นไปไม่ได้ที่จะนำไปใช้) และดังนั้นการแสดงออกของการส่งคืนจึงยังถือว่าเป็นค่า rvalue
Howard Hinnant

42

ไม่มีของพวกเขาจะคัดลอก แต่ที่สองจะอ้างถึงเวกเตอร์ที่ถูกทำลาย การอ้างอิง rvalue ที่ระบุชื่อแทบไม่เคยมีอยู่ในรหัสปกติ คุณเขียนมันเป็นวิธีที่คุณจะเขียนสำเนาใน C ++ 03

std::vector<int> return_vector()
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

ยกเว้นตอนนี้เวกเตอร์จะถูกย้าย ผู้ใช้ของชั้นไม่ได้จัดการกับมันอ้างอิง rvalue ในส่วนใหญ่ของกรณี


คุณแน่ใจจริงๆหรือว่าตัวอย่างที่สามกำลังจะคัดลอกเวกเตอร์?
ทารันทูล่า

@Tarantula: มันจะจับภาพเวกเตอร์ของคุณ ไม่ว่าจะทำหรือไม่ได้คัดลอกก่อนที่จะทำลายไม่สำคัญ
ลูกสุนัข

4
ฉันไม่เห็นเหตุผลใด ๆ ที่คุณขอให้จับจอง เป็นการดีที่จะผูกตัวแปรอ้างอิง rvalue ท้องถิ่นกับค่า rvalue ในกรณีดังกล่าวอายุการใช้งานของวัตถุชั่วคราวจะถูกยืดอายุการใช้งานของตัวแปรอ้างอิง rvalue
fredoverflow

1
เป็นเพียงคำอธิบายเพราะฉันเรียนรู้สิ่งนี้ ในตัวอย่างใหม่นี้เวกเตอร์tmpไม่ได้ถูกย้ายเข้าไปrval_refแต่เขียนลงในโดยตรงrval_refโดยใช้ RVO (เช่นการคัดลอกข้อมูล) มีความแตกต่างระหว่างstd::moveและคัดลอกเป็น elision A std::moveอาจยังเกี่ยวข้องกับข้อมูลที่จะคัดลอก; ในกรณีของเวกเตอร์เวกเตอร์ใหม่ถูกสร้างขึ้นจริงในตัวสร้างการคัดลอกและข้อมูลถูกจัดสรร แต่ส่วนใหญ่ของอาร์เรย์ข้อมูลจะถูกคัดลอกโดยการคัดลอกตัวชี้เท่านั้น ตัวเลือกการคัดลอกจะหลีกเลี่ยง 100% ของสำเนาทั้งหมด
ทำเครื่องหมาย Lakata

@MarkLakata นี่คือ NRVO ไม่ใช่ RVO NRVO เป็นทางเลือกแม้ใน C ++ 17 ถ้ายังไม่ได้นำมาใช้ทั้งค่าตอบแทนและตัวแปรที่ถูกสร้างขึ้นโดยใช้ย้ายสร้างของrval_ref std::vectorไม่มีตัวสร้างสำเนาที่เกี่ยวข้องทั้งมี / std::moveไม่มีคือ tmpจะถือว่าเป็นrvalueในreturnคำสั่งในกรณีนี้
Daniel Langr

16

คำตอบง่ายๆคือคุณควรเขียนโค้ดสำหรับการอ้างอิงค่าเช่นเดียวกับการอ้างอิงรหัสปกติและคุณควรปฏิบัติต่อพวกเขาด้วยวิธีเดียวกัน 99% ของเวลา ซึ่งรวมถึงกฎเก่าทั้งหมดเกี่ยวกับการส่งคืนการอ้างอิง (เช่นจะไม่ส่งคืนการอ้างอิงไปยังตัวแปรโลคอล)

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

ข้อดีอย่างหนึ่งของตัวสร้างการย้ายและการกำหนดย้ายคือถ้าคุณกำหนดคอมไพเลอร์สามารถใช้ในกรณีที่ RVO (การเพิ่มประสิทธิภาพค่าที่ส่งคืน) และ NRVO (การเพิ่มประสิทธิภาพค่าที่ส่งคืนชื่อ) ล้มเหลวในการเรียกใช้ นี่เป็นเรื่องที่ค่อนข้างใหญ่ในการส่งคืนวัตถุที่มีราคาแพงเช่นภาชนะและสายอักขระตามค่าอย่างมีประสิทธิภาพจากวิธีการ

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

std::vector vec;
for(int x=0; x<10; ++x)
{
    // automatically uses rvalue reference constructor if available
    // because MyCheapType is an unamed temporary variable
    vec.push_back(MyCheapType(0.f));
}


std::vector vec;
for(int x=0; x<10; ++x)
{
    MyExpensiveType temp(1.0, 3.0);
    temp.initSomeOtherFields(malloc(5000));

    // old way, passed via const reference, expensive copy
    vec.push_back(temp);

    // new way, passed via rvalue reference, cheap move
    // just don't use temp again,  not difficult in a loop like this though . . .
    vec.push_back(std::move(temp));
}

คอนเทนเนอร์ STL ได้รับการปรับปรุงให้มีการย้ายเกินพิกัดสำหรับเกือบทุกอย่าง (คีย์แฮชและค่าการแทรกเวกเตอร์ ฯลฯ ) และเป็นที่ที่คุณจะเห็นได้มากที่สุด

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

TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName)
{
    std::unique_ptr<TextureObject> tex = D3DCreateTexture(width, height, fmt);
    tex->friendlyName = std::move(friendlyName);
    return tex;
}

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

// move from temporary
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));

หรือ

// explicit move (not going to use the variable 'str' after the create call)
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str));

หรือ

// explicitly make a copy and pass the temporary of the copy down
// since we need to use str again for some reason
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str));

แต่สิ่งนี้จะไม่รวบรวม!

string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);

3

ไม่ใช่คำตอบต่อคำแนะนำ แต่เป็นแนวทาง เวลาส่วนใหญ่มีความรู้สึกไม่มากในการประกาศT&&ตัวแปรท้องถิ่น(เช่นที่คุณทำกับstd::vector<int>&& rval_ref) คุณจะต้องให้std::move()พวกเขาใช้foo(T&&)วิธีการพิมพ์ นอกจากนี้ยังมีปัญหาที่กล่าวถึงแล้วว่าเมื่อคุณพยายามที่จะส่งคืนrval_refจากฟังก์ชั่นดังกล่าวคุณจะได้รับการอ้างอิงถึงการทำลายแบบชั่วคราวที่ล้มเหลว

ส่วนใหญ่ฉันจะไปกับรูปแบบดังต่อไปนี้:

// Declarations
A a(B&&, C&&);
B b();
C c();

auto ret = a(b(), c());

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

auto bRet = b();
auto cRet = c();
auto aRet = a(std::move(b), std::move(c));

// Either these just fail (assert/exception), or you won't get 
// your expected results due to their clean state.
bRet.foo();
cRet.bar();

เห็นได้ชัดว่ามี (แม้ว่าจะค่อนข้างหายาก) ในกรณีที่ฟังก์ชั่นส่งกลับอย่างแท้จริงT&&ซึ่งเป็นการอ้างอิงไปยังวัตถุที่ไม่ใช่ชั่วคราวที่คุณสามารถย้ายเข้าไปในวัตถุของคุณ

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


2

จะไม่มีการคัดลอกเพิ่มเติมใด ๆ แม้ว่าจะไม่ได้ใช้ RVO มาตรฐานใหม่บอกว่าการสร้างการเคลื่อนย้ายนั้นต้องการคัดลอกเมื่อทำสิ่งที่ฉันเชื่อว่ากลับมา

ฉันเชื่อว่าตัวอย่างที่สองของคุณทำให้เกิดพฤติกรรมที่ไม่ได้กำหนดเนื่องจากคุณจะส่งคืนการอ้างอิงถึงตัวแปรท้องถิ่น


1

ดังที่ได้กล่าวไปแล้วในความคิดเห็นต่อคำตอบแรกการreturn std::move(...);สร้างสามารถสร้างความแตกต่างในกรณีอื่นนอกเหนือจากการส่งคืนตัวแปรท้องถิ่น นี่คือตัวอย่างที่เรียกใช้ได้ซึ่งบันทึกสิ่งที่เกิดขึ้นเมื่อคุณส่งคืนวัตถุสมาชิกที่มีและไม่มีstd::move():

#include <iostream>
#include <utility>

struct A {
  A() = default;
  A(const A&) { std::cout << "A copied\n"; }
  A(A&&) { std::cout << "A moved\n"; }
};

class B {
  A a;
 public:
  operator A() const & { std::cout << "B C-value: "; return a; }
  operator A() & { std::cout << "B L-value: "; return a; }
  operator A() && { std::cout << "B R-value: "; return a; }
};

class C {
  A a;
 public:
  operator A() const & { std::cout << "C C-value: "; return std::move(a); }
  operator A() & { std::cout << "C L-value: "; return std::move(a); }
  operator A() && { std::cout << "C R-value: "; return std::move(a); }
};

int main() {
  // Non-constant L-values
  B b;
  C c;
  A{b};    // B L-value: A copied
  A{c};    // C L-value: A moved

  // R-values
  A{B{}};  // B R-value: A copied
  A{C{}};  // C R-value: A moved

  // Constant L-values
  const B bc;
  const C cc;
  A{bc};   // B C-value: A copied
  A{cc};   // C C-value: A copied

  return 0;
}

สมมุติreturn std::move(some_member);เพียงทำให้รู้สึกถ้าคุณต้องการที่จะย้ายจริงสมาชิกระดับโดยเฉพาะอย่างยิ่งเช่นในกรณีที่class Cแสดงให้เห็นถึงวัตถุอะแดปเตอร์สั้น ๆ struct Aโดยมีจุดประสงค์ของการสร้างอินสแตนซ์

โปรดสังเกตว่าstruct Aจะถูกคัดลอกออกมาได้ตลอดเวลาclass Bแม้เมื่อclass Bวัตถุนั้นเป็นค่า R นี่เป็นเพราะคอมไพเลอร์ไม่มีทางที่จะบอกได้ว่าclass Bอินสแตนซ์ของstruct Aจะไม่ถูกใช้อีกต่อไป ในclass Cคอมไพเลอร์ไม่มีข้อมูลนี้std::move()ซึ่งเป็นสาเหตุที่struct Aทำให้ถูกย้ายเว้นแต่ว่าอินสแตนซ์ของclass Cเป็นค่าคงที่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.