C ++ 17 มีการรับประกันลำดับการประเมินอะไรบ้าง


95

ผลกระทบของการโหวตใน การรับประกันลำดับการประเมิน C ++ 17 (P0145)ในรหัส C ++ ทั่วไป

สิ่งต่อไปนี้มีการเปลี่ยนแปลงอย่างไร

i = 1;
f(i++, i)

และ

std::cout << f() << f() << f();

หรือ

f(g(), h(), j());

เกี่ยวข้องกับลำดับการประเมินคำสั่งมอบหมายงานใน C ++และรหัสนี้จาก“ ภาษาโปรแกรม C ++” รุ่นที่ 4 ตอนที่ 36.3.6 มีพฤติกรรมที่กำหนดไว้อย่างชัดเจนหรือไม่? ซึ่งทั้งสองปิดด้วยกระดาษ อันแรกอาจเป็นตัวอย่างเพิ่มเติมที่ดีในคำตอบของคุณด้านล่าง
Shafik Yaghmour

นอกจากนี้ยังค่อนข้างที่เกี่ยวข้อง: C ++ การสั่งซื้อ 17 การประเมินผลกับการทำงานที่ผู้ประกอบการบรรทุกเกินพิกัด
dfrib

คำตอบ:


83

บางกรณีทั่วไปที่เพื่อการประเมินผลที่ได้รับเพื่อให้ห่างไกลที่ไม่ระบุC++17จะถูกกำหนดและถูกต้องด้วย ตอนนี้พฤติกรรมที่ไม่ได้กำหนดบางอย่างไม่ได้ระบุไว้

i = 1;
f(i++, i)

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

อย่างไรก็ตามการประเมินผลของแต่ละอาร์กิวเมนต์จำเป็นต้องดำเนินการอย่างสมบูรณ์โดยมีผลข้างเคียงทั้งหมดก่อนที่จะดำเนินการกับอาร์กิวเมนต์อื่น ๆ ดังนั้นคุณอาจได้รับf(1, 1)(อาร์กิวเมนต์ที่สองประเมินก่อน) หรือf(1, 2)(อาร์กิวเมนต์แรกได้รับการประเมินก่อน) แต่คุณจะไม่ได้รับf(2, 2)หรือสิ่งอื่นใดจากธรรมชาตินั้น

std::cout << f() << f() << f();

ไม่ได้ระบุไว้ แต่จะเข้ากันได้กับลำดับความสำคัญของตัวดำเนินการดังนั้นการประเมินครั้งแรกfจะมาก่อนในสตรีม (ตัวอย่างด้านล่าง)

f(g(), h(), j());

ยังคงมีลำดับการประเมินที่ไม่ระบุของ g, h และ j โปรดทราบว่าสำหรับgetf()(g(),h(),j())กฎgetf()จะระบุไว้ก่อนหน้าg, h, jนี้

นอกจากนี้โปรดสังเกตตัวอย่างต่อไปนี้จากข้อความข้อเสนอ:

 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, "");

ตัวอย่างมาจากThe C ++ Programming Language , 4th edition, Stroustrup และเคยเป็นลักษณะการทำงานที่ไม่ระบุ แต่ C ++ 17 จะทำงานได้ตามที่คาดไว้ มีปัญหาที่คล้ายกันกับฟังก์ชันที่กลับมาทำงานได้ ( .then( . . . ))

เป็นอีกตัวอย่างหนึ่งให้พิจารณาสิ่งต่อไปนี้:

#include <iostream>
#include <string>
#include <vector>
#include <cassert>

struct Speaker{
    int i =0;
    Speaker(std::vector<std::string> words) :words(words) {}
    std::vector<std::string> words;
    std::string operator()(){
        assert(words.size()>0);
        if(i==words.size()) i=0;
        // Pre-C++17 version:
        auto word = words[i] + (i+1==words.size()?"\n":",");
        ++i;
        return word;
        // Still not possible with C++17:
        // return words[i++] + (i==words.size()?"\n":",");

    }
};

int main() {
    auto spk = Speaker{{"All", "Work", "and", "no", "play"}};
    std::cout << spk() << spk() << spk() << spk() << spk() ;
}

ด้วย C ++ 14 และก่อนที่เราจะ (และจะ) ได้รับผลลัพธ์เช่น

play
no,and,Work,All,

แทน

All,work,and,no,play

โปรดทราบว่าข้างต้นมีผลเช่นเดียวกับ

(((((std::cout << spk()) << spk()) << spk()) << spk()) << spk()) ;

แต่ถึงกระนั้นก่อน C ++ 17 ก็ไม่มีการรับประกันว่าสายแรกจะเข้าสู่สตรีมก่อน

การอ้างอิง: จากข้อเสนอที่ยอมรับ :

นิพจน์ Postfix จะประเมินจากซ้ายไปขวา ซึ่งรวมถึงการเรียกฟังก์ชันและนิพจน์การเลือกสมาชิก

นิพจน์การมอบหมายจะประเมินจากขวาไปซ้าย ซึ่งรวมถึงการกำหนดแบบผสม

ตัวดำเนินการในการเปลี่ยนตัวดำเนินการจะประเมินจากซ้ายไปขวา โดยสรุปนิพจน์ต่อไปนี้ได้รับการประเมินตามลำดับ a แล้ว b ตามด้วย c จากนั้น d:

  1. a-> ข
  2. ก -> * ข
  3. ก (b1, b2, b3)
  4. b @ = a
  5. ก [b]
  6. ก << ข
  7. ก >> ข

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

แก้ไขหมายเหตุ:a(b1, b2, b3)คำตอบเดิมของฉันตีความผิด คำสั่งของb1, b2, b3ยังคงไม่ได้ระบุ (ขอบคุณ @KABoissonneault ผู้แสดงความคิดเห็นทั้งหมด)

อย่างไรก็ตาม (ตาม @Yakk ชี้ให้เห็น) และนี่คือสิ่งที่สำคัญ: แม้เมื่อb1, b2, b3มีการแสดงออกที่ไม่น่ารำคาญแต่ละของพวกเขาได้รับการประเมินอย่างสมบูรณ์และเชื่อมโยงกับฟังก์ชั่นพารามิเตอร์ที่เกี่ยวข้องก่อนที่คนอื่น ๆ จะเริ่มต้นที่จะได้รับการประเมิน มาตรฐานระบุไว้ดังนี้:

§5.2.2 - การเรียกใช้ฟังก์ชัน 5.2.2.4:

. . . postfix-expression เรียงตามลำดับก่อนหน้าแต่ละนิพจน์ในรายการนิพจน์และอาร์กิวเมนต์เริ่มต้นใด ๆ ทุกการคำนวณค่าและผลข้างเคียงที่เกี่ยวข้องกับการเริ่มต้นพารามิเตอร์และการเริ่มต้นเองจะถูกจัดลำดับก่อนการคำนวณค่าและผลข้างเคียงที่เกี่ยวข้องกับการเริ่มต้นของพารามิเตอร์ที่ตามมา

อย่างไรก็ตามหนึ่งในประโยคใหม่เหล่านี้หายไปจากร่าง GitHub :

การคำนวณค่าและผลข้างเคียงทั้งหมดที่เกี่ยวข้องกับการเริ่มต้นพารามิเตอร์และการเริ่มต้นเองจะถูกจัดลำดับก่อนการคำนวณค่าและผลข้างเคียงที่เกี่ยวข้องกับการเริ่มต้นของพารามิเตอร์ที่ตามมา

ตัวอย่างก็มี ช่วยแก้ปัญหาเก่าแก่หลายสิบปี ( ตามที่ Herb Sutter อธิบาย ) โดยมีข้อยกเว้นในเรื่องความปลอดภัย

f(std::unique_ptr<A> a, std::unique_ptr<B> b);

f(get_raw_a(), get_raw_a());

จะรั่วไหลหากสายget_raw_a()ใดสายหนึ่งโยนก่อนที่ตัวชี้ดิบอื่น ๆ จะเชื่อมโยงกับพารามิเตอร์ตัวชี้อัจฉริยะ

ตามที่ TC ชี้ให้เห็นตัวอย่างมีข้อบกพร่องเนื่องจากโครงสร้าง unique_ptr จากตัวชี้ดิบนั้นชัดเจนทำให้ไม่สามารถรวบรวมสิ่งนี้ได้ *

สังเกตคำถามคลาสสิกนี้ด้วย(ติดแท็กCไม่ใช่C ++ ):

int x=0;
x++ + ++x;

ยังไม่ได้กำหนด


1
"ข้อที่สองข้อเสนอย่อยแทนที่ลำดับการประเมินของการเรียกใช้ฟังก์ชันดังนี้: ฟังก์ชันจะได้รับการประเมินก่อนอาร์กิวเมนต์ทั้งหมด แต่คู่ของอาร์กิวเมนต์ใด ๆ (จากรายการอาร์กิวเมนต์) จะเรียงตามลำดับไม่แน่นอนซึ่งหมายความว่ามีการประเมินก่อนอีกข้อหนึ่ง แต่ ไม่ได้ระบุคำสั่งรับประกันว่าฟังก์ชันนี้ได้รับการประเมินก่อนอาร์กิวเมนต์ซึ่งสะท้อนให้เห็นถึงข้อเสนอแนะของสมาชิกบางคนของคณะทำงานหลัก "
Yakk - Adam Nevraumont

1
ฉันได้รับความประทับใจนั้นจากกระดาษที่บอกว่า "นิพจน์ต่อไปนี้ได้รับการประเมินตามลำดับaจากbนั้นcจึงแสดงd" และแสดงa(b1, b2, b3)ว่าbนิพจน์ทั้งหมดไม่จำเป็นต้องได้รับการประเมินตามลำดับใด ๆ (มิฉะนั้นจะเป็นa(b, c, d))
KABoissonneault

1
@KABoissoneault คุณถูกต้องและฉันได้อัปเดตคำตอบตามนั้น นอกจากนี้ทั้งหมด: คำพูดเป็นรูปแบบเวอร์ชัน 3 ซึ่งเป็นการโหวตในเวอร์ชันเท่าที่ฉันเข้าใจ
Johan Lundberg

2
@JohanLundberg มีอีกสิ่งหนึ่งจากกระดาษที่ฉันเชื่อว่าสำคัญ a(b1()(), b2()())สามารถสั่งซื้อb1()()และb2()()ในลำดับใด ๆ แต่ก็ไม่สามารถทำเช่นb1()นั้นb2()()แล้วb1()(): มันอาจจะไม่แทรกประหารชีวิต กล่าวโดยย่อว่า "8. ALTERNATE EVALUATION ORDER FOR FUNCTION CALLS" เป็นส่วนหนึ่งของการเปลี่ยนแปลงที่ได้รับอนุมัติ
Yakk - Adam Nevraumont

3
f(i++, i)ไม่ได้กำหนด ตอนนี้ยังไม่ระบุ ตัวอย่างสตริงของ Stroustrup อาจไม่ได้ระบุไม่ใช่ไม่ได้กำหนด `f (get_raw_a (), get_raw_a ());` จะไม่คอมไพล์เนื่องจากตัวunique_ptrสร้างที่เกี่ยวข้องนั้นชัดเจน สุดท้ายx++ + ++xไม่ได้กำหนดระยะเวลา
TC

44

ห้ามการแทรกสลับใน C ++ 17

ใน C ++ 14 สิ่งต่อไปนี้ไม่ปลอดภัย:

void foo(std::unique_ptr<A>, std::unique_ptr<B>);

foo(std::unique_ptr<A>(new A), std::unique_ptr<B>(new B));

มีการดำเนินการสี่อย่างที่เกิดขึ้นที่นี่ระหว่างการเรียกใช้ฟังก์ชัน

  1. new A
  2. unique_ptr<A> ผู้สร้าง
  3. new B
  4. unique_ptr<B> ผู้สร้าง

ลำดับของสิ่งเหล่านี้ไม่ได้ระบุไว้อย่างสมบูรณ์ดังนั้นการสั่งซื้อที่ถูกต้องสมบูรณ์คือ (1), (3), (2), (4) หากเลือกลำดับนี้และ (3) พ่นแสดงว่าหน่วยความจำจาก (1) รั่ว - เรายังไม่ได้เรียกใช้ (2) ซึ่งจะป้องกันการรั่วไหล


ใน C ++ 17 กฎใหม่ห้ามการแทรกสลับ จาก [intro.execution]:

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

มีเชิงอรรถของประโยคที่อ่านว่า:

กล่าวอีกนัยหนึ่งการดำเนินการของฟังก์ชันจะไม่แทรกสลับกัน

สิ่งนี้ทำให้เรามีสองคำสั่งที่ถูกต้อง: (1), (2), (3), (4) หรือ (3), (4), (1), (2) ไม่ระบุว่าจะสั่งซื้อใด แต่ทั้งสองอย่างนี้ปลอดภัย คำสั่งทั้งหมดที่ (1) (3) เกิดขึ้นก่อน (2) และ (4) เป็นสิ่งต้องห้ามในขณะนี้


1
กันเล็กน้อย แต่นี่เป็นสาเหตุหนึ่งของการเพิ่ม :: make_shared และ std :: make_shared ในภายหลัง (เหตุผลอื่นคือการจัดสรรน้อยลง + พื้นที่ที่ดีกว่า) ดูเหมือนว่าไม่มีแรงจูงใจด้านความปลอดภัย / การรั่วไหลของทรัพยากรอีกต่อไป ดูตัวอย่าง Code ที่ 3 boost.org/doc/libs/1_67_0/libs/smart_ptr/doc/html/… Edit and stackoverflow.com/a/48844115 , herbutter.com/2013/05/29/gotw-89-solution- ตัวชี้อัจฉริยะ
Max Barraclough

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

2

ฉันพบหมายเหตุเกี่ยวกับลำดับการประเมินนิพจน์:

  • ถามด่วน: เหตุใด c ++ จึงไม่มีลำดับที่ระบุสำหรับการประเมินอาร์กิวเมนต์ของฟังก์ชัน

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

  • ลำดับการประเมิน

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

  • นิพจน์ไม่ชัดเจน

    ภาษา C ++ ไม่รับประกันลำดับที่จะประเมินอาร์กิวเมนต์ของการเรียกใช้ฟังก์ชัน

ในP0145R3 การปรับลำดับการประเมินนิพจน์สำหรับ Idiomatic C ++ฉันพบ:

การคำนวณค่าและผลข้างเคียงที่เกี่ยวข้องของนิพจน์ postfix จะเรียงลำดับก่อนนิพจน์ในรายการนิพจน์ การเริ่มต้นของพารามิเตอร์ที่ประกาศจะเรียงตามลำดับอย่างไม่แน่นอนโดยไม่มีการแทรกสลับกัน

แต่ฉันไม่พบในมาตรฐานแทนที่จะพบในมาตรฐาน:

6.8.1.8 การดำเนินการตามลำดับ [intro.execution] นิพจน์ X ถูกกล่าวว่าจะเรียงลำดับก่อนนิพจน์ Y หากการคำนวณค่าทุกครั้งและผลข้างเคียงที่เกี่ยวข้องกับนิพจน์ X ถูกเรียงลำดับก่อนการคำนวณค่าทุกครั้งและผลข้างเคียงทั้งหมดที่เกี่ยวข้องกับนิพจน์ Y .

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

7.6.19.1 ตัวดำเนินการลูกน้ำ [expr.comma] คู่ของนิพจน์ที่คั่นด้วยลูกน้ำจะถูกประเมินจากซ้ายไปขวา ...

ดังนั้นฉันจึงเปรียบเทียบตามพฤติกรรมในสามคอมไพเลอร์สำหรับมาตรฐาน 14 และ 17 รหัสที่สำรวจคือ:

#include <iostream>

struct A
{
    A& addInt(int i)
    {
        std::cout << "add int: " << i << "\n";
        return *this;
    }

    A& addFloat(float i)
    {
        std::cout << "add float: " << i << "\n";
        return *this;
    }
};

int computeInt()
{
    std::cout << "compute int\n";
    return 0;
}

float computeFloat()
{
    std::cout << "compute float\n";
    return 1.0f;
}

void compute(float, int)
{
    std::cout << "compute\n";
}

int main()
{
    A a;
    a.addFloat(computeFloat()).addInt(computeInt());
    std::cout << "Function call:\n";
    compute(computeFloat(), computeInt());
}

ผลลัพธ์ (ยิ่งสอดคล้องมากขึ้นคือเสียงดัง):

<style type="text/css">
  .tg {
    border-collapse: collapse;
    border-spacing: 0;
    border-color: #aaa;
  }
  
  .tg td {
    font-family: Arial, sans-serif;
    font-size: 14px;
    padding: 10px 5px;
    border-style: solid;
    border-width: 1px;
    overflow: hidden;
    word-break: normal;
    border-color: #aaa;
    color: #333;
    background-color: #fff;
  }
  
  .tg th {
    font-family: Arial, sans-serif;
    font-size: 14px;
    font-weight: normal;
    padding: 10px 5px;
    border-style: solid;
    border-width: 1px;
    overflow: hidden;
    word-break: normal;
    border-color: #aaa;
    color: #fff;
    background-color: #f38630;
  }
  
  .tg .tg-0pky {
    border-color: inherit;
    text-align: left;
    vertical-align: top
  }
  
  .tg .tg-fymr {
    font-weight: bold;
    border-color: inherit;
    text-align: left;
    vertical-align: top
  }
</style>
<table class="tg">
  <tr>
    <th class="tg-0pky"></th>
    <th class="tg-fymr">C++14</th>
    <th class="tg-fymr">C++17</th>
  </tr>
  <tr>
    <td class="tg-fymr"><br>gcc 9.0.1<br></td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
  </tr>
  <tr>
    <td class="tg-fymr">clang 9</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute float<br>compute int<br>compute</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute float<br>compute int<br>compute</td>
  </tr>
  <tr>
    <td class="tg-fymr">msvs 2017</td>
    <td class="tg-0pky">compute int<br>compute float<br>add float: 1<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
  </tr>
</table>

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