วันของการส่ง const std :: string & เป็นพารามิเตอร์มากกว่าหรือไม่


604

ผมได้ยินพูดคุยล่าสุดโดย Sutter สมุนไพรที่ชี้ให้เห็นว่าเหตุผลที่จะผ่านstd::vectorและstd::stringโดยconst &ที่หายไปส่วนใหญ่ เขาแนะนำว่าการเขียนฟังก์ชั่นเช่นตอนนี้ดีกว่า

std::string do_something ( std::string inval )
{
   std::string return_val;
   // ... do stuff ...
   return return_val;
}

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

มีใครอธิบายได้บ้างว่าทำไมสมุนไพรถึงพูดแบบนี้?


89
ผมคิดว่าคำตอบที่ดีที่สุดที่จะเป็นคำถามที่น่าจะเป็นที่จะอ่านเดฟอับราฮัมของบทความเกี่ยวกับเรื่อง c ++ ถัดไป ฉันจะเพิ่มว่าฉันไม่เห็นอะไรเกี่ยวกับสิ่งนี้ที่มีคุณสมบัติเป็นนอกหัวข้อหรือไม่สร้างสรรค์ มันเป็นคำถามที่ชัดเจนเกี่ยวกับการเขียนโปรแกรมซึ่งมีคำตอบจริง
Jerry Coffin

สิ่งที่น่าสนใจดังนั้นหากคุณจะต้องทำสำเนาอยู่ดีการส่งผ่านค่าจะเร็วกว่าการอ้างอิงผ่าน
Benj

3
@Sz ฉันมีความละเอียดอ่อนต่อคำถามที่ถูกจัดหมวดหมู่เป็นเท็จและปิด ฉันจำรายละเอียดของคดีนี้ไม่ได้และไม่ได้ตรวจสอบอีกครั้ง แต่ฉันเพียงแค่จะลบความคิดเห็นของฉันบนสมมติฐานที่ว่าฉันทำผิด ขอขอบคุณที่แจ้งเรื่องนี้ให้ฉันทราบ
Howard Hinnant

2
@HowardHinnant ขอบคุณมากมันเป็นช่วงเวลาที่มีค่าเสมอเมื่อเราพบกับความใส่ใจและความรู้สึกในระดับนี้มันสดชื่นมาก! (ฉันจะลบของฉันแล้วแน่นอน)
Sz

คำตอบ:


393

เหตุผลที่ Herb พูดในสิ่งที่เขาพูดนั้นเป็นเพราะเรื่องแบบนี้

สมมติว่าผมมีฟังก์ชั่นAที่เรียกฟังก์ชั่นที่เรียกฟังก์ชั่นB CและAผ่านสตริงผ่านและเข้าสู่B ไม่ทราบหรือดูแลเกี่ยวกับ; ทั้งหมดรู้เกี่ยวกับการเป็น นั่นคือเป็นรายละเอียดการดำเนินงานของCACABCB

สมมติว่า A ถูกนิยามดังนี้:

void A()
{
  B("value");
}

หาก B และ C รับสายอักขระจากconst&นั้นจะมีลักษณะดังนี้:

void B(const std::string &str)
{
  C(str);
}

void C(const std::string &str)
{
  //Do something with `str`. Does not store it.
}

ทุกอย่างดีและดี คุณเพิ่งผ่านพอยน์เตอร์ไปรอบ ๆ ไม่มีการคัดลอกไม่มีการเคลื่อนไหวความสุขของทุกคน Cใช้เวลาconst&เพราะมันไม่ได้เก็บสตริง มันใช้มัน

ตอนนี้ฉันต้องการเปลี่ยนแปลงเพียงครั้งเดียว: Cต้องการเก็บสตริงไว้ที่ใดที่หนึ่ง

void C(const std::string &str)
{
  //Do something with `str`.
  m_str = str;
}

สวัสดีคัดลอกคอนสตรัคเตอร์และการจัดสรรหน่วยความจำที่เป็นไปได้ (ละเว้นShort String Optimization (SSO) ) ซีแมนทิกส์การย้ายของ C ++ 11 ควรทำให้สามารถลบการสร้างสำเนาโดยไม่จำเป็นใช่ไหม? และAผ่านชั่วคราว ไม่มีเหตุผลว่าทำไมCต้องคัดลอกข้อมูล มันควรจะหลีกเลี่ยงสิ่งที่ได้รับ

ยกเว้นมันจะทำไม่ได้ const&เพราะมันต้องใช้เวลา

ถ้าฉันเปลี่ยนCพารามิเตอร์ตามพารามิเตอร์นั่นเป็นสาเหตุBให้ทำการคัดลอกลงในพารามิเตอร์นั้น ฉันไม่ได้อะไรเลย

ดังนั้นถ้าฉันเพิ่งส่งผ่านstrค่าผ่านฟังก์ชั่นทั้งหมดโดยอาศัยstd::moveการสับเปลี่ยนข้อมูลเราจะไม่มีปัญหานี้ หากมีคนต้องการที่จะยึดมั่นในมันพวกเขาสามารถ ถ้าพวกเขาทำไม่ได้

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

มันขึ้นอยู่กับกรณีการใช้งานของคุณ คุณเกลียดการจัดสรรหน่วยความจำมากแค่ไหน


2
เมื่อคุณบอกว่าการย้ายเข้าสู่ค่านั้นมีราคาแพงกว่าการใช้การอ้างอิงซึ่งยังคงมีราคาแพงกว่าด้วยจำนวนคงที่
Neil G

3
@NeilG: คุณเข้าใจไหมว่า "การพึ่งพาการใช้งาน" หมายถึงอะไร สิ่งที่คุณกำลังพูดนั้นผิดเพราะขึ้นอยู่กับว่ามีการนำ SSO ไปใช้อย่างไรและอย่างไร
ildjarn

17
@ildjarn: ในการวิเคราะห์คำสั่งถ้ากรณีที่เลวร้ายที่สุดของบางสิ่งบางอย่างที่ถูกผูกไว้กับค่าคงที่แล้วมันก็ยังคงเวลา ไม่ใช่สายเล็กที่ยาวที่สุด? สตริงนั้นใช้เวลาในการคัดลอกอย่างต่อเนื่องหรือไม่? ไม่ใช่สายอักขระขนาดเล็กทั้งหมดใช้เวลาในการคัดลอกน้อยลงใช่ไหม จากนั้นการคัดลอกสตริงสำหรับสตริงขนาดเล็กคือ "เวลาคงที่" ในการวิเคราะห์คำสั่ง - แม้ว่าสตริงขนาดเล็กจะใช้เวลาในการคัดลอกต่างกัน การวิเคราะห์คำสั่งเกี่ยวข้องพฤติกรรม asymptotic
Neil G

8
@ NeilG: แน่นอน แต่คำถามเดิมของคุณคือ "ที่ยังคงมีราคาแพงมากขึ้นตามจำนวนเงินที่คงที่ (อิสระจากความยาวของสตริงที่ถูกย้าย) ใช่มั้ย? " จุดฉันพยายามที่จะทำคือมันอาจจะแพงมากขึ้นโดยการที่แตกต่างกันจำนวนคงที่ขึ้นอยู่กับความยาวของสตริงซึ่งจะรวมเป็น "ไม่"
ildjarn

13
เหตุใดสตริงจึงmovedมาจาก B ถึง C ในกรณีตามค่า ถ้า B เป็นB(std::string b)และ C เป็นC(std::string c)แล้วทั้งเรามีการโทรC(std::move(b))ใน B หรือbมีการยังคงไม่เปลี่ยนแปลง (เช่น 'ไม่ไหวติงจาก') Bจนกระทั่งออก (บางทีคอมไพเลอร์เพิ่มประสิทธิภาพจะย้ายสายใต้ตามกฎถ้าหากbไม่ได้ใช้หลังจากที่โทร แต่ฉันไม่คิดว่าจะมีการรับประกันที่แข็งแกร่ง.) เช่นเดียวกับที่เป็นจริงสำหรับสำเนาของการstr m_strแม้ว่าฟังก์ชั่นพารามิเตอร์จะเริ่มต้นด้วย rvalue มันเป็น lvalue ภายในฟังก์ชั่นและstd::moveจำเป็นต้องย้ายจาก lvalue นั้น
Pixelchemist

163

วันของการส่ง const std :: string & เป็นพารามิเตอร์มากกว่าหรือไม่

ไม่ หลายคนใช้คำแนะนำนี้ (รวมถึง Dave Abrahams) นอกเหนือจากโดเมนที่ใช้และทำให้มันง่ายขึ้นเพื่อนำไปใช้กับพารามิเตอร์ทั้งหมด std::string - การส่งผ่านค่าเสมอstd::stringไม่ใช่ "วิธีปฏิบัติที่ดีที่สุด" สำหรับพารามิเตอร์และแอปพลิเคชันใด ๆ การพูดคุย / บทความมุ่งเน้นไปที่ใช้เฉพาะกับชุดกรณีที่จำกัด

หากคุณส่งคืนค่าการกลายพันธุ์พารามิเตอร์หรือรับค่าการผ่านค่าอาจช่วยประหยัดการคัดลอกที่มีราคาแพงและให้ความสะดวกในการสร้างประโยค

ในฐานะที่เคยผ่านการอ้างอิง const บันทึกการคัดลอกมากเมื่อคุณไม่จำเป็นต้องสำเนา

ตอนนี้เป็นตัวอย่างเฉพาะ:

อย่างไรก็ตาม inval ยังคงมีขนาดใหญ่กว่าขนาดของการอ้างอิงค่อนข้างมาก (ซึ่งโดยทั่วไปจะใช้เป็นตัวชี้) นี่เป็นเพราะสตริง std :: มีองค์ประกอบต่าง ๆ รวมถึงตัวชี้ไปที่ฮีปและสมาชิกถ่าน [] สำหรับการปรับสตริงให้สั้น ดังนั้นสำหรับฉันแล้วการอ้างอิงโดยผ่านยังคงเป็นความคิดที่ดี มีใครอธิบายได้บ้างว่าทำไมสมุนไพรถึงพูดแบบนี้?

หากขนาดสแต็กเป็นเรื่องที่น่ากังวล (และสมมติว่านี่ไม่ใช่อินไลน์ / ปรับให้เหมาะสม), return_val+ inval>return_val - IOW การใช้งานสแต็กสูงสุดสามารถลดลงได้โดยการส่งผ่านค่าที่นี่ (หมายเหตุ: oversimplification ของ ABIs) ในขณะเดียวกันการส่งผ่านการอ้างอิง const สามารถปิดใช้งานการเพิ่มประสิทธิภาพได้ เหตุผลหลักที่นี่ไม่ใช่เพื่อหลีกเลี่ยงการเติบโตของสแต็ก แต่เพื่อให้แน่ใจว่าสามารถดำเนินการเพิ่มประสิทธิภาพได้ในกรณีที่สามารถใช้งานได้

วันที่ผ่านไปโดยอ้างอิง const ไม่เกิน - กฎเพียงซับซ้อนกว่าพวกเขาเคย หากประสิทธิภาพเป็นสิ่งสำคัญคุณควรพิจารณาว่าคุณจะผ่านประเภทเหล่านี้อย่างไรโดยขึ้นอยู่กับรายละเอียดที่คุณใช้ในการนำไปใช้งาน


3
ในการใช้งานสแต็ค ABIs ทั่วไปจะส่งการอ้างอิงเดียวในการลงทะเบียนโดยไม่มีการใช้สแต็ก
ahcox

63

สิ่งนี้ขึ้นอยู่กับการนำไปใช้ของคอมไพเลอร์

อย่างไรก็ตามมันก็ขึ้นอยู่กับสิ่งที่คุณใช้

ให้พิจารณาฟังก์ชั่นต่อไป:

bool foo1( const std::string v )
{
  return v.empty();
}
bool foo2( const std::string & v )
{
  return v.empty();
}

ฟังก์ชั่นเหล่านี้มีการใช้งานในหน่วยการรวบรวมที่แยกต่างหากเพื่อหลีกเลี่ยงการอินไลน์ จากนั้น:
1. ถ้าคุณส่งสัญลักษณไปยังทั้งสองฟังก์ชั่นคุณจะไม่เห็นความแตกต่างในการแสดงมากนัก ในทั้งสองกรณีวัตถุสตริงจะต้องถูกสร้างขึ้น
2 ถ้าคุณส่งวัตถุ std :: string อื่นfoo2จะดีกว่าfoo1เพราะfoo1จะทำสำเนาลึก

บนพีซีของฉันโดยใช้ g ++ 4.6.1 ฉันได้ผลลัพธ์เหล่านี้:

  • ตัวแปรโดยการอ้างอิง: การทำซ้ำ 1000000000 ครั้ง -> เวลาที่ผ่านไป: 2.25912 วินาที
  • ตัวแปรตามค่า: การวนซ้ำ 1000000000 ครั้ง -> เวลาที่ผ่านไป: 27.2259 วินาที
  • ตามตัวอักษรโดยการอ้างอิง: การซ้ำ 100000000 -> เวลาที่ผ่านไป: 9.10319 วินาที
  • ตามตัวอักษรตามค่า: การวนซ้ำ 100000000 -> เวลาที่ผ่านไป: 8.62659 วินาที

4
สิ่งที่เกี่ยวข้องมากขึ้นคือสิ่งที่เกิดขึ้นภายในฟังก์ชั่น: หากเรียกด้วยการอ้างอิงจะต้องทำสำเนาภายในที่สามารถตัดออกได้เมื่อส่งผ่านค่าหรือไม่
leftaroundabout

1
@leftaroundabout ใช่แน่นอน ฉันสันนิษฐานว่าทั้งสองฟังก์ชั่นกำลังทำสิ่งเดียวกัน
BЈовић

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

3
สำหรับฉัน "มันขึ้นอยู่กับสิ่งที่คุณทำในฟังก์ชั่น" คือการออกแบบที่ไม่ดี - เนื่องจากคุณกำลังใช้ลายเซ็นของฟังก์ชั่นบน internals ของการใช้งาน
Hans Olsson

1
อาจเป็นตัวพิมพ์ผิด แต่การกำหนดเวลาสองตัวสุดท้ายมีลูปน้อยลง 10 เท่า
TankorSmash

54

คำตอบสั้น ๆ : ไม่! คำตอบยาว:

  • ถ้าคุณจะไม่ปรับเปลี่ยนสตริง (รักษาเป็นแบบอ่านอย่างเดียว) const ref&ผ่านเป็น
    (ความconst ref&ชัดจะต้องอยู่ในขอบเขตในขณะที่ฟังก์ชั่นที่ใช้มันทำงาน)
  • หากคุณวางแผนที่จะดัดแปลงมันหรือคุณรู้ว่ามันจะออกนอกขอบเขต(เธรด)ให้ส่งมันเป็น a valueอย่าคัดลอกส่วนconst ref&ภายในของฟังก์ชัน

มีการโพสต์บนcpp-next.comชื่อ"ต้องการความเร็วผ่านตามตัวอักษร!" . TL; DR:

คำแนะนำ : อย่าคัดลอกอาร์กิวเมนต์ของฟังก์ชัน ให้ส่งพวกเขาด้วยค่าและให้คอมไพเลอร์ทำการคัดลอกแทน

การแปลของ ^

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

ดังนั้นอย่าทำสิ่งนี้ :

std::string function(const std::string& aString){
    auto vString(aString);
    vString.clear();
    return vString;
}

ทำสิ่งนี้ :

std::string function(std::string aString){
    aString.clear();
    return aString;
}

เมื่อคุณต้องการแก้ไขค่าอาร์กิวเมนต์ในร่างกายของคุณทำงาน

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


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

3
@KeithThompson อย่าคัดลอกอาร์กิวเมนต์ฟังก์ชันของคุณ หมายความว่าอย่าคัดลอกconst ref&ไปยังตัวแปรภายในเพื่อแก้ไข หากคุณต้องการแก้ไขมัน ... ทำให้พารามิเตอร์เป็นค่า มันค่อนข้างชัดเจนสำหรับตัวฉันที่ไม่ได้พูดภาษาอังกฤษ
CodeAngry

5
@KeithThompson The Guideline quote (อย่าคัดลอกอาร์กิวเมนต์ฟังก์ชันของคุณ แต่ให้ส่งค่าและให้คอมไพเลอร์ทำการคัดลอก) คัดลอกจากหน้านั้น หากยังไม่ชัดเจนพอฉันอดไม่ได้ ฉันไม่ไว้วางใจคอมไพเลอร์อย่างเต็มที่เพื่อสร้างทางเลือกที่ดีที่สุด ฉันค่อนข้างชัดเจนเกี่ยวกับความตั้งใจของฉันในวิธีที่ฉันกำหนดอาร์กิวเมนต์ของฟังก์ชัน # 1 const ref&ถ้ามันอ่านอย่างเดียวมันเป็น # 2 ถ้าฉันต้องการเขียนหรือรู้ว่ามันอยู่นอกขอบเขต ... ฉันใช้ค่า # 3 ref&ถ้าผมจำเป็นต้องปรับเปลี่ยนค่าเดิมที่ฉันผ่าน # 4 ผมใช้pointers *ถ้าอาร์กิวเมนต์เป็นตัวเลือกเพื่อให้สามารถnullptrมัน
CodeAngry

11
ฉันไม่ได้เข้าข้างคำถามว่าจะส่งผ่านค่าหรืออ้างอิง ประเด็นของฉันคือคุณสนับสนุนการส่งต่อโดยอ้างอิงในบางกรณี แต่จากนั้นอ้างถึง (ดูเหมือนว่าจะสนับสนุนตำแหน่งของคุณ) แนวทางที่แนะนำให้ส่งผ่านคุณค่าโดยตลอด หากคุณไม่เห็นด้วยกับแนวทางคุณอาจต้องการพูดอย่างนั้นและอธิบายว่าทำไม (ลิงก์ไปยัง cpp-next.com ใช้งานไม่ได้สำหรับฉัน)
Keith Thompson

4
@ KeithThompson: คุณถอดความผิดแนวทาง ไม่ผ่านค่า "เสมอ" เพื่อสรุปมันเป็น "ถ้าคุณจะทำสำเนาโลคัลใช้ pass by value เพื่อให้คอมไพเลอร์ดำเนินการคัดลอกนั้นสำหรับคุณ" มันไม่ได้บอกว่าจะใช้ pass-by-value เมื่อคุณไม่ได้ทำสำเนา
Ben Voigt

43

const &ถ้าคุณต้องการจริงคัดลอกก็ยังคงที่เหมาะสมที่จะใช้ ตัวอย่างเช่น:

bool isprint(std::string const &s) {
    return all_of(begin(s),end(s),(bool(*)(char))isprint);
}

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

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


ในระยะสั้น C ++ 11 ไม่ได้เปลี่ยนแปลงอะไรเลยในเรื่องนี้ยกเว้นคนที่ไม่ไว้ใจการคัดลอก


2
โดยทั่วไปแล้วตัวสร้างการเคลื่อนย้ายจะถูกนำไปใช้กับnoexceptแต่การคัดลอกตัวสร้างไม่ได้
leftaroundabout

25

เกือบจะ

ใน C ++ 17 เรามีbasic_string_view<?>ซึ่งนำเราไปสู่กรณีการใช้งานที่แคบสำหรับstd::string const&พารามิเตอร์

การมีอยู่ของซีแมนทิกส์ย้ายได้กำจัดกรณีการใช้งานหนึ่งกรณีstd::string const&- หากคุณวางแผนที่จะเก็บพารามิเตอร์การใช้std::stringค่าตามค่าจะเหมาะสมที่สุดเนื่องจากคุณไม่สามารถmoveใช้พารามิเตอร์ได้

หากมีคนเรียกฟังก์ชันของคุณด้วย raw C "string"นี่หมายความว่าstd::stringมีการจัดสรรบัฟเฟอร์เพียงหนึ่งครั้งเท่านั้นซึ่งต่างกับสองstd::string const&กรณี

อย่างไรก็ตามหากคุณไม่ต้องการทำสำเนาการถ่ายโดยstd::string const&ยังคงมีประโยชน์ใน C ++ 14

ด้วยstd::string_viewตราบใดที่คุณไม่ได้ผ่านสายอักขระดังกล่าวไปยัง API ที่คาดว่า'\0'บัฟเฟอร์ตัวอักษรสไตล์ C จะสามารถทำงานได้อย่างมีประสิทธิภาพมากขึ้นstd::stringโดยไม่ต้องเสี่ยงกับการจัดสรรใด ๆ สตริง C ดิบสามารถเปลี่ยนเป็น a std::string_viewโดยไม่มีการจัดสรรหรือการคัดลอกอักขระ

ณ จุดนั้นการใช้งานstd::string const&คือเมื่อคุณไม่ได้คัดลอกข้อมูลขายส่งและกำลังจะส่งผ่านไปยัง API แบบ C ซึ่งคาดว่าบัฟเฟอร์ที่ยกเลิกเป็นโมฆะและคุณต้องการฟังก์ชันสตริงระดับสูงกว่าที่std::stringมีให้ ในทางปฏิบัตินี่เป็นข้อกำหนดที่หายาก


2
ฉันซาบซึ้งกับคำตอบนี้ - แต่ฉันต้องการชี้ให้เห็นว่ามันต้องทนทุกข์ทรมาน (เป็นคำตอบที่มีคุณภาพมาก) จากอคติเล็กน้อยโดเมน หากต้องการปัญญา: "ในทางปฏิบัตินี่เป็นข้อกำหนดที่หายาก" ... ในประสบการณ์การพัฒนาของฉันเองข้อ จำกัด เหล่านี้ - ซึ่งดูเหมือนแคบผิดปกติกับผู้เขียน - จะได้พบกันอย่างแท้จริงตลอดเวลา มันคุ้มที่จะชี้ให้เห็น
fish2000

1
@ fish2000 ต้องมีความชัดเจนสำหรับstd::stringการครองคุณไม่เพียงแค่ต้องบางส่วนของความต้องการเหล่านั้น แต่ทั้งหมดของพวกเขา คนใดคนหนึ่งหรือสองคนคือคนที่ฉันยอมรับ บางทีคุณอาจต้องการทั้ง 3 ข้อ (เช่นคุณกำลังแยกวิเคราะห์อาร์กิวเมนต์สตริงเพื่อเลือก C API ที่คุณจะส่งไปให้?)
Yakk - Adam Nevraumont

@ Yakk-AdamNevraumont มันเป็นสิ่งที่ YMMV - แต่มันเป็นกรณีการใช้งานบ่อยครั้งถ้าหาก (พูดว่า) คุณกำลังเขียนโปรแกรมกับ POSIX หรือ API อื่น ๆ ที่ความหมาย C-string เป็นตัวหารร่วมที่ต่ำที่สุด ฉันควรพูดจริงๆว่าฉันรักstd::string_view- ตามที่คุณชี้ให้เห็น“ สตริง C ดิบสามารถเปลี่ยนเป็น std :: string_view โดยไม่ต้องจัดสรรหรือคัดลอกอักขระ” ซึ่งเป็นสิ่งที่ควรค่าแก่ผู้ที่ใช้ C ++ ในบริบทของ การใช้งาน API ดังกล่าวแน่นอน
fish2000

1
@ fish2000 "'สตริง C ดิบสามารถเปลี่ยนเป็น std :: string_view ได้โดยไม่ต้องจัดสรรหรือคัดลอกอักขระ' ซึ่งเป็นสิ่งที่ควรค่าแก่การจดจำ" แน่นอน แต่มันทำให้ส่วนที่ดีที่สุดออกมา - ในกรณีที่สตริงดิบเป็นตัวอักษรสตริงมันไม่จำเป็นต้องมี strlen runtime () !
Don Hatch

17

std::stringไม่ได้เป็นข้อมูลเก่าธรรมดา (POD)และขนาดที่แท้จริงของมันไม่ได้เป็นสิ่งที่เกี่ยวข้องมากที่สุดเท่าที่เคยมีมา ตัวอย่างเช่นถ้าคุณส่งผ่านสตริงที่อยู่เหนือความยาวของ SSO และจัดสรรบนฮีปฉันคาดว่าตัวสร้างการคัดลอกจะไม่คัดลอกที่เก็บข้อมูล SSO

เหตุผลที่แนะนำคือเนื่องจากinvalสร้างจากนิพจน์อาร์กิวเมนต์ดังนั้นจึงถูกย้ายหรือคัดลอกเสมอตามความเหมาะสม - ไม่มีการสูญเสียประสิทธิภาพสมมติว่าคุณต้องการความเป็นเจ้าของอาร์กิวเมนต์ ถ้าคุณทำไม่ได้การconstอ้างอิงอาจเป็นวิธีที่ดีกว่า


2
จุดที่น่าสนใจเกี่ยวกับตัวสร้างการคัดลอกนั้นฉลาดพอที่จะไม่ต้องกังวลกับ SSO หากไม่ได้ใช้มัน อาจจะถูกต้องฉันจะต้องตรวจสอบว่าเป็นเรื่องจริง ;-) #
34911

3
@Benj: ความคิดเห็นเก่าที่ฉันรู้ แต่ถ้า SSO มีขนาดเล็กพอที่จะคัดลอกโดยไม่มีเงื่อนไขจะเร็วกว่าการทำสาขาตามเงื่อนไข ตัวอย่างเช่น 64 ไบต์เป็นสายแคชและสามารถคัดลอกได้ในระยะเวลาเล็กน้อย อาจ 8 รอบหรือน้อยกว่าใน x86_64
Zan Lynx

แม้ว่า SSO จะไม่ถูกคัดลอกโดยตัวสร้างการคัดลอก แต่std::string<>เป็น 32 ไบต์ที่จัดสรรจากสแต็กซึ่ง 16 รายการนั้นจำเป็นต้องเริ่มต้น เปรียบเทียบสิ่งนี้กับการจัดสรรและกำหนดค่าเริ่มต้นเพียง 8 ไบต์สำหรับการอ้างอิง: เป็นจำนวน CPU ที่ใช้งานเป็นสองเท่าและใช้พื้นที่แคชมากถึงสี่เท่าซึ่งไม่สามารถใช้กับข้อมูลอื่นได้
cmaster - คืนสถานะโมนิกา

โอ้และฉันลืมที่จะพูดคุยเกี่ยวกับการส่งผ่านอาร์กิวเมนต์ของฟังก์ชันในรีจิสเตอร์; ที่จะนำการใช้งานสแต็คของการอ้างอิงลงไปที่ศูนย์สำหรับ callee ล่าสุด ...
cmaster - reinstate monica

16

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

นี่คือรหัสสำหรับวัดสิ่งที่ถูกถาม:

#include <iostream>

struct string
{
    string() {}
    string(const string&) {std::cout << "string(const string&)\n";}
    string& operator=(const string&) {std::cout << "string& operator=(const string&)\n";return *this;}
#if (__has_feature(cxx_rvalue_references))
    string(string&&) {std::cout << "string(string&&)\n";}
    string& operator=(string&&) {std::cout << "string& operator=(string&&)\n";return *this;}
#endif

};

#if PROCESS == 1

string
do_something(string inval)
{
    // do stuff
    return inval;
}

#elif PROCESS == 2

string
do_something(const string& inval)
{
    string return_val = inval;
    // do stuff
    return return_val; 
}

#if (__has_feature(cxx_rvalue_references))

string
do_something(string&& inval)
{
    // do stuff
    return std::move(inval);
}

#endif

#endif

string source() {return string();}

int main()
{
    std::cout << "do_something with lvalue:\n\n";
    string x;
    string t = do_something(x);
#if (__has_feature(cxx_rvalue_references))
    std::cout << "\ndo_something with xvalue:\n\n";
    string u = do_something(std::move(x));
#endif
    std::cout << "\ndo_something with prvalue:\n\n";
    string v = do_something(source());
}

สำหรับฉันผลลัพธ์นี้:

$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=1 test.cpp
$ a.out
do_something with lvalue:

string(const string&)
string(string&&)

do_something with xvalue:

string(string&&)
string(string&&)

do_something with prvalue:

string(string&&)
$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=2 test.cpp
$ a.out
do_something with lvalue:

string(const string&)

do_something with xvalue:

string(string&&)

do_something with prvalue:

string(string&&)

ตารางด้านล่างสรุปผลลัพธ์ของฉัน (ใช้ clang -std = c ++ 11) หมายเลขแรกคือจำนวนของการสร้างสำเนาและหมายเลขที่สองคือจำนวนของการสร้างการย้าย:

+----+--------+--------+---------+
|    | lvalue | xvalue | prvalue |
+----+--------+--------+---------+
| p1 |  1/1   |  0/2   |   0/1   |
+----+--------+--------+---------+
| p2 |  1/0   |  0/1   |   0/1   |
+----+--------+--------+---------+

โซลูชัน pass-by-value ต้องการเพียงโอเวอร์โหลดเดียว แต่มีค่าใช้จ่ายในการเคลื่อนย้ายแบบพิเศษเมื่อผ่าน lvalues ​​และ xvalues สิ่งนี้อาจหรืออาจไม่เป็นที่ยอมรับสำหรับสถานการณ์ใด ๆ ก็ตาม โซลูชันทั้งสองมีข้อดีและข้อเสีย


1
std :: string เป็นคลาสไลบรารีมาตรฐาน มันสามารถเคลื่อนย้ายและคัดลอกได้อยู่แล้ว ฉันไม่เห็นว่าสิ่งนี้เกี่ยวข้องกันอย่างไร OP กำลังขอข้อมูลเพิ่มเติมเกี่ยวกับประสิทธิภาพของการย้ายกับการอ้างอิงไม่ใช่ประสิทธิภาพของการย้ายและการคัดลอก
Nicol Bolas

3
คำตอบนี้จะนับจำนวนการเคลื่อนไหวและคัดลอกสตริง std :: จะได้รับภายใต้การออกแบบแบบ pass-by-value ซึ่งอธิบายโดย Herb และ Dave ทั้งคู่เทียบกับการอ้างอิงโดยใช้ฟังก์ชันโอเวอร์โหลดคู่ ฉันใช้รหัสของ OP ในการสาธิตยกเว้นการแทนที่ด้วยสตริงจำลองเพื่อตะโกนเมื่อมีการคัดลอก / ย้าย
Howard Hinnant

คุณควรปรับโค้ดให้เหมาะสมก่อนทำการทดสอบ ...
The Paramagnetic Croissant

3
@TheParamagneticCroissant: คุณได้รับผลลัพธ์ที่แตกต่างกันอย่างไร ถ้าเป็นเช่นนั้นใช้คอมไพเลอร์ใดกับอาร์กิวเมนต์บรรทัดคำสั่งอะไร
Howard Hinnant

14

Herb Sutter ยังคงบันทึกอยู่พร้อมกับ Bjarne Stroustroup เพื่อแนะนำให้const std::string&เป็นประเภทพารามิเตอร์ ดูhttps://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rf-in

มีข้อผิดพลาดที่ไม่ได้กล่าวถึงในคำตอบอื่น ๆ ที่นี่: ถ้าคุณส่งสตริงตัวอักษรไปยังconst std::string&พารามิเตอร์มันจะผ่านการอ้างอิงไปยังสตริงชั่วคราวที่สร้างขึ้นทันทีเพื่อเก็บอักขระของตัวอักษร หากคุณบันทึกการอ้างอิงนั้นจะไม่ถูกต้องเมื่อยกเลิกการจัดสรรสตริงชั่วคราว เพื่อความปลอดภัยคุณต้องบันทึกสำเนาไม่ใช่การอ้างอิง ปัญหาเกิดขึ้นจากข้อเท็จจริงที่ว่าตัวอักษรสตริงเป็นconst char[N]ประเภทต้องการการเลื่อนstd::stringระดับ

โค้ดข้างล่างนี้แสดงให้เห็นถึงอันตรายและวิธีแก้ปัญหาพร้อมกับตัวเลือกที่มีประสิทธิภาพเล็กน้อย - การบรรทุกเกินพิกัดด้วยconst char*วิธีการตามที่อธิบายไว้ที่จะมีวิธีที่จะผ่านตัวอักษรสตริงเป็นข้อมูลอ้างอิงใน C

(หมายเหตุ: Sutter & Stroustroup ให้คำแนะนำว่าหากคุณเก็บสำเนาของสตริงไว้ก็จะมีฟังก์ชั่นโอเวอร์โหลดพร้อมพารามิเตอร์ && และ std :: move ())

#include <string>
#include <iostream>
class WidgetBadRef {
public:
    WidgetBadRef(const std::string& s) : myStrRef(s)  // copy the reference...
    {}

    const std::string& myStrRef;    // might be a reference to a temporary (oops!)
};

class WidgetSafeCopy {
public:
    WidgetSafeCopy(const std::string& s) : myStrCopy(s)
            // constructor for string references; copy the string
    {std::cout << "const std::string& constructor\n";}

    WidgetSafeCopy(const char* cs) : myStrCopy(cs)
            // constructor for string literals (and char arrays);
            // for minor efficiency only;
            // create the std::string directly from the chars
    {std::cout << "const char * constructor\n";}

    const std::string myStrCopy;    // save a copy, not a reference!
};

int main() {
    WidgetBadRef w1("First string");
    WidgetSafeCopy w2("Second string"); // uses the const char* constructor, no temp string
    WidgetSafeCopy w3(w2.myStrCopy);    // uses the String reference constructor
    std::cout << w1.myStrRef << "\n";   // garbage out
    std::cout << w2.myStrCopy << "\n";  // OK
    std::cout << w3.myStrCopy << "\n";  // OK
}

เอาท์พุท:

const char * constructor
const std::string& constructor

Second string
Second string

นี่เป็นปัญหาที่แตกต่างและ WidgetBadRef ไม่จำเป็นต้องมี const & พารามิเตอร์ผิดพลาด คำถามคือถ้า WidgetSafeCopy เพิ่งใช้พารามิเตอร์สตริงมันจะช้าลง? (ฉันคิดว่าการคัดลอกชั่วคราวไปยังสมาชิกนั้นเป็นเรื่องง่ายกว่าที่จะเห็นได้อย่างแน่นอน)
Superfly Jon

โปรดทราบว่าT&&ไม่ใช่การอ้างอิงสากลเสมอไป ในความเป็นจริงstd::string&&จะเป็นข้อมูลอ้างอิง rvalue และไม่เคยมีการอ้างอิงสากลเพราะไม่มีการหักประเภทจะดำเนินการ ดังนั้นคำแนะนำของ Stroustroup & Sutter จึงไม่ขัดแย้งกับ Meyers '
Justin Time - Reinstate Monica

@ จัสตินไทม์: ขอบคุณ; ฉันลบประโยคสุดท้ายที่ไม่ถูกต้องโดยอ้างว่า std :: string && จะเป็นการอ้างอิงสากล
circlepi314

@ circlepi314 ยินดีต้อนรับ มันเป็นการผสมผสานที่ง่ายที่จะทำให้บางครั้งอาจสับสนได้ว่าสิ่งใดก็ตามที่ได้รับT&&เป็นการอ้างอิงสากลที่อนุมานหรือการอ้างอิงค่าที่ไม่อนุมาน มันอาจจะชัดเจนกว่านี้หากพวกเขาแนะนำสัญลักษณ์ที่แตกต่างกันสำหรับการอ้างอิงสากล (เช่น&&&การรวมกันของ&และ&&) แต่นั่นอาจจะดูโง่
Justin Time - Reinstate Monica

8

IMO ที่ใช้การอ้างอิง C ++ สำหรับstd::stringเป็นการเพิ่มประสิทธิภาพท้องถิ่นอย่างรวดเร็วและสั้นในขณะที่การส่งผ่านค่าอาจเป็นการเพิ่มประสิทธิภาพระดับโลกที่ดีขึ้น (หรือไม่)

ดังนั้นคำตอบคือ: ขึ้นอยู่กับสถานการณ์:

  1. หากคุณเขียนรหัสทั้งหมดจากภายนอกไปยังฟังก์ชั่นด้านในคุณรู้ว่าโค้ดทำอะไรคุณสามารถใช้การอ้างอิงconst std::string &ได้
  2. หากคุณเขียนรหัสห้องสมุดหรือใช้รหัสห้องสมุดที่มีการส่งผ่านสตริงคุณอาจได้รับประโยชน์มากขึ้นในระดับโลกโดยการเชื่อถือstd::stringพฤติกรรมของตัวสร้างสำเนา

6

ดู“ Herb Sutter” กลับไปสู่พื้นฐาน! สิ่งจำเป็นสำหรับสไตล์ C ++ สมัยใหม่”ในหัวข้ออื่น ๆ เขาได้ตรวจสอบคำแนะนำการส่งพารามิเตอร์ที่ได้รับในอดีตและแนวคิดใหม่ ๆ ที่มาพร้อมกับ C ++ 11 และดูเฉพาะ แนวคิดของการส่งสตริงตามค่า

สไลด์ 24

มาตรฐานแสดงให้เห็นว่าการส่งผ่านstd::stringค่าในกรณีที่ฟังก์ชั่นจะคัดลอกในต่อไปอาจช้าลงอย่างมีนัยสำคัญ!

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

ดูสไลด์ของเขา 27: สำหรับฟังก์ชั่น“ set” ตัวเลือก 1 จะเหมือนกันทุกประการ ตัวเลือกที่ 2 เพิ่มเกินพิกัดสำหรับการอ้างอิง rvalue แต่นี่จะเป็นการระเบิดแบบ combinatorial หากมีหลายพารามิเตอร์

มีไว้สำหรับพารามิเตอร์“ sink” เท่านั้นที่จะต้องสร้างสตริง (ไม่มีการเปลี่ยนแปลงค่าที่มีอยู่) ว่าเคล็ดลับ pass-by-value นั้นถูกต้อง นั่นคือผู้สร้างซึ่งพารามิเตอร์เริ่มต้นสมาชิกประเภทการจับคู่โดยตรง

หากคุณต้องการดูว่าคุณจะกังวลมากเพียงใดให้ดูการนำเสนอของ Nicolai Josuttisและขอให้โชคดีกับสิ่งนั้น ( “ สมบูรณ์แบบ - เสร็จสิ้น!” n ครั้งหลังจากพบความผิดกับรุ่นก่อนหน้าเคยอยู่ที่นั่นหรือไม่)


สรุปได้ว่า⧺F.15ในหลักเกณฑ์มาตรฐาน


3

ในฐานะที่ @ JDługoszชี้ให้เห็นในความคิดเห็นสมุนไพรให้คำแนะนำอื่น ๆ ในการพูดคุย (ดูภายหลัง) อีกดูจากที่นี่: https://youtu.be/xnqTKD8uD64?t=54m50s https://youtu.be/xnqTKD8uD64?t=54m50s

คำแนะนำของเขาจะลดลงเหลือเพียงการใช้พารามิเตอร์ค่าสำหรับฟังก์ชั่นfที่เรียกว่า sink อาร์กิวเมนต์โดยสมมติว่าคุณจะย้ายโครงสร้างจาก sink อาร์กิวเมนต์เหล่านี้

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

void f(T x) {
  T y{std::move(x)};
}

โทรfกับข้อโต้แย้ง lvalue จะส่งผลให้ตัวสร้างสำเนาถูกเรียกว่าการสร้างและย้ายคอนสตรัคถูกเรียกว่าการสร้างx yในทางกลับกันการเรียกfด้วยอาร์กิวเมนต์ rvalue จะทำให้ตัวสร้างการย้ายถูกเรียกเพื่อสร้างxและตัวสร้างการย้ายอื่นจะถูกเรียกให้สร้างyและย้ายคอนสตรัคอีกครั้งเพื่อให้ได้รับการเรียกตัวไปสร้าง

โดยทั่วไปการใช้งานที่ดีที่สุดของfอาร์กิวเมนต์ lvalue มีดังนี้:

void f(const T& x) {
  T y{x};
}

yในกรณีนี้เพียงหนึ่งตัวสร้างสำเนาที่เรียกว่าการสร้าง การใช้งานที่เหมาะสมที่สุดfสำหรับอาร์กิวเมนต์ rvalue คือโดยทั่วไปอีกครั้งดังนี้:

void f(T&& x) {
  T y{std::move(x)};
}

ในกรณีนี้คอนสตรัคเตอร์ย้ายเดียวเท่านั้นที่ถูกเรียกให้สร้าง yในกรณีนี้เพียงหนึ่งคอนสตรัคย้ายที่เรียกว่าการสร้าง

ดังนั้นการประนีประนอมที่สมเหตุสมผลคือการใช้พารามิเตอร์ค่าและมีการเรียกตัวสร้างการย้ายพิเศษหนึ่งรายการสำหรับอาร์กิวเมนต์ lvalue หรือ rvalue ที่เกี่ยวข้องกับการใช้งานที่เหมาะสมซึ่งเป็นคำแนะนำที่ได้รับในการพูดคุยของ Herb

ในฐานะที่เป็น @ JDługoszชี้ให้เห็นในความคิดเห็นที่ผ่านค่าตามความเหมาะสมเท่านั้นสำหรับฟังก์ชั่นที่จะสร้างวัตถุบางอย่างจากการโต้แย้งอ่าง เมื่อคุณมีฟังก์ชั่นfที่คัดลอกอาร์กิวเมนต์ของมันวิธีการ pass-by-value จะมีค่าใช้จ่ายมากกว่าวิธีการอ้างอิง pass-by-const ทั่วไป วิธีการแบบ pass-by-value สำหรับฟังก์ชั่นfที่เก็บสำเนาของพารามิเตอร์จะมีรูปแบบ:

void f(T x) {
  T y{...};
  ...
  y = std::move(x);
}

ในกรณีนี้มีการสร้างสำเนาและการกำหนดย้ายสำหรับอาร์กิวเมนต์ lvalue และการสร้างการย้ายและการกำหนดย้ายสำหรับอาร์กิวเมนต์ rvalue กรณีที่ดีที่สุดสำหรับอาร์กิวเมนต์ lvalue คือ:

void f(const T& x) {
  T y{...};
  ...
  y = x;
}

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

สำหรับอาร์กิวเมนต์ rvalue การใช้งานที่ดีที่สุดสำหรับการfเก็บสำเนามีรูปแบบ:

void f(T&& x) {
  T y{...};
  ...
  y = std::move(x);
}

ดังนั้นเฉพาะการย้ายที่ทำในกรณีนี้ การส่งผ่านค่า rvalue ไปยังเวอร์ชันfที่ใช้การอ้างอิงแบบ const จะคิดค่าใช้จ่ายเฉพาะการมอบหมายแทนการโอนย้าย ค่อนข้างจะพูดรุ่นของfการอ้างอิง const ในกรณีนี้เนื่องจากการใช้งานทั่วไปจะดีกว่า

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


ดูการค้นพบของ Herb Sutter ในคำตอบใหม่ของฉัน: ทำอย่างนั้นเมื่อคุณย้ายสิ่งก่อสร้างเท่านั้นไม่ย้ายการมอบหมาย
JDługosz

1
@ JDługoszขอบคุณสำหรับตัวชี้ไปที่การพูดคุยของ Herb ฉันเพิ่งดูและแก้ไขคำตอบของฉันทั้งหมด ฉันไม่ทราบคำแนะนำการย้าย (ย้าย)
Ton van den Heuvel

รูปและคำแนะนำนั้นอยู่ในเอกสารแนวทางมาตรฐานตอนนี้
JDługosz

1

ปัญหาคือ "const" เป็นตัวระบุที่ไม่ละเอียด สิ่งที่มักจะมีความหมายโดย "อ้างอิงสตริง const" คือ "ไม่แก้ไขสตริงนี้" ไม่ใช่ "ไม่แก้ไขจำนวนการอ้างอิง" ไม่มีทางที่จะพูดได้ใน C ++ที่สมาชิกเป็น "const" พวกเขาทั้งหมดเป็นหรือไม่มีพวกเขา

เพื่อแฮ็คปัญหาภาษานี้ STL อาจอนุญาตให้ "C ()" ในตัวอย่างของคุณทำสำเนาความหมายแบบย้ายต่อไปและไม่สนใจ "const" ที่เกี่ยวข้องกับจำนวนการอ้างอิง (ไม่แน่นอน) ตราบใดที่มันมีการระบุที่ดี

เนื่องจาก STL ไม่มีฉันมีรุ่นของสตริงที่ const_casts <> เก็บตัวนับการอ้างอิง (ไม่มีวิธีย้อนกลับทำสิ่งที่ไม่แน่นอนในลำดับชั้นของคลาสย้อนหลัง) และ - แท้จริงและเห็น - คุณสามารถส่ง cmstring ของเป็นการอ้างอิง const ได้อย่างอิสระ และทำสำเนาของพวกเขาในหน้าที่ลึกตลอดทั้งวันโดยไม่มีการรั่วไหลหรือปัญหา

เนื่องจาก C ++ ไม่มี "gran constity class const ที่ได้มา" ที่นี่การเขียนข้อกำหนดที่ดีและการสร้างวัตถุ "สตริงที่เคลื่อนย้ายได้ const" (cmstring) ใหม่เป็นวิธีที่ดีที่สุดที่ฉันเคยเห็น


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