ข้อดีของ pass-by-value และ std :: move over pass-by-reference


107

ฉันกำลังเรียนรู้ C ++ ในขณะนี้และพยายามหลีกเลี่ยงนิสัยที่ไม่ดี จากสิ่งที่ฉันเข้าใจเสียงดังเป็นระเบียบมี "แนวทางปฏิบัติที่ดีที่สุด" มากมายและฉันพยายามยึดมั่นในแนวทางปฏิบัติเหล่านี้ให้ดีที่สุด (แม้ว่าฉันจะไม่เข้าใจว่าเหตุใดจึงถือว่าดี) แต่ฉันไม่แน่ใจว่า เข้าใจสิ่งที่แนะนำที่นี่

ฉันใช้คลาสนี้จากบทช่วยสอน:

class Creature
{
private:
    std::string m_name;

public:
    Creature(const std::string &name)
            :  m_name{name}
    {
    }
};

std::moveนี้นำไปสู่ข้อเสนอแนะจากเสียงดังกราว-เป็นระเบียบเรียบร้อยที่ฉันควรจะผ่านค่าแทนการอ้างอิงและการใช้งาน ถ้าเป็นเช่นนั้นฉันจะได้รับคำแนะนำให้ทำการnameอ้างอิง (เพื่อให้แน่ใจว่าจะไม่มีการคัดลอกทุกครั้ง) และคำเตือนที่std::moveจะไม่มีผลใด ๆ เพราะnameเป็นconstดังนั้นฉันจึงควรลบออก

วิธีเดียวที่ฉันไม่ได้รับคำเตือนคือการลบconstทั้งหมด:

Creature(std::string name)
        :  m_name{std::move(name)}
{
}

ซึ่งดูเหมือนจะเป็นตรรกะเนื่องจากประโยชน์เพียงอย่างเดียวconstคือการป้องกันไม่ให้ยุ่งกับสตริงเดิม (ซึ่งไม่ได้เกิดขึ้นเพราะฉันส่งผ่านค่า) แต่ฉันอ่านในCPlusPlus.com :

แม้ว่าโปรดทราบว่า -in การย้ายไลบรารีมาตรฐานหมายความว่าอ็อบเจ็กต์ที่ย้ายจากถูกปล่อยให้อยู่ในสถานะที่ถูกต้อง แต่ไม่ได้ระบุ ซึ่งหมายความว่าหลังจากการดำเนินการดังกล่าวมูลค่าของวัตถุที่ย้ายจากควรถูกทำลายหรือกำหนดค่าใหม่เท่านั้น การเข้าถึงมิฉะนั้นจะให้ค่าที่ไม่ระบุ

ลองนึกภาพรหัสนี้:

std::string nameString("Alex");
Creature c(nameString);

เนื่องจากnameStringได้รับการส่งผ่านค่าstd::moveจะทำให้ไม่ถูกต้องnameภายในตัวสร้างและไม่สัมผัสสตริงเดิม แต่ข้อดีของสิ่งนี้คืออะไร? ดูเหมือนว่าเนื้อหาจะถูกคัดลอกเพียงครั้งเดียว - หากฉันผ่านการอ้างอิงเมื่อฉันโทรm_name{name}ถ้าฉันส่งผ่านค่าเมื่อฉันส่งผ่าน (แล้วมันจะถูกย้าย) ฉันเข้าใจว่าสิ่งนี้ดีกว่าการส่งผ่านค่าและไม่ใช้std::move(เพราะถูกคัดลอกสองครั้ง)

คำถามสองข้อ:

  1. ฉันเข้าใจถูกต้องหรือไม่ว่าเกิดอะไรขึ้นที่นี่?
  2. มีข้อดีของการใช้std::moveover pass by reference และแค่โทรm_name{name}?

3
ด้วยการอ้างอิงCreature c("John");ทำสำเนาเพิ่มเติม
user253751

1
ลิงก์นี้อาจเป็นการอ่านที่มีคุณค่าซึ่งครอบคลุมถึงการผ่านstd::string_viewและ SSO ด้วย
lubgr

ฉันพบว่าclang-tidyเป็นวิธีที่ดีในการทำให้ตัวเองหมกมุ่นอยู่กับการเพิ่มประสิทธิภาพขนาดเล็กที่ไม่จำเป็นโดยเสียค่าใช้จ่ายในการอ่าน คำถามที่จะถามที่นี่ก่อนสิ่งอื่นคือเราเรียกตัวสร้างจริงกี่ครั้ง Creature
cz

คำตอบ:


37
  1. ฉันเข้าใจถูกต้องหรือไม่ว่าเกิดอะไรขึ้นที่นี่?

ใช่.

  1. มีข้อดีของการใช้std::moveover pass by reference และแค่โทรm_name{name}?

ลายเซ็นของฟังก์ชันที่เข้าใจง่ายโดยไม่มีการโอเวอร์โหลดเพิ่มเติม ลายเซ็นจะเปิดเผยทันทีว่าอาร์กิวเมนต์จะถูกคัดลอกซึ่งจะช่วยไม่ให้ผู้โทรสงสัยว่าconst std::string&ข้อมูลอ้างอิงอาจถูกจัดเก็บเป็นสมาชิกข้อมูลหรือไม่ซึ่งอาจกลายเป็นการอ้างอิงแบบห้อยในภายหลัง และไม่จำเป็นต้องโอเวอร์โหลดstd::string&& nameและconst std::string&อาร์กิวเมนต์เพื่อหลีกเลี่ยงสำเนาที่ไม่จำเป็นเมื่อส่งค่า rvalues ​​ไปยังฟังก์ชัน ผ่าน lvalue

std::string nameString("Alex");
Creature c(nameString);

ไปยังฟังก์ชันที่ใช้อาร์กิวเมนต์ตามค่าทำให้เกิดสำเนาหนึ่งชุดและโครงสร้างการย้ายหนึ่งครั้ง ส่งค่า rvalue ไปยังฟังก์ชันเดียวกัน

std::string nameString("Alex");
Creature c(std::move(nameString));

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

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

void setName(std::string name)
{
    m_name = std::move(name);
}

จะทำให้เกิดการยกเลิกการจัดสรรทรัพยากรที่m_nameอ้างถึงก่อนที่จะมอบหมายใหม่ ฉันขอแนะนำให้อ่านรายการ 41 ใน C ++ สมัยใหม่ที่มีประสิทธิภาพและคำถามนี้ด้วย


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

1
ใช่นั่นแหล่ะ เมื่อกำหนดให้m_nameจากconst std::string&พารามิเตอร์หน่วยความจำภายในจะถูกนำมาใช้ใหม่ตราบเท่าที่m_nameพอดีเมื่อทำการย้าย - กำหนดm_nameหน่วยความจำจะต้องถูกจัดสรรก่อนล่วงหน้า มิฉะนั้นจะเป็นไปไม่ได้ที่จะ "ขโมย" ทรัพยากรจากด้านขวามือของงาน
lubgr

เมื่อใดที่จะกลายเป็นข้อมูลอ้างอิงที่ห้อยลงมา ฉันคิดว่ารายการเริ่มต้นใช้สำเนาลึก
Li Taiji

105
/* (0) */ 
Creature(const std::string &name) : m_name{name} { }
  • ผ่านlvalueกระหม่อมnameแล้วจะถูกคัดลอกm_nameลงใน

  • ผ่านrvalueกระหม่อมnameแล้วจะถูกคัดลอกm_nameลงใน


/* (1) */ 
Creature(std::string name) : m_name{std::move(name)} { }
  • ผ่านlvalueถูกคัดลอกลงในnameนั้นคือการย้ายm_nameเข้า

  • ผ่านrvalueจะย้ายเข้าไปnameแล้วจะย้ายm_nameเข้าไป


/* (2) */ 
Creature(const std::string &name) : m_name{name} { }
Creature(std::string &&rname) : m_name{std::move(rname)} { }
  • ผ่านlvalueกระหม่อมnameแล้วจะถูกคัดลอกm_nameลงใน

  • ผ่านrvalueกระหม่อมrnameแล้วจะย้ายm_nameเข้าไป


เนื่องจากการดำเนินการย้ายมักจะเร็วกว่าการทำสำเนา(1)จึงดีกว่า(0)หากคุณส่งผ่านจังหวะจำนวนมาก (2)เหมาะสมที่สุดในแง่ของการคัดลอก / การเคลื่อนไหว แต่ต้องใช้รหัสซ้ำ

สามารถหลีกเลี่ยงการซ้ำรหัสได้ด้วยการส่งต่อที่สมบูรณ์แบบ :

/* (3) */
template <typename T,
          std::enable_if_t<
              std::is_convertible_v<std::remove_cvref_t<T>, std::string>, 
          int> = 0
         >
Creature(T&& name) : m_name{std::forward<T>(name)} { }

คุณอาจเลือกที่จะ จำกัดTเพื่อ จำกัด โดเมนของประเภทที่ตัวสร้างนี้สามารถสร้างอินสแตนซ์ได้ (ดังที่แสดงด้านบน) C ++ 20 จุดมุ่งหมายเพื่อลดความซับซ้อนนี้กับแนวคิด


ใน C ++ 17 ค่าprจะได้รับผลกระทบจากการคัดลอกที่รับประกันซึ่งจะลดจำนวนสำเนา / ย้ายเมื่อส่งอาร์กิวเมนต์ไปยังฟังก์ชัน


สำหรับ (1) pr-value และ xvalue case ไม่เหมือนกันเนื่องจาก c ++ 17 ไม่ใช่?
Oliv

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

@ โอลิฟใช่ xvalues ​​จำเป็นต้องมีการย้ายในขณะที่ prvalues ​​สามารถถูกจุดไข่ปลาได้ :)
Rakete1111

1
เราเขียน: Creature(const std::string &name) : m_name{std::move(name)} { }ใน(2) ได้ไหม
skytree

4
@skytree: คุณไม่สามารถย้ายจากวัตถุ const ได้เนื่องจากการเคลื่อนย้ายทำให้แหล่งที่มากลายพันธุ์ ที่จะคอมไพล์ แต่จะทำสำเนา
Vittorio Romeo

1

วิธีที่คุณส่งผ่านไม่ใช่ตัวแปรเดียวที่นี่สิ่งที่คุณผ่านทำให้เกิดความแตกต่างอย่างมากระหว่างสอง

ใน C ++ เรามีทุกชนิดของหมวดมูลค่าและนี่ "สำนวน" ที่มีอยู่สำหรับกรณีที่คุณผ่านในrvalue (เช่น"Alex-string-literal-that-constructs-temporary-std::string"หรือstd::move(nameString)) ซึ่งผลลัพธ์ใน0 สำเนาของstd::stringการทำ (ชนิดไม่ได้จะต้องมีการคัดลอก constructible สำหรับอาร์กิวเมนต์ rvalue) และใช้std::stringตัวสร้างการย้ายของเท่านั้น

คำถามและคำตอบที่เกี่ยวข้อง


1

การอ้างอิง pass-by-value-and-move มีข้อเสียหลายประการ:

  • มันทำให้เกิด 3 วัตถุแทนที่จะเป็น 2;
  • การส่งผ่านวัตถุด้วยค่าอาจนำไปสู่ค่าใช้จ่ายสแต็กพิเศษเนื่องจากแม้คลาสสตริงปกติจะมีขนาดใหญ่กว่าตัวชี้อย่างน้อย 3 หรือ 4 เท่า
  • การสร้างวัตถุอาร์กิวเมนต์จะทำในฝั่งผู้โทรทำให้โค้ดขยายตัว

คุณช่วยอธิบายได้ไหมว่าทำไมมันถึงทำให้วัตถุ 3 ชิ้นเกิด? จากสิ่งที่ฉันเข้าใจฉันสามารถส่ง "ปีเตอร์" เป็นสตริง สิ่งนี้จะเกิดขึ้นคัดลอกแล้วย้ายไปใช่หรือไม่? และจะไม่ใช้สแต็กในบางจุดหรือไม่? ไม่ได้อยู่ที่จุดของการเรียกตัวสร้าง แต่อยู่ในm_name{name}ส่วนที่ถูกคัดลอก?
Blackbot

@Blackbot ฉันอ้างถึงตัวอย่างของคุณstd::string nameString("Alex"); Creature c(nameString);ออบเจ็กต์หนึ่งคือnameStringอีกอันคืออาร์กิวเมนต์ของฟังก์ชันและอันที่สามคือฟิลด์คลาส
user7860670

0

ในกรณีของฉันการเปลี่ยนเป็น pass by value จากนั้นทำการ std: move ทำให้เกิดข้อผิดพลาด heap-use-after-free ใน Address Sanitizer

https://travis-ci.org/github/acgetchell/CDT-plusplus/jobs/679520360#L3165

ดังนั้นฉันจึงปิดมันไปแล้วเช่นเดียวกับข้อเสนอแนะในเสียงดัง

https://github.com/acgetchell/CDT-plusplus/compare/80c96789f0a2...0d78fd63b332

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