รหัสจาก“ ภาษาโปรแกรม C ++” ฉบับที่ 4 ส่วน 36.3.6 มีลักษณะการทำงานที่กำหนดไว้อย่างชัดเจนหรือไม่


94

ใน Bjarne Stroustrup ของc ++ เขียนโปรแกรมภาษาส่วนฉบับที่ 4 36.3.6 STL เหมือนการดำเนินงานรหัสต่อไปนี้จะใช้เป็นตัวอย่างของการผูกมัด :

void f2()
{
    std::string s = "but I have heard it works even if you don't believe in it" ;
    s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" )
        .replace( s.find( " don't" ), 6, "" );

    assert( s == "I have heard it works only if you believe in it" ) ;
}

การยืนยันล้มเหลวในgcc( ดูสด ) และVisual Studio( ดูสด ) แต่ไม่ล้มเหลวเมื่อใช้เสียงดัง ( ดูสด )

เหตุใดฉันจึงได้รับผลลัพธ์ที่แตกต่างกัน คอมไพเลอร์ใด ๆ เหล่านี้ประเมินนิพจน์โซ่ไม่ถูกต้องหรือโค้ดนี้แสดงพฤติกรรมบางรูปแบบที่ไม่ระบุหรือไม่ได้กำหนดหรือไม่


Better:s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );
Ben Voigt

20
กันแมลงฉันเป็นคนเดียวที่คิดว่าโค้ดน่าเกลียดแบบนั้นไม่ควรอยู่ในหนังสือ?
Karoly Horvath

5
@KarolyHorvath โปรดทราบว่าcout << a << b << coperator<<(operator<<(operator<<(cout, a), b), c)น่าเกลียดเพียงเล็กน้อย
Oktalist

1
@Oktalist: :) อย่างน้อยฉันก็มีความตั้งใจที่นั่น มันสอนการค้นหาชื่อที่ขึ้นกับอาร์กิวเมนต์และไวยากรณ์ของตัวดำเนินการในเวลาเดียวกันในรูปแบบ terse ... และมันไม่ได้ให้ความรู้สึกที่คุณควรเขียนโค้ดแบบนั้นจริงๆ
Karoly Horvath

คำตอบ:


104

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

ตัวอย่างนี้กล่าวถึงในข้อเสนอN4228: การปรับแต่งลำดับการประเมินนิพจน์สำหรับ Idiomatic C ++ซึ่งกล่าวถึงโค้ดในคำถามต่อไปนี้:

[... ] โค้ดนี้ได้รับการตรวจสอบโดยผู้เชี่ยวชาญ C ++ ทั่วโลกและเผยแพร่ (The C ++ Programming Language, 4 th edition.) อย่างไรก็ตามความเสี่ยงต่อลำดับการประเมินที่ไม่ระบุได้ถูกค้นพบเมื่อเร็ว ๆ นี้โดยเครื่องมือ [.. .]

รายละเอียด

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

เมื่อมองแวบแรกอาจดูเหมือนว่าเนื่องจากแต่ละreplaceรายการต้องได้รับการประเมินจากซ้ายไปขวาจึงต้องประเมินกลุ่มอาร์กิวเมนต์ของฟังก์ชันที่เกี่ยวข้องเป็นกลุ่มจากซ้ายไปขวาเช่นกัน

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

s.find( "even" )

และ:

s.find( " don't" )

ซึ่งเรียงลำดับอย่างไม่แน่นอนเกี่ยวกับ:

s.replace(0, 4, "" )

ทั้งสองfindสายจะได้รับการประเมินก่อนหรือหลังreplaceซึ่งเป็นเรื่องสำคัญเพราะมันมีผลข้างเคียงในการsในทางที่จะแก้ไขผลมาจากการที่จะเปลี่ยนความยาวของfind sดังนั้นขึ้นอยู่กับว่าเมื่อใดที่replaceได้รับการประเมินเทียบกับการfindเรียกสองครั้งผลลัพธ์จะแตกต่างกัน

หากเราดูที่นิพจน์โซ่และตรวจสอบลำดับการประเมินของนิพจน์ย่อยบางส่วน:

s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" )
^ ^       ^  ^  ^    ^        ^                 ^  ^
A B       |  |  |    C        |                 |  |
          1  2  3             4                 5  6

และ:

.replace( s.find( " don't" ), 6, "" );
 ^        ^                   ^  ^
 D        |                   |  |
          7                   8  9

โปรดทราบว่าเรากำลังเพิกเฉยต่อความจริงที่ว่า4และ7สามารถแยกย่อยออกเป็นนิพจน์ย่อยเพิ่มเติมได้ ดังนั้น:

  • Aเป็นลำดับก่อนหน้าBซึ่งเรียงตามลำดับก่อนหน้าCซึ่งจะเรียงลำดับก่อนหลังD
  • 1จะ9เรียงลำดับอย่างไม่แน่นอนเมื่อเทียบกับนิพจน์ย่อยอื่น ๆ โดยมีข้อยกเว้นบางประการที่ระบุไว้ด้านล่าง
    • 1ที่จะ3มีการลำดับขั้นตอนก่อนB
    • 4ที่จะ6มีการลำดับขั้นตอนก่อนC
    • 7ที่จะ9มีการลำดับขั้นตอนก่อนD

กุญแจสำคัญของปัญหานี้คือ:

  • 4จะ9เรียงตามลำดับอย่างไม่แน่นอนด้วยความเคารพB

การสั่งซื้อที่มีศักยภาพของทางเลือกสำหรับการประเมินผล4และการ7ที่เกี่ยวกับการBอธิบายความแตกต่างในผลระหว่างclangและเมื่อมีการประเมินgcc f2()ในการทดสอบของฉันclangประเมินBก่อนที่จะประเมิน4และ7ในขณะที่gccประเมินหลังจากนั้น เราสามารถใช้โปรแกรมทดสอบต่อไปนี้เพื่อสาธิตสิ่งที่เกิดขึ้นในแต่ละกรณี:

#include <iostream>
#include <string>

std::string::size_type my_find( std::string s, const char *cs )
{
    std::string::size_type pos = s.find( cs ) ;
    std::cout << "position " << cs << " found in complete expression: "
        << pos << std::endl ;

    return pos ;
}

int main()
{
   std::string s = "but I have heard it works even if you don't believe in it" ;
   std::string copy_s = s ;

   std::cout << "position of even before s.replace(0, 4, \"\" ): " 
         << s.find( "even" ) << std::endl ;
   std::cout << "position of  don't before s.replace(0, 4, \"\" ): " 
         << s.find( " don't" ) << std::endl << std::endl;

   copy_s.replace(0, 4, "" ) ;

   std::cout << "position of even after s.replace(0, 4, \"\" ): " 
         << copy_s.find( "even" ) << std::endl ;
   std::cout << "position of  don't after s.replace(0, 4, \"\" ): "
         << copy_s.find( " don't" ) << std::endl << std::endl;

   s.replace(0, 4, "" ).replace( my_find( s, "even" ) , 4, "only" )
        .replace( my_find( s, " don't" ), 6, "" );

   std::cout << "Result: " << s << std::endl ;
}

ผลการค้นหาgcc( ดูสด )

position of even before s.replace(0, 4, "" ): 26
position of  don't before s.replace(0, 4, "" ): 37

position of even after s.replace(0, 4, "" ): 22
position of  don't after s.replace(0, 4, "" ): 33

position  don't found in complete expression: 37
position even found in complete expression: 26

Result: I have heard it works evenonlyyou donieve in it

ผลการค้นหาclang( ดูสด ):

position of even before s.replace(0, 4, "" ): 26
position of  don't before s.replace(0, 4, "" ): 37

position of even after s.replace(0, 4, "" ): 22
position of  don't after s.replace(0, 4, "" ): 33

position even found in complete expression: 22
position don't found in complete expression: 33

Result: I have heard it works only if you believe in it

ผลการค้นหาVisual Studio( ดูสด ):

position of even before s.replace(0, 4, "" ): 26
position of  don't before s.replace(0, 4, "" ): 37

position of even after s.replace(0, 4, "" ): 22
position of  don't after s.replace(0, 4, "" ): 33

position  don't found in complete expression: 37
position even found in complete expression: 26
Result: I have heard it works evenonlyyou donieve in it

รายละเอียดจากมาตรฐาน

เราทราบดีว่าเว้นแต่จะระบุไว้การประเมินของนิพจน์ย่อยจะไม่ได้รับผลจากการดำเนินการของโปรแกรมส่วนมาตรฐาน C ++ 11ซึ่งระบุว่า:1.9

ยกเว้นในกรณีที่ระบุไว้การประเมินตัวถูกดำเนินการของแต่ละตัวดำเนินการและนิพจน์ย่อยของนิพจน์แต่ละรายการจะไม่ได้รับผลกระทบ [... ]

และเรารู้ว่าการเรียกใช้ฟังก์ชันแนะนำลำดับก่อนความสัมพันธ์ของฟังก์ชันเรียกนิพจน์ postfix และอาร์กิวเมนต์ที่เกี่ยวข้องกับเนื้อความของฟังก์ชันจากส่วน1.9:

[... ] เมื่อเรียกใช้ฟังก์ชัน (ไม่ว่าฟังก์ชันจะอยู่ในบรรทัดหรือไม่ก็ตาม) การคำนวณค่าและผลข้างเคียงที่เกี่ยวข้องกับนิพจน์อาร์กิวเมนต์ใด ๆ หรือด้วยนิพจน์ postfix ที่กำหนดฟังก์ชันที่เรียกว่าจะเรียงลำดับก่อนดำเนินการของทุกนิพจน์หรือทุกคำสั่ง ในร่างกายของฟังก์ชันที่เรียกว่า [... ]

เรายังทราบด้วยว่าการเข้าถึงของสมาชิกชั้นเรียนดังนั้นการเชื่อมโยงจะประเมินจากซ้ายไปขวาจากส่วน5.2.5 การเข้าถึงของสมาชิกชั้นเรียนซึ่งระบุว่า:

[... ] นิพจน์ postfix ก่อนที่จะประเมินจุดหรือลูกศร; 64 ผลลัพธ์ของการประเมินพร้อมกับ id-expression จะกำหนดผลลัพธ์ของนิพจน์ postfix ทั้งหมด

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

postfix-expression:
    postfix-expression ( expression-listopt)       // function call
    postfix-expression . templateopt id-expression // Class member access, ends
                                                   // up as a postfix-expression

การเปลี่ยนแปลง C ++ 17

ข้อเสนอp0145r3: การปรับแต่งลำดับการประเมินนิพจน์สำหรับ Idiomatic C ++ได้ทำการเปลี่ยนแปลงหลายอย่าง รวมถึงการเปลี่ยนแปลงที่ให้รหัสพฤติกรรมที่ระบุไว้อย่างดีโดยการเสริมสร้างคำสั่งของกฎการประเมินผลสำหรับpostfix แสดงออกของพวกเขาและการแสดงออกของรายการ

[expr.call] p5พูดว่า:

postfix แสดงออกเป็นลำดับขั้นตอนก่อนที่จะแสดงออกในการแสดงออกของรายการและข้อโต้แย้งใด การกำหนดค่าเริ่มต้นของพารามิเตอร์รวมถึงการคำนวณค่าที่เกี่ยวข้องและผลข้างเคียงทั้งหมดจะเรียงตามลำดับอย่างไม่แน่นอนเมื่อเทียบกับพารามิเตอร์อื่น ๆ [หมายเหตุ: ผลข้างเคียงทั้งหมดของการประเมินอาร์กิวเมนต์จะเรียงตามลำดับก่อนที่จะป้อนฟังก์ชัน (ดู 4.6) —end note] [ตัวอย่าง:

void f() {
std::string s = "but I have heard it works even if you don’t believe in it";
s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find(" don’t"), 6, "");
assert(s == "I have heard it works only if you believe in it"); // OK
}

- ส่งตัวอย่าง]


7
ฉันประหลาดใจเล็กน้อยที่เห็นว่า "ผู้เชี่ยวชาญหลายคน" มองข้ามปัญหานี้เป็นที่ทราบกันดีว่าการประเมินpostfix-expressionของการเรียกฟังก์ชันไม่ได้เรียงตามลำดับก่อนที่จะประเมินอาร์กิวเมนต์ (ใน C และ C ++ ทุกเวอร์ชัน)
MM

@ShafikYaghmour การเรียกใช้ฟังก์ชันจะเรียงลำดับอย่างไม่แน่นอนโดยเกี่ยวเนื่องกันและอื่น ๆ ยกเว้นความสัมพันธ์ที่เรียงตามลำดับก่อนหน้าที่คุณสังเกตเห็น อย่างไรก็ตามการประเมินผลของ 1, 2, 3, 5, 6, 8, 9, "even", "don't"และหลายกรณีของการsเป็นญาติ unsequenced กับแต่ละอื่น ๆ
TC

4
@TC ไม่มันไม่ใช่ (ซึ่งเป็น "จุดบกพร่อง" นี้เกิดขึ้นได้อย่างไร เช่นfoo().func( bar() )มันสามารถเรียกได้ทั้งก่อนหรือหลังการโทรfoo() postfix แสดงออกคือ ข้อโต้แย้งและ postfix แสดงออกจะติดใจก่อนที่ร่างของแต่ญาติ unsequenced กับแต่ละอื่น ๆ bar()foo().funcfunc()
MM

@MattMcNabb อ่าใช่ฉันอ่านผิด คุณกำลังพูดถึงpostfix-expressionมากกว่าการโทร ใช่ถูกต้องแล้วพวกเขาจะไม่ได้รับผลกระทบใด ๆ (เว้นแต่จะใช้กฎอื่น ๆ )
TC

6
นอกจากนี้ยังมีปัจจัยที่มีแนวโน้มว่าโค้ดที่ปรากฏในหนังสือ B.Stroustrup นั้นถูกต้องไม่เช่นนั้นจะมีคนสังเกตเห็นแน่นอน! (ที่เกี่ยวข้องผู้ใช้ SO ยังคงพบข้อผิดพลาดใหม่ ๆ ใน K&R)
MM

4

สิ่งนี้มีวัตถุประสงค์เพื่อเพิ่มข้อมูลในเรื่องที่เกี่ยวข้องกับ C ++ 17 ข้อเสนอ ( ลำดับการประเมินนิพจน์การปรับแต่งสำหรับ Idiomatic C ++ Revision 2 ) สำหรับC++17การแก้ไขปัญหาที่อ้างถึงรหัสข้างต้นเป็นเพียงตัวอย่าง

ตามที่แนะนำฉันได้เพิ่มข้อมูลที่เกี่ยวข้องจากข้อเสนอและเสนอราคา (ไฮไลต์ของฉัน):

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

พิจารณาส่วนของโปรแกรมต่อไปนี้:

void f()
{
  std::string s = "but I have heard it works even if you don't believe in it"
  s.replace(0, 4, "").replace(s.find("even"), 4, "only")
      .replace(s.find(" don't"), 6, "");
  assert(s == "I have heard it works only if you believe in it");
}

การยืนยันควรจะตรวจสอบผลลัพธ์ที่ต้องการของโปรแกรมเมอร์ มันใช้ "การผูกมัด" ของการเรียกฟังก์ชันสมาชิกซึ่งเป็นแนวทางปฏิบัติมาตรฐานทั่วไป โค้ดนี้ได้รับการตรวจสอบโดยผู้เชี่ยวชาญ C ++ ทั่วโลกและเผยแพร่ (The C ++ Programming Language, 4th edition.) อย่างไรก็ตามความเสี่ยงต่อลำดับการประเมินที่ไม่ระบุได้ถูกค้นพบโดยเครื่องมือเมื่อไม่นานมานี้

บทความนี้แนะนำให้เปลี่ยนC++17กฎเบื้องต้นเกี่ยวกับลำดับการประเมินการแสดงออกซึ่งได้รับอิทธิพลCและมีมานานกว่าสามทศวรรษ เสนอว่าภาษาควรรับประกันสำนวนร่วมสมัยหรือความเสี่ยง"กับดักและแหล่งที่มาของข้อบกพร่องที่คลุมเครือยากต่อการค้นหา"เช่นสิ่งที่เกิดขึ้นกับตัวอย่างโค้ดด้านบน

ข้อเสนอC++17คือต้องการให้ทุกนิพจน์มีลำดับการประเมินที่กำหนดไว้อย่างชัดเจน :

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

โค้ดข้างต้นรวบรวมโดยใช้GCC 7.1.1และClang 4.0.0.

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