นี่เป็นข้อผิดพลาดที่ทราบของ C ++ 11 สำหรับลูปหรือไม่?


89

สมมติว่าเรามีโครงสร้างสำหรับการถือครอง 3 คู่พร้อมฟังก์ชันสมาชิกบางอย่าง:

struct Vector {
  double x, y, z;
  // ...
  Vector &negate() {
    x = -x; y = -y; z = -z;
    return *this;
  }
  Vector &normalize() {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  // ...
};

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

Vector v = ...;
v.normalize().negate();

หรือแม้กระทั่ง:

Vector v = Vector{1., 2., 3.}.normalize().negate();

ตอนนี้ถ้าเราให้ฟังก์ชั่นเริ่มต้น () และ end () เราสามารถใช้เวกเตอร์ของเราในรูปแบบใหม่สำหรับการวนซ้ำโดยพูดว่าจะวนรอบพิกัด 3 x, y และ z (คุณไม่ต้องสงสัยเลยว่าสร้างตัวอย่างที่ "มีประโยชน์" มากขึ้น โดยการแทนที่ Vector ด้วยเช่น String):

Vector v = ...;
for (double x : v) { ... }

เราสามารถทำได้:

Vector v = ...;
for (double x : v.normalize().negate()) { ... }

และนอกจากนี้ยังมี:

for (double x : Vector{1., 2., 3.}) { ... }

อย่างไรก็ตามสิ่งต่อไปนี้ (ดูเหมือนสำหรับฉัน) เสีย:

for (double x : Vector{1., 2., 3.}.normalize()) { ... }

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

  • สิ่งนี้ถูกต้องและเป็นที่ชื่นชมอย่างกว้างขวางหรือไม่?
  • ส่วนไหนของข้างต้นเป็นส่วนที่ "ไม่ดี" ที่ควรหลีกเลี่ยง?
  • ภาษาจะได้รับการปรับปรุงโดยการเปลี่ยนนิยามของ range-based for loop เช่นที่ temporaries ที่สร้างใน for-expression มีอยู่ตลอดระยะเวลาของลูปหรือไม่

ด้วยเหตุผลบางอย่างฉันจำคำถามที่คล้ายกันมากที่เคยถามมาก่อนลืมไปแล้วว่ามันเรียกว่าอะไร
Pubby

ฉันถือว่านี่เป็นข้อบกพร่องทางภาษา อายุการใช้งานของชั่วขณะไม่ได้ขยายไปยังเนื้อความทั้งหมดของ for-loop แต่สำหรับการตั้งค่าสำหรับลูปเท่านั้น ไม่ใช่แค่ไวยากรณ์ช่วงที่ทนทุกข์ทรมานเท่านั้น แต่ไวยากรณ์แบบคลาสสิกก็ทำได้เช่นกัน ในความคิดของฉันอายุการใช้งานของชั่วขณะในคำสั่ง init ควรจะขยายไปตลอดอายุการใช้งานของลูป
edA-qa mort-ora-y

1
@ edA-qamort-ora-y: ฉันมักจะยอมรับว่ามีข้อบกพร่องด้านภาษาเล็กน้อยแฝงตัวอยู่ที่นี่ แต่ฉันคิดว่าโดยเฉพาะอย่างยิ่งความจริงที่ว่าการขยายอายุการใช้งานเกิดขึ้นโดยปริยายเมื่อใดก็ตามที่คุณเชื่อมโยงการอ้างอิงชั่วคราวโดยตรงกับข้อมูลอ้างอิง แต่ไม่เกิดขึ้น สถานการณ์อื่น ๆ - ดูเหมือนว่าจะเป็นวิธีแก้ปัญหาแบบครึ่งๆกลางๆสำหรับปัญหาชีวิตชั่วคราวแม้ว่าจะไม่ได้บอกว่ามันชัดเจนว่าทางออกที่ดีกว่าจะเป็นอย่างไร บางทีอาจเป็นไวยากรณ์ 'ส่วนขยายอายุการใช้งาน' ที่ชัดเจนเมื่อสร้างชั่วคราวซึ่งทำให้มันอยู่ได้จนถึงจุดสิ้นสุดของบล็อกปัจจุบันคุณคิดว่าอย่างไร?
ndkrempel

@ edA-qamort-ora-y: ... จำนวนนี้เป็นสิ่งเดียวกับการผูกชั่วคราวกับการอ้างอิง แต่มีข้อได้เปรียบที่ชัดเจนมากขึ้นสำหรับผู้อ่านที่ 'การขยายอายุการใช้งาน' เกิดขึ้นแบบอินไลน์ (ในนิพจน์ แทนที่จะต้องมีการประกาศแยกต่างหาก) และไม่ต้องการให้คุณตั้งชื่อชั่วคราว
ndkrempel

1
เป็นไปได้ที่จะทำซ้ำวัตถุชั่วคราวในช่วงอิงสำหรับ
ildjarn

คำตอบ:


64

สิ่งนี้ถูกต้องและเป็นที่ชื่นชมอย่างกว้างขวางหรือไม่?

ใช่ความเข้าใจของคุณถูกต้อง

ส่วนไหนของข้างต้นเป็นส่วนที่ "ไม่ดี" ที่ควรหลีกเลี่ยง?

ส่วนที่ไม่ดีคือการอ้างอิงค่า l ไปยังการส่งคืนชั่วคราวจากฟังก์ชันและผูกเข้ากับการอ้างอิงค่า r มันแย่พอ ๆ กับสิ่งนี้:

auto &&t = Vector{1., 2., 3.}.normalize();

Vector{1., 2., 3.}ไม่สามารถขยายอายุการใช้งานชั่วคราวได้เนื่องจากคอมไพลเลอร์ไม่ทราบว่าค่าที่ส่งคืนจากnormalizeการอ้างอิงนั้น

ภาษาจะได้รับการปรับปรุงโดยการเปลี่ยนนิยามของ range-based for loop เช่นที่ temporaries ที่สร้างใน for-expression มีอยู่ตลอดระยะเวลาของลูปหรือไม่

ซึ่งจะไม่สอดคล้องอย่างมากกับวิธีการทำงานของ C ++

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

วิธีแก้ปัญหาที่สมเหตุสมผลกว่ามากจะเป็นวิธีแจ้งคอมไพลเลอร์ว่าค่าที่ส่งคืนของฟังก์ชันเป็นข้อมูลอ้างอิงเสมอthisดังนั้นหากค่าที่ส่งคืนถูกผูกไว้กับโครงสร้างที่ขยายชั่วคราวก็จะขยายชั่วคราวที่ถูกต้อง นั่นเป็นวิธีแก้ปัญหาระดับภาษา

ในปัจจุบัน (หากคอมไพเลอร์รองรับ) คุณสามารถสร้างมันขึ้นมาเพื่อที่normalize ไม่สามารถเรียกใช้งานชั่วคราวได้:

struct Vector {
  double x, y, z;
  // ...
  Vector &normalize() & {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  Vector &normalize() && = delete;
};

สิ่งนี้จะทำให้เกิดVector{1., 2., 3.}.normalize()ข้อผิดพลาดในการคอมไพล์ในขณะที่v.normalize()ทำงานได้ดี เห็นได้ชัดว่าคุณจะไม่สามารถแก้ไขสิ่งต่างๆเช่นนี้ได้:

Vector t = Vector{1., 2., 3.}.normalize();

แต่คุณจะไม่สามารถทำสิ่งที่ไม่ถูกต้องได้เช่นกัน

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

struct Vector {
  double x, y, z;
  // ...
  Vector &normalize() & {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  Vector normalize() && {
     Vector ret = *this;
     ret.normalize();
     return ret;
  }
};

หากVectorเป็นประเภทที่มีทรัพยากรจริงให้ย้ายคุณสามารถใช้Vector ret = std::move(*this);แทนได้ การเพิ่มประสิทธิภาพมูลค่าผลตอบแทนที่ตั้งชื่อทำให้สิ่งนี้เหมาะสมที่สุดในแง่ของประสิทธิภาพ


1
สิ่งที่อาจทำให้เป็น "gotcha" มากขึ้นก็คือสิ่งใหม่สำหรับลูปนั้นซ่อนความจริงที่ว่าการเชื่อมโยงการอ้างอิงเกิดขึ้นภายใต้ฝาครอบกล่าวคือมีความชัดเจนน้อยกว่าตัวอย่างที่ "แย่พอ ๆ กัน" ด้านบน นั่นเป็นเหตุผลว่าทำไมจึงเป็นไปได้ที่จะแนะนำกฎการขยายอายุการใช้งานเพิ่มเติมสำหรับกฎใหม่สำหรับลูป
ndkrempel

1
@ndkrempel: ใช่ แต่ถ้าคุณกำลังจะนำเสนอคุณลักษณะภาษาเพื่อแก้ไขปัญหานี้ (และดังนั้นจึงต้องรอจนกระทั่ง 2017 อย่างน้อย) ฉันชอบว่ามันเป็นที่ครอบคลุมมากขึ้นซึ่งเป็นสิ่งที่สามารถแก้ปัญหาที่เกิดขึ้นส่วนขยายชั่วคราวทุกที่
Nicol Bolas

3
+1. ในแนวทางสุดท้ายแทนที่จะdeleteให้การดำเนินการทางเลือกที่คืนค่า rvalue: Vector normalize() && { normalize(); return std::move(*this); }(ฉันเชื่อว่าการเรียกไปที่normalizeภายในฟังก์ชันจะส่งไปยัง lvalue overload แต่มีคนตรวจสอบ :)
David Rodríguez - dribeas

3
ฉันไม่เคยเห็นวิธีนี้&/ &&คุณสมบัติของวิธีการนี้ นี่มาจาก C ++ 11 หรือนี่คือส่วนขยายคอมไพเลอร์ที่เป็นกรรมสิทธิ์ (อาจแพร่หลาย) บางส่วน ให้ความเป็นไปได้ในการขัดจังหวะ
Christian Rau

1
@ChristianRau: เป็นผลิตภัณฑ์ใหม่สำหรับ C ++ 11 และคล้ายคลึงกับคุณสมบัติ C ++ 03 "const" และ "volatile" ของฟังก์ชันสมาชิกที่ไม่คงที่เนื่องจากมีคุณสมบัติเป็น "this" ในบางแง่ อย่างไรก็ตาม g ++ 4.7.0 ไม่รองรับ
ndkrempel

25

สำหรับ (x คู่: เวกเตอร์ {1., 2. , 3.}. normalize ()) {... }

นั่นไม่ใช่ข้อ จำกัด ของภาษา แต่เป็นปัญหากับรหัสของคุณ การแสดงออกVector{1., 2., 3.}สร้างชั่วคราว แต่normalizeฟังก์ชั่นส่งกลับlvalue อ้างอิง เนื่องจากนิพจน์เป็นlvalueคอมไพลเลอร์จึงสันนิษฐานว่าอ็อบเจ็กต์จะยังมีชีวิตอยู่ แต่เนื่องจากเป็นการอ้างอิงถึงชั่วคราวอ็อบเจ็กต์จึงตายหลังจากที่นิพจน์แบบเต็มได้รับการประเมินดังนั้นคุณจึงเหลือการอ้างอิงที่ห้อยอยู่

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


1
การconstอ้างอิงจะขยายอายุการใช้งานของวัตถุในกรณีนี้หรือไม่
David Stone

5
ซึ่งจะทำลายความหมายที่ต้องการอย่างชัดเจนในnormalize()ฐานะฟังก์ชันการกลายพันธุ์บนวัตถุที่มีอยู่ ดังนั้นคำถาม ชั่วคราวนั้นมี "อายุการใช้งานที่ยาวนานขึ้น" เมื่อใช้เพื่อจุดประสงค์เฉพาะของการวนซ้ำไม่ใช่อย่างอื่นฉันคิดว่าเป็นความผิดพลาดที่ทำให้สับสน
Andy Ross

2
@AndyRoss: ทำไม? การผูกชั่วคราวใด ๆกับการอ้างอิงค่า r (หรือconst&) จะยืดอายุการใช้งาน
Nicol Bolas

2
@ndkrempel: ยังไม่ได้เป็นข้อ จำกัด Vector & r = Vector{1.,2.,3.}.normalize();ของช่วงที่ใช้สำหรับห่วงปัญหาเดียวกันจะมาถ้าคุณผูกไว้กับการอ้างอิง: การออกแบบของคุณมีข้อ จำกัด ดังกล่าวและนั่นหมายความว่าคุณยินดีที่จะตอบแทนด้วยมูลค่า (ซึ่งอาจสมเหตุสมผลในหลาย ๆ สถานการณ์และอื่น ๆ ด้วยการอ้างอิงค่า rvalueและการเคลื่อนย้าย ) หรืออื่น ๆ ที่คุณต้องจัดการปัญหาในสถานที่ โทร: สร้างตัวแปรที่เหมาะสมจากนั้นใช้ใน for loop โปรดทราบว่านิพจน์Vector v = Vector{1., 2., 3.}.normalize().negate();สร้างวัตถุสองชิ้น ...
David Rodríguez - dribeas

1
@ DavidRodríguez-dribeas: ปัญหาในการเชื่อมโยงกับ const-reference คือสิ่งนี้T const& f(T const&);ทำได้ดีอย่างสมบูรณ์ T const& t = f(T());เรียบร้อยดี แล้วในม ธ . อื่นคุณค้นพบสิ่งนั้นT const& f(T const& t) { return t; }และคุณจะร้องไห้ ... ถ้าoperator+ทำงานบนค่ามันเป็นความปลอดภัยมากขึ้น ; จากนั้นคอมไพเลอร์อาจปรับแต่งการคัดลอกให้เหมาะสม (ต้องการความเร็วหรือไม่ส่งผ่านค่า) แต่นั่นเป็นโบนัส การผูกแบบชั่วคราวเท่านั้นที่ฉันอนุญาตคือการเชื่อมโยงกับการอ้างอิงค่า r แต่ฟังก์ชันควรส่งคืนค่าเพื่อความปลอดภัยและพึ่งพา Copy Elision / Move Semantics
Matthieu M.

4

IMHO ตัวอย่างที่สองมีข้อบกพร่องอยู่แล้ว การที่ตัวดำเนินการปรับเปลี่ยนกลับมา*thisนั้นสะดวกในแบบที่คุณพูดถึงนั่นคืออนุญาตให้มีการเชื่อมโยงตัวปรับแต่ง มันสามารถนำมาใช้สำหรับการมอบเพียงผลของการปรับเปลี่ยน แต่การทำเช่นนี้ผิดพลาดได้ง่ายเพราะสามารถมองข้ามได้ง่าย ถ้าฉันเห็นสิ่งที่ชอบ

Vector v{1., 2., 3.};
auto foo = somefunction1(v, 17);
auto bar = somefunction2(true, v, 2, foo);
auto baz = somefunction3(bar.quun(v), 93.2, v.qwarv(foo));

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

auto normalized(Vector v) -> Vector {return v.normalize();}
auto negated(Vector v) -> Vector {return v.negate();}

จากนั้นเขียนลูป

for( double x : negated(normalized(v)) ) { ... }

และ

for( double x : normalized(Vector{1., 2., 3}) ) { ... }

นั่นคือ IMO อ่านได้ดีกว่าและปลอดภัยกว่า แน่นอนว่าต้องใช้สำเนาพิเศษอย่างไรก็ตามสำหรับข้อมูลที่จัดสรรแบบฮีปสิ่งนี้อาจทำได้ในการย้าย C ++ 11 ราคาถูก


ขอบคุณ. ตามปกติมีให้เลือกมากมาย สถานการณ์หนึ่งที่คำแนะนำของคุณอาจใช้ไม่ได้คือถ้า Vector เป็นอาร์เรย์ (ไม่ได้จัดสรรฮีป) จำนวน 1,000 คู่ การลดประสิทธิภาพการใช้งานง่ายและความปลอดภัยในการใช้งาน
ndkrempel

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