คีย์เวิร์ดเดียวที่หายไป เปลี่ยนการคัดลอกจากศูนย์เป็นหมื่นหก โดยไม่มีสวิตช์ไหนเตือน
คุณเขียน class ที่มี move constructor ของตัวเอง แล้วเอาไปใส่ std::vector
class นั้นย้ายได้ · vector รู้ว่ามันย้ายได้ · แต่ตอน vector ขยาย มันเลือก คัดลอก
โปรแกรมเดียวกัน push element 10,000 ตัว สองรอบ ต่างกันที่คีย์เวิร์ดเดียว
| คัดลอก | ย้าย | ขยายกี่ครั้ง | ความจุปลายทาง | |
|---|---|---|---|---|
move ไม่ได้ประกาศ noexcept | 16,383 | 10,000 | 15 | 16,384 |
ประกาศ noexcept | 0 | 26,383 | 15 | 16,384 |
สอง column ขวาเท่ากันเป๊ะ และนั่นคือ column ที่สำคัญที่สุดของตาราง
มันปิดคำอธิบายที่ง่ายที่สุดและผิด ว่ารอบหนึ่งขยายบ่อยกว่าอีกรอบ · ทั้งสองรอบขยาย 15 ครั้งเท่ากัน และจบที่ความจุเดียวกัน · สิ่งที่ต่างคือตอนขยาย มันเลือกทำอะไร
ทำไม vector ถึงเลือกคัดลอกทั้งที่ย้ายได้
หัวข้อที่มีชื่อว่า “ทำไม vector ถึงเลือกคัดลอกทั้งที่ย้ายได้”ตอน push_back แล้วที่เต็ม vector ต้องจองบล็อกใหม่แล้วย้ายของเดิมข้ามไป
ระหว่างนั้นมันสัญญากับคุณว่า ถ้ามีอะไรโยน exception ออกมา vector จะยังอยู่ในสภาพเดิม
เหมือนไม่เคยเรียก push_back เลย
ลองคิดว่าถ้ามันย้ายไปได้ครึ่งทางแล้วตัวที่ 5,000 โยน · ของ 4,999 ตัวแรกถูกย้ายออกจากบล็อกเดิม ไปแล้ว บล็อกเดิมจึงพัง และบล็อกใหม่ก็ยังไม่ครบ · ไม่มีทางถอยกลับ
การคัดลอกไม่มีปัญหานั้น เพราะของเดิมยังอยู่ครบ ถ้าโยนกลางทางก็แค่ทิ้งบล็อกใหม่
คำถามที่ vector ถามคือ “move constructor ตัวนี้รับประกันว่าไม่โยนหรือเปล่า”
· คำตอบมาจากคีย์เวิร์ด noexcept ที่ประกาศไว้ ไม่ได้มาจากการที่ code ข้างในไม่โยนจริง
move constructor ที่ ไม่โยนแน่ ๆ แต่ไม่ได้ประกาศ ให้คำตอบเดียวกับตัวที่โยนได้จริง · คอมไพเลอร์ไม่ได้เดาให้ และ vector ก็ทำสิ่งที่ปลอดภัยกว่าเสมอ
แล้วเครื่องมือเตือนไหม
หัวข้อที่มีชื่อว่า “แล้วเครื่องมือเตือนไหม”นี่คือส่วนที่หน้านี้ไปวัดต่อ เพราะข้อเสียที่เครื่องมือเตือนได้ ไม่ใช่ข้อเสียที่น่ากลัว
เปิดสวิตช์เป็นชั้น ๆ บน Apple clang version 21.0.0 (clang-2100.1.1.101) แล้วนับสิ่งที่มันพูด
| ชุดสวิตช์ | diagnostic ที่พ่นใส่ file ที่วัด |
|---|---|
-O2 | 0 |
-O2 -Wall -Wextra | 0 |
| ชุดแรงสุดที่คอมไพเลอร์ตัวนี้มี | 12 |
ชุดแรงสุดพ่นออกมา 12 ตัว · 11 ตัวในนั้นบ่นว่า code ของคุณใหม่เกินไป สำหรับ C++98 ซึ่งเป็นมาตรฐานที่ออกก่อน move semantics จะมีอยู่
| diagnostic ที่ชุดแรงสุดพ่นออกมา | กี่ตัว |
|---|---|
| defaulted function definitions are incompatible with C++98 | 3 |
| constructor call from initializer list is incompatible with C++98 | 2 |
| rvalue references are incompatible with C++98 | 2 |
| consecutive right angle brackets are incompatible with C++98 (use '> >') | 1 |
| default member initializer for non-static data members is incompatible with C++98 | 1 |
| generalized initializer lists are incompatible with C++98 | 1 |
| include location '/usr/local/include' is unsafe for cross-compilation | 1 |
| noexcept specifications are incompatible with C++98 | 1 |
ไล่ดูทีละบรรทัดแล้ว ไม่มีตัวไหนพูดถึงการคัดลอก 16,383 ครั้งเลย
· ตัวที่เข้าใกล้ที่สุดคือ noexcept specifications are incompatible with C++98
ซึ่งบ่นว่าคุณ ใช้ คีย์เวิร์ดนั้น ไม่ได้บ่นว่าคุณ ไม่ได้ใช้ มัน
ตัววัดตัวเดียวกันรันบน CI ด้วย GCC 13.3 บน Ubuntu (run 32968018416) ได้ผลสองอย่าง
- จำนวน copy เท่ากันเป๊ะ: 16,383 เพราะ libstdc++ ก็โตทีละสองเท่าเหมือน libc++ · เลขนี้จึงข้าม implementation ที่โตสองเท่าได้ แต่ยังข้ามไปหาตัวที่โต 1.5 เท่าไม่ได้
- ชุดสวิตช์แรงสุดของ GCC พ่นออกมา 0 ตัว เพราะ
-Wc++98-compatเป็นของ clang เท่านั้น
แปลว่าบน GCC เครื่องมือ ไม่พูดอะไรเลยสักคำ ไม่ใช่พูดเยอะแล้วพูดผิดเรื่อง · ข้ออ้างของหน้านี้จึงแข็งกว่าบนคอมไพเลอร์ตัวนั้น ไม่ใช่อ่อนกว่า
ตอนวัดครั้งแรก ผู้เขียนหน้านี้ได้ ศูนย์ทุกชุดสวิตช์ และเกือบเขียนมันเป็นพาดหัว
ของจริงคือ shell ส่ง -O2 -Weverything ไปเป็น argument ตัวเดียว คอมไพเลอร์อ่านไม่ออก
แล้วไม่พ่นอะไรเลย · ศูนย์นั้นหน้าตาเหมือนคำตอบทุกประการ
ตัววัดของหน้านี้จึงมี file ควบคุมที่มีตัวแปรซึ่งไม่ถูกอ่าน และต้องดังทุกครั้งที่สวิตช์ทำงาน
· วัดได้ 1 ตัวที่ -Wall -Wextra และ 3 ตัวที่ชุดแรงสุด
· ถ้ามันเงียบเมื่อไร ด่านแดงทันทีและบอกว่า การวัดพัง ไม่ใช่ว่าคอมไพเลอร์เงียบ
กฎที่เอาไปใช้ได้
หัวข้อที่มีชื่อว่า “กฎที่เอาไปใช้ได้”ประกาศ noexcept บน move constructor และ move assignment ของทุก class ที่ย้ายได้จริง
โดยไม่โยน
class ที่ถือ pointer, handle, index หรือ std::unique_ptr เข้าเกณฑ์นี้เกือบทั้งหมด
เพราะการย้ายของพวกนี้คือการก๊อป field แล้วล้างของเดิม ซึ่งไม่มีอะไรให้โยน
= default ช่วยได้ในหลายกรณี เพราะคอมไพเลอร์อนุมาน noexcept ให้เองเมื่อสมาชิกทุกตัว
ย้ายแบบไม่โยน · ปัญหาเกิดตอนคุณเขียน move constructor เอง ซึ่งเป็นตอนที่คุณต้องพิมพ์
คีย์เวิร์ดนั้นด้วยมือ
สิ่งที่หน้านี้ไม่ได้วัด
หัวข้อที่มีชื่อว่า “สิ่งที่หน้านี้ไม่ได้วัด”- ตัวเลขผูกกับ standard library ที่รันอยู่ Apple clang version 21.0.0 (clang-2100.1.1.101) โตทีละสองเท่า จึงได้ 16,383 · implementation ที่โต 1.5 เท่าให้ตัวเลขคนละตัวโดยสิ้นเชิง · อย่ายกตัวเลขนี้ไปใช้โดยไม่ยกชื่อ toolchain ไปด้วย
- ไม่ได้วัดเวลา หน่วยของหน้านี้คือจำนวนครั้งที่ constructor ถูกเรียก ซึ่งข้ามเครื่องได้ ส่วนวินาทีข้ามไม่ได้
- ไม่ได้วัด container ตัวอื่น
std::dequeกับ container ของเอนจินเกมมีกฎการขยาย ของตัวเอง หน้านี้พูดถึงstd::vectorเท่านั้น - ไม่ได้วัดคอมไพเลอร์ตัวอื่น ชุดสวิตช์แรงสุดที่ใช้คือ
-O2 -Weverythingซึ่งมีเฉพาะบนคอมไพเลอร์ตระกูลนี้
ตัววัดอยู่ที่ scripts/noexcept-realloc/ รันด้วย npm run verify:noexcept
· มันอยู่ในเกตของ CI จึงมีคนรันทุกครั้งที่มีคน push