ผลกระทบของการโหวตใน การรับประกันลำดับการประเมิน C ++ 17 (P0145)ในรหัส C ++ ทั่วไป
สิ่งต่อไปนี้มีการเปลี่ยนแปลงอย่างไร
i = 1;
f(i++, i)
และ
std::cout << f() << f() << f();
หรือ
f(g(), h(), j());
ผลกระทบของการโหวตใน การรับประกันลำดับการประเมิน C ++ 17 (P0145)ในรหัส C ++ ทั่วไป
สิ่งต่อไปนี้มีการเปลี่ยนแปลงอย่างไร
i = 1;
f(i++, i)
และ
std::cout << f() << f() << f();
หรือ
f(g(), h(), j());
คำตอบ:
บางกรณีทั่วไปที่เพื่อการประเมินผลที่ได้รับเพื่อให้ห่างไกลที่ไม่ระบุ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:
- ก
- a-> ข
- ก -> * ข
- ก (b1, b2, b3)
- b @ = a
- ก [b]
- ก << ข
- ก >> ข
นอกจากนี้เราขอแนะนำกฎเพิ่มเติมดังต่อไปนี้: ลำดับของการประเมินนิพจน์ที่เกี่ยวข้องกับตัวดำเนินการที่โอเวอร์โหลดจะถูกกำหนดโดยลำดับที่เกี่ยวข้องกับตัวดำเนินการในตัวที่เกี่ยวข้องไม่ใช่กฎสำหรับการเรียกใช้ฟังก์ชัน
แก้ไขหมายเหตุ: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;
ยังไม่ได้กำหนด
a
จากb
นั้นc
จึงแสดงd
" และแสดงa(b1, b2, b3)
ว่าb
นิพจน์ทั้งหมดไม่จำเป็นต้องได้รับการประเมินตามลำดับใด ๆ (มิฉะนั้นจะเป็นa(b, c, d)
)
a(b1()(), b2()())
สามารถสั่งซื้อb1()()
และb2()()
ในลำดับใด ๆ แต่ก็ไม่สามารถทำเช่นb1()
นั้นb2()()
แล้วb1()()
: มันอาจจะไม่แทรกประหารชีวิต กล่าวโดยย่อว่า "8. ALTERNATE EVALUATION ORDER FOR FUNCTION CALLS" เป็นส่วนหนึ่งของการเปลี่ยนแปลงที่ได้รับอนุมัติ
f(i++, i)
ไม่ได้กำหนด ตอนนี้ยังไม่ระบุ ตัวอย่างสตริงของ Stroustrup อาจไม่ได้ระบุไม่ใช่ไม่ได้กำหนด `f (get_raw_a (), get_raw_a ());` จะไม่คอมไพล์เนื่องจากตัวunique_ptr
สร้างที่เกี่ยวข้องนั้นชัดเจน สุดท้ายx++ + ++x
ไม่ได้กำหนดระยะเวลา
ใน 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));
มีการดำเนินการสี่อย่างที่เกิดขึ้นที่นี่ระหว่างการเรียกใช้ฟังก์ชัน
new A
unique_ptr<A>
ผู้สร้างnew B
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) เป็นสิ่งต้องห้ามในขณะนี้
ฉันพบหมายเหตุเกี่ยวกับลำดับการประเมินนิพจน์:
ลำดับของการประเมินผลบางอย่างรับประกันโดยรอบตัวดำเนินการที่โอเวอร์โหลดและกฎอาร์กิวเมนต์ที่สมบูรณ์ซึ่งเพิ่มใน 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>