คำตอบที่ยอมรับของ Cort Ammonนั้นดี แต่ฉันคิดว่ามีอีกหนึ่งประเด็นสำคัญที่ต้องทำเกี่ยวกับการนำไปใช้งานได้
สมมติว่าฉันมีหน่วยการแปลสองหน่วยที่ต่างกัน "one.cpp" และ "two.cpp"
struct A { int operator()(int x) const { return x+1; } };
auto b = [](int x) { return x+1; };
using A1 = A;
using B1 = decltype(b);
extern void foo(A1);
extern void foo(B1);
การโอเวอร์โหลดทั้งสองfoo
ใช้ตัวระบุเดียวกัน ( foo
) แต่มีชื่อที่แตกต่างกัน (ใน Itanium ABI ที่ใช้กับระบบ POSIX-ish ชื่อที่แตกต่างกันคือ_Z3foo1A
และในกรณีนี้โดยเฉพาะ_Z3fooN1bMUliE_E
)
struct A { int operator()(int x) const { return x + 1; } };
auto b = [](int x) { return x + 1; };
using A2 = A;
using B2 = decltype(b);
void foo(A2) {}
void foo(B2) {}
คอมไพลเลอร์ C ++ ต้องตรวจสอบให้แน่ใจว่าชื่อที่แตกของvoid foo(A1)
ใน "two.cpp" นั้นเหมือนกับชื่อที่ถูกแยกของextern void foo(A2)
"one.cpp" เพื่อให้เราสามารถเชื่อมโยงไฟล์อ็อบเจ็กต์ทั้งสองเข้าด้วยกัน นี่คือความหมายทางกายภาพของสองประเภทที่เป็น "ประเภทเดียวกัน" โดยพื้นฐานแล้วเกี่ยวกับความเข้ากันได้ของ ABI ระหว่างไฟล์อ็อบเจ็กต์ที่คอมไพล์แยกกัน
c ++ คอมไพเลอร์จะไม่จำเป็นต้องใช้เพื่อให้มั่นใจว่าB1
และB2
เป็น "ประเภทเดียวกัน." (อันที่จริงต้องตรวจสอบให้แน่ใจว่าเป็นประเภทต่างๆ แต่ตอนนี้ไม่สำคัญเท่า)
สิ่งที่กลไกทางกายภาพที่ไม่ใช้คอมไพเลอร์เพื่อให้มั่นใจว่าA1
และA2
เป็น "ประเภทเดียวกัน"?
เพียงแค่เจาะผ่าน typedefs จากนั้นดูชื่อที่มีคุณสมบัติครบถ้วนของประเภท A
เป็นประเภทระดับที่ชื่อว่า (อืม::A
เนื่องจากอยู่ในเนมสเปซส่วนกลาง) ดังนั้นจึงเป็นประเภทเดียวกันในทั้งสองกรณี ที่เข้าใจง่าย ที่สำคัญมันเป็นเรื่องง่ายที่จะดำเนินการ หากต้องการดูว่าคลาสสองประเภทเป็นประเภทเดียวกันหรือไม่ให้ใช้ชื่อและทำกstrcmp
. ในการผสมประเภทคลาสเป็นชื่อที่ยุ่งเหยิงของฟังก์ชันคุณต้องเขียนจำนวนอักขระในชื่อตามด้วยอักขระเหล่านั้น
ดังนั้นประเภทที่ตั้งชื่อจึงง่ายต่อการทำลาย
กลไกทางกายภาพอะไรที่คอมไพเลอร์อาจใช้เพื่อให้แน่ใจว่าB1
และB2
เป็น "ประเภทเดียวกัน" ในโลกสมมุติที่ C ++ กำหนดให้เป็นประเภทเดียวกัน
ดีก็ไม่สามารถใช้ชื่อของชนิดเพราะชนิดไม่ได้มีชื่อ
บางทีมันอาจเข้ารหัสข้อความของเนื้อแลมด้า แต่ที่จะเป็นชนิดของที่น่าอึดอัดใจเพราะจริงb
ใน "one.cpp" เป็นอย่างละเอียดแตกต่างจากb
ใน "two.cpp": "one.cpp" มีx+1
และ "two.cpp" x + 1
ได้ ดังนั้นเราจะต้องสร้างกฎที่บอกว่าความแตกต่างของช่องว่างนี้ไม่สำคัญหรือมันเป็นเช่นนั้น (ทำให้เป็นประเภทที่แตกต่างกันไป) หรืออาจจะเป็นเช่นนั้น (ความถูกต้องของโปรแกรมอาจถูกกำหนดให้ใช้งานได้ หรืออาจเป็น "รูปแบบที่ไม่ดีไม่ต้องมีการวินิจฉัย") อย่างไรก็ตาม mangling lambda พิมพ์ในลักษณะเดียวกันในหน่วยการแปลหลายหน่วยเป็นปัญหาที่ยากกว่าการโกงชื่อA
วิธีที่ง่ายที่สุดในการแก้ปัญหาคือการบอกว่านิพจน์แลมบ์ดาแต่ละนิพจน์จะสร้างค่าที่ไม่ซ้ำกัน แลมด้าสองประเภทที่กำหนดไว้ในหน่วยการแปลที่แตกต่างกันนั้นไม่ใช่ประเภทเดียวกันอย่างแน่นอน ภายในหน่วยการแปลเดียวเราสามารถ "ตั้งชื่อ" ประเภทแลมบ์ดาได้เพียงแค่นับจากจุดเริ่มต้นของซอร์สโค้ด:
auto a = [](){};
auto b = [](){};
auto f(int x) {
return [x](int y) { return x+y; };
}
auto g(float x) {
return [x](int y) { return x+y; };
}
แน่นอนว่าชื่อเหล่านี้มีความหมายเฉพาะในหน่วยการแปลนี้ ม ธ . นี้$_0
จะแตกต่างจากม ธ . อื่น ๆเสมอ$_0
แม้ว่าม ธ . นี้struct A
จะเป็นประเภทเดียวกับม ธ . อื่น ๆstruct A
ก็ตาม
อย่างไรก็ตามโปรดสังเกตว่าแนวคิด "เข้ารหัสข้อความของแลมบ์ดา" ของเรามีปัญหาที่ละเอียดอ่อนอีกประการหนึ่งนั่นคือแลมบ์ดา$_2
และ$_3
ประกอบด้วยข้อความที่เหมือนกันทุกประการแต่ไม่ควรถือว่าเหมือนกันอย่างชัดเจนประเภท !
โดยวิธีการที่ C ++ ต้องการให้คอมไพเลอร์รู้วิธีการแยกข้อความของนิพจน์ C ++ โดยพลการเช่นเดียวกับใน
template<class T> void foo(decltype(T())) {}
template void foo<int>(int);
แต่ c ++ ไม่ได้ (ยัง) ต้องใช้คอมไพเลอร์จะทราบวิธีการฉีกกฎเกณฑ์ c ++ คำสั่ง decltype([](){ ...arbitrary statements... })
ยังคงมีรูปแบบไม่ถูกต้องแม้ใน C ++ 20
นอกจากนี้ยังแจ้งให้ทราบว่ามันเป็นเรื่องง่ายที่จะให้นามแฝงในท้องถิ่นที่จะไม่มีชื่อประเภทใช้/typedef
using
ฉันรู้สึกว่าคำถามของคุณอาจเกิดขึ้นจากการพยายามทำบางสิ่งที่สามารถแก้ไขได้เช่นนี้
auto f(int x) {
return [x](int y) { return x+y; };
}
using AdderLambda = decltype(f(0));
int of_one(AdderLambda g) { return g(1); }
int main() {
auto f1 = f(1);
assert(of_one(f1) == 2);
auto f42 = f(42);
assert(of_one(f42) == 43);
}
แก้ไขเพื่อเพิ่ม: จากการอ่านความคิดเห็นของคุณเกี่ยวกับคำตอบอื่น ๆ ดูเหมือนว่าคุณสงสัยว่าทำไม
int add1(int x) { return x + 1; }
int add2(int x) { return x + 2; }
static_assert(std::is_same_v<decltype(add1), decltype(add2)>);
auto add3 = [](int x) { return x + 3; };
auto add4 = [](int x) { return x + 4; };
static_assert(not std::is_same_v<decltype(add3), decltype(add4)>);
นั่นเป็นเพราะ lambdas ที่จับภาพไม่ได้เป็นค่าเริ่มต้นที่สร้างได้ (ใน C ++ เท่าของ C ++ 20 แต่ก็เป็นจริงตามแนวคิดเสมอ)
template<class T>
int default_construct_and_call(int x) {
T t;
return t(x);
}
assert(default_construct_and_call<decltype(add3)>(42) == 45);
assert(default_construct_and_call<decltype(add4)>(42) == 46);
หากคุณลองdefault_construct_and_call<decltype(&add1)>
แล้วt
จะเป็นตัวชี้ฟังก์ชันเริ่มต้นที่เป็นค่าเริ่มต้นและคุณอาจจะเลือกผิด ที่เหมือนไม่มีประโยชน์