เหตุใดจึงต้องออกแบบภาษาด้วยประเภทที่ไม่ระบุตัวตน


92

นี่เป็นสิ่งที่ทำให้ฉันติดขัดในฐานะคุณลักษณะของนิพจน์แลมบ์ดา C ++ ประเภทของนิพจน์แลมบ์ดา C ++ นั้นไม่เหมือนใครและไม่ระบุตัวตนฉันไม่สามารถเขียนมันลงไปได้ แม้ว่าฉันจะสร้างแลมบ์ดาสองตัวที่มีวากยสัมพันธ์เหมือนกันทุกประการ แต่ประเภทผลลัพธ์ก็ถูกกำหนดให้แตกต่างกัน ผลที่ตามมาคือ a) lambdas สามารถส่งผ่านไปยังฟังก์ชันเทมเพลตที่อนุญาตให้ส่งต่อเวลาคอมไพล์ประเภทที่ไม่สามารถบรรยายได้พร้อมกับออบเจ็กต์และ b) lambdas นั้นมีประโยชน์ก็ต่อเมื่อถูกลบผ่านประเภทstd::function<>เท่านั้น

โอเค แต่นั่นเป็นเพียงวิธีที่ C ++ ทำฉันก็พร้อมที่จะเขียนมันออกมาเป็นเพียงคุณลักษณะที่น่ารำคาญของภาษานั้น อย่างไรก็ตามฉันเพิ่งเรียนรู้ว่า Rust ดูเหมือนจะเหมือนกัน: ฟังก์ชัน Rust หรือแลมบ์ดาแต่ละฟังก์ชันมีลักษณะเฉพาะและไม่ระบุตัวตน และตอนนี้ฉันสงสัยว่า: ทำไม?

ดังนั้นคำถามของฉันคือ
สิ่งนี้มีข้อดีอย่างไรจากมุมมองของนักออกแบบภาษาในการแนะนำแนวคิดของประเภทที่ไม่ซ้ำใครและไม่ระบุตัวตนในภาษา


6
เช่นเคยคำถามที่ดีกว่าคือทำไมไม่
Stargateur

31
"ที่ lambdas มีประโยชน์เพียงครั้งเดียวที่พวกเขาเป็นประเภทลบผ่านฟังก์ชั่นมาตรฐาน :: <>" - std::functionไม่มีพวกเขามีประโยชน์โดยตรงโดยไม่ต้อง std::functionแลมบ์ดาที่ได้รับการส่งผ่านไปยังฟังก์ชั่นแม่แบบสามารถเรียกได้โดยตรงโดยไม่เกี่ยวข้องกับ จากนั้นคอมไพเลอร์สามารถแทรกแลมด้าลงในฟังก์ชันเทมเพลตซึ่งจะช่วยเพิ่มประสิทธิภาพของรันไทม์
Erlkoenig

1
ฉันเดาว่ามันทำให้การใช้แลมด้าง่ายขึ้นและทำให้ภาษาเข้าใจง่ายขึ้น หากคุณอนุญาตให้พับนิพจน์แลมบ์ดาที่เหมือนกันทุกประการให้เป็นประเภทเดียวกันคุณจะต้องใช้กฎพิเศษในการจัดการ{ int i = 42; auto foo = [&i](){ return i; }; } { int i = 13; auto foo = [&i](){ return i; }; }เนื่องจากตัวแปรที่อ้างถึงนั้นแตกต่างกันแม้ว่าจะเหมือนกันก็ตาม ถ้าคุณบอกว่ามันไม่เหมือนใครคุณก็ไม่ต้องกังวลกับการพยายามคิดออก
NathanOliver

5
แต่คุณสามารถตั้งชื่อให้กับประเภท lambdas ได้เช่นกันและทำเช่นเดียวกันกับสิ่งนั้น lambdas_type = decltype( my_lambda);
maximum_prime_is_463035818

3
แต่แลมด้าทั่วไปควรเป็นประเภท[](auto) {}ใด? ควรมีประเภทเริ่มต้นด้วยหรือไม่?
Evg

คำตอบ:


78

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

หาก lambdas ไม่เปิดเผยชื่อเราจะต้องกำหนดพวกเขา สิ่งนี้จะต้องพูดอย่างมากเกี่ยวกับวิธีการจับตัวแปร [=](){...}พิจารณากรณีของแลมบ์ดาที่ ประเภทจะต้องระบุประเภทที่แลมด้าจับได้จริงซึ่งอาจเป็นเรื่องที่ไม่สำคัญในการพิจารณา นอกจากนี้จะเกิดอะไรขึ้นหากคอมไพเลอร์ปรับตัวแปรให้เหมาะสมได้สำเร็จ? พิจารณา:

static const int i = 5;
auto f = [i]() { return i; }

คอมไพเลอร์เพิ่มประสิทธิภาพได้อย่างง่ายดายสามารถรับรู้ว่าค่าได้เพียงหนึ่งเดียวของiที่อาจจะถูกจับคือ 5 auto f = []() { return 5; }และแทนที่นี้กับ อย่างไรก็ตามหากประเภทไม่ระบุชื่อสิ่งนี้อาจเปลี่ยนประเภทหรือบังคับให้คอมไพเลอร์เพิ่มประสิทธิภาพให้น้อยลงโดยจัดเก็บiแม้ว่าจะไม่จำเป็นต้องใช้จริงก็ตาม นี่คือกระเป๋าทั้งหมดของความซับซ้อนและความแตกต่างเล็กน้อยที่ไม่จำเป็นสำหรับสิ่งที่ lambdas ตั้งใจจะทำ

และในกรณีที่คุณต้องการประเภทที่ไม่ระบุตัวตนคุณสามารถสร้างคลาสการปิดด้วยตัวเองและทำงานกับ functor แทนฟังก์ชันแลมบ์ดา ดังนั้นพวกเขาสามารถทำให้ lambdas จัดการเคส 99% และปล่อยให้คุณเขียนโค้ดโซลูชันของคุณเองใน 1%


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

int counter()
{
    static int count = 0;
    return count++;
}

template <typename FuncT>
void action(const FuncT& func)
{
    static int ct = counter();
    func(ct);
}

...
for (int i = 0; i < 5; i++)
    action([](int j) { std::cout << j << std::endl; });

for (int i = 0; i < 5; i++)
    action([](int j) { std::cout << j << std::endl; });

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


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

2
@ComicSansMS การรวมสิ่งต่างๆเข้าด้วยกันเมื่อใช้คอมไพเลอร์นั้นง่ายกว่ามากเมื่อคุณไม่จำเป็นต้องปรับการใช้งานของคุณให้เข้ากับมาตรฐานของคนอื่น พูดจากประสบการณ์ก็มักจะไกลได้ง่ายขึ้นในผู้ดูแลมาตรฐานการทำงาน overspecify กว่าก็คือการพยายามที่จะหาจำนวนเงินขั้นต่ำในการระบุในขณะที่ยังคงได้รับการทำงานที่ต้องการออกจากภาษาของคุณ ในฐานะที่เป็นกรณีศึกษาที่ยอดเยี่ยมให้ดูว่าพวกเขาใช้เวลาทำงานมากแค่ไหนเพื่อหลีกเลี่ยงการระบุ memory_order_consume มากเกินไปในขณะที่ยังคงทำให้เป็นประโยชน์ (ในบางสถาปัตยกรรม)
Cort Ammon

1
เหมือนคนอื่น ๆ ที่คุณทำกรณีที่น่าสนใจสำหรับการที่ไม่ระบุชื่อ แต่มันเป็นความคิดที่ดีจริงๆหรือที่จะบังคับให้มันไม่เหมือนใครด้วย?
Deduplicator

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

คุณไม่สามารถจับตัวแปรคงที่
Ruslan

70

lambdas ไม่ได้ฟังก์ชั่นเพียงแค่พวกเขาเป็นฟังก์ชั่นและรัฐ ดังนั้นทั้ง C ++ และ Rust จึงใช้มันเป็นวัตถุด้วยตัวดำเนินการโทร ( operator()ใน C ++ Fn*ลักษณะ3 ประการใน Rust)

โดยทั่วไป[a] { return a + 1; }ใน C ++ desugars เป็นสิ่งที่ต้องการ

struct __SomeName {
    int a;

    int operator()() {
        return a + 1;
    }
};

จากนั้นใช้ตัวอย่าง__SomeNameที่ใช้แลมด้า

ในขณะที่อยู่ใน Rust || a + 1ใน Rust จะมีสิ่งที่ต้องการ

{
    struct __SomeName {
        a: i32,
    }

    impl FnOnce<()> for __SomeName {
        type Output = i32;
        
        extern "rust-call" fn call_once(self, args: ()) -> Self::Output {
            self.a + 1
        }
    }

    // And FnMut and Fn when necessary

    __SomeName { a }
}

ซึ่งหมายความว่าส่วนใหญ่ lambdas ต้องมีแตกต่างกันประเภท

ตอนนี้มีสองสามวิธีที่เราสามารถทำได้:

  • ด้วยประเภทที่ไม่ระบุตัวตนซึ่งเป็นสิ่งที่ทั้งสองภาษาใช้ ผลที่ตามมาอีกประการหนึ่งก็คือlambdas ทั้งหมดต้องมีประเภทที่แตกต่างกัน แต่สำหรับนักออกแบบภาษาสิ่งนี้มีข้อได้เปรียบที่ชัดเจน: Lambdas สามารถอธิบายได้ง่ายๆโดยใช้ส่วนอื่น ๆ ที่ง่ายกว่าที่มีอยู่แล้วของภาษา พวกเขาเป็นเพียงน้ำตาลทางไวยากรณ์รอบ ๆ บิตของภาษาที่มีอยู่แล้ว
  • ด้วยไวยากรณ์พิเศษบางประการสำหรับการตั้งชื่อประเภทแลมบ์ดา: อย่างไรก็ตามสิ่งนี้ไม่จำเป็นเนื่องจากแลมบ์ดาสามารถใช้กับเทมเพลตใน C ++ หรือกับข้อมูลทั่วไปและFn*ลักษณะใน Rust ได้ ทั้งสองภาษาไม่เคยบังคับให้คุณพิมพ์ - ลบ lambdas เพื่อใช้ (ด้วยstd::functionภาษา C ++ หรือBox<Fn*>ใน Rust)

โปรดทราบว่าทั้งสองภาษายอมรับว่า lambdas เล็กน้อยที่ไม่จับบริบทสามารถแปลงเป็นตัวชี้ฟังก์ชันได้


การอธิบายคุณลักษณะที่ซับซ้อนของภาษาโดยใช้คุณลักษณะที่ง่ายกว่านั้นเป็นเรื่องปกติ ตัวอย่างเช่นทั้ง C ++ และ Rust มีช่วงสำหรับลูปและทั้งคู่อธิบายว่าเป็นน้ำตาลไวยากรณ์สำหรับคุณสมบัติอื่น ๆ

C ++ กำหนด

for (auto&& [first,second] : mymap) {
    // use first and second
}

เทียบเท่ากับ

{

    init-statement
    auto && __range = range_expression ;
    auto __begin = begin_expr ;
    auto __end = end_expr ;
    for ( ; __begin != __end; ++__begin) {

        range_declaration = *__begin;
        loop_statement

    }

} 

และสนิมเป็นตัวกำหนด

for <pat> in <head> { <body> }

เทียบเท่ากับ

let result = match ::std::iter::IntoIterator::into_iter(<head>) {
    mut iter => {
        loop {
            let <pat> = match ::std::iter::Iterator::next(&mut iter) {
                ::std::option::Option::Some(val) => val,
                ::std::option::Option::None => break
            };
            SemiExpr(<body>);
        }
    }
};

ซึ่งแม้ว่ามันจะดูซับซ้อนกว่าสำหรับมนุษย์ แต่ก็ง่ายกว่าสำหรับนักออกแบบภาษาหรือคอมไพเลอร์


15
@ cmaster-reinstatemonica พิจารณาส่งแลมด้าเป็นอาร์กิวเมนต์เปรียบเทียบสำหรับฟังก์ชันการเรียงลำดับ คุณต้องการกำหนดค่าเหนือศีรษะของการเรียกฟังก์ชันเสมือนที่นี่หรือไม่?
Daniel Langr

5
@ cmaster-reinstatemonica เพราะไม่มีอะไรเสมือนโดยค่าเริ่มต้นใน C ++
Caleth

4
@cmaster - คุณหมายถึงบังคับให้ผู้ใช้ lambdas ทุกคนจ่ายเงินสำหรับไดแพตช์แบบไดนามิกแม้ว่าพวกเขาจะไม่ต้องการก็ตาม?
StoryTeller - Unslander Monica

4
@ cmaster-reinstatemonica สิ่งที่ดีที่สุดที่คุณจะได้รับคือการเลือกเข้าร่วมเสมือน เดาว่าstd::functionยัง
ไง

9
@ cmaster-reinstatemonica กลไกใด ๆที่คุณสามารถชี้ฟังก์ชันที่จะเรียกอีกครั้งจะมีสถานการณ์ที่มีโอเวอร์เฮดรันไทม์ นั่นไม่ใช่วิธี C ++ คุณเลือกใช้std::function
Caleth

13

(เพิ่มคำตอบของ Caleth แต่ยาวเกินไปที่จะใส่ในความคิดเห็น)

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

คุณสามารถดูความคล้ายคลึงกันระหว่างโครงสร้างแบบไม่ระบุตัวตนและการไม่เปิดเผยตัวตนของแลมบ์ดาในข้อมูลโค้ดนี้:

#include <iostream>
#include <typeinfo>

using std::cout;

int main() {
    struct { int x; } foo{5};
    struct { int x; } bar{6};
    cout << foo.x << " " << bar.x << "\n";
    cout << typeid(foo).name() << "\n";
    cout << typeid(bar).name() << "\n";
    auto baz = [x = 7]() mutable -> int& { return x; };
    auto quux = [x = 8]() mutable -> int& { return x; };
    cout << baz() << " " << quux() << "\n";
    cout << typeid(baz).name() << "\n";
    cout << typeid(quux).name() << "\n";
}

หากยังไม่เป็นที่พอใจสำหรับแลมด้าก็ควรจะไม่พอใจเช่นกันสำหรับโครงสร้างที่ไม่ระบุตัวตน

บางภาษาอนุญาตให้มีการพิมพ์แบบเป็ดที่ยืดหยุ่นกว่าเล็กน้อยและแม้ว่า C ++ จะมีเทมเพลตที่ไม่ได้ช่วยในการสร้างวัตถุจากเทมเพลตที่มีช่องสมาชิกที่สามารถแทนที่ lambda ได้โดยตรงแทนที่จะใช้std::functionเสื้อคลุม


3
ขอบคุณที่ให้ความกระจ่างเล็กน้อยเกี่ยวกับเหตุผลเบื้องหลังวิธีการกำหนด lambdas ใน C ++ (ฉันต้องจำคำว่า "ประเภทโวลเดอมอร์" :-)) อย่างไรก็ตามคำถามยังคงอยู่: ข้อดีของสิ่งนี้ในสายตาของนักออกแบบภาษาคืออะไร?
cmaster - คืนสถานะโมนิกา

1
คุณยังสามารถเพิ่มint& operator()(){ return x; }โครงสร้างเหล่านั้นได้
Caleth

2
@ cmaster-reinstatemonica •โดยเฉพาะ...ส่วนที่เหลือของ C ++ จะทำงานในลักษณะนั้น ในการสร้าง lambdas ให้ใช้การพิมพ์แบบเป็ด "รูปร่างพื้นผิว" บางอย่างจะเป็นสิ่งที่แตกต่างจากภาษาอื่น ๆ มาก การเพิ่มสิ่งอำนวยความสะดวกประเภทนั้นในภาษาสำหรับ lambdas อาจถือได้ว่าเป็นภาษาทั่วไปสำหรับทั้งภาษาและนั่นอาจเป็นการเปลี่ยนแปลงครั้งใหญ่ การละเว้นสิ่งอำนวยความสะดวกดังกล่าวสำหรับ lambdas เพียงอย่างเดียวก็เข้ากันได้ดีกับการพิมพ์ C ++ ที่เหลือ
Eljay

ในทางเทคนิคจะเป็นประเภทโวลเดอมอร์auto foo(){ struct DarkLord {} tom_riddle; return tom_riddle; }เนื่องจากfooไม่มีอะไรสามารถใช้ตัวระบุได้DarkLord
Caleth

@ cmaster-reinstatemonica ประสิทธิภาพอีกทางเลือกหนึ่งคือการจัดส่งแลมด้าทุกตัวแบบไดนามิก (จัดสรรไว้ในฮีปและลบประเภทที่แม่นยำ) ตอนนี้ตามที่คุณทราบว่าคอมไพเลอร์สามารถลบข้อมูลซ้ำซ้อนของ lambdas ประเภทที่ไม่ระบุตัวตนได้ แต่คุณยังไม่สามารถเขียนลงไปได้และจะต้องใช้งานที่สำคัญเพื่อให้ได้ผลตอบแทนน้อยมากดังนั้นอัตราต่อรองจึงไม่เป็นที่โปรดปราน
Masklinn

10

เหตุใดจึงต้องออกแบบภาษาด้วยประเภทที่ไม่ระบุตัวตน

เนื่องจากมีหลายกรณีที่ชื่อไม่เกี่ยวข้องและไม่มีประโยชน์หรือแม้แต่ต่อต้าน ในกรณีนี้ความสามารถในการแยกแยะการดำรงอยู่ของพวกมันมีประโยชน์เพราะมันช่วยลดมลพิษทางชื่อและแก้ปัญหาหนักหนึ่งในสองปัญหาในวิทยาศาสตร์คอมพิวเตอร์ (วิธีตั้งชื่อสิ่งต่างๆ) ด้วยเหตุผลเดียวกันวัตถุชั่วคราวจึงมีประโยชน์

แลมด้า

ความเป็นเอกลักษณ์ไม่ได้เป็นของแลมด้าพิเศษหรือแม้แต่สิ่งพิเศษสำหรับประเภทนิรนาม ใช้กับประเภทที่มีชื่อในภาษาด้วย พิจารณาสิ่งต่อไปนี้:

struct A {
    void operator()(){};
};

struct B {
    void operator()(){};
};

void foo(A);

โปรดทราบว่าฉันไม่สามารถผ่านBเข้าไปได้fooได้แม้ว่าคลาสจะเหมือนกันก็ตาม คุณสมบัติเดียวกันนี้ใช้กับชนิดที่ไม่มีชื่อ

lambdas สามารถส่งผ่านไปยังฟังก์ชันเทมเพลตที่อนุญาตให้ส่งต่อเวลาคอมไพล์ประเภทที่ไม่สามารถบรรยายได้พร้อมกับอ็อบเจ็กต์ ... ลบผ่าน std :: function <>

มีตัวเลือกที่สามสำหรับชุดย่อยของ lambdas: lambdas ที่ไม่จับภาพสามารถแปลงเป็นตัวชี้ฟังก์ชันได้


โปรดทราบว่าหากข้อ จำกัด ของชนิดที่ไม่ระบุชื่อเป็นปัญหาสำหรับกรณีการใช้งานวิธีแก้ไขก็ง่าย: สามารถใช้ประเภทที่ระบุชื่อแทนได้ Lambdas ไม่ทำอะไรที่ไม่สามารถทำได้กับคลาสที่มีชื่อ


10

คำตอบที่ยอมรับของ Cort Ammonนั้นดี แต่ฉันคิดว่ามีอีกหนึ่งประเด็นสำคัญที่ต้องทำเกี่ยวกับการนำไปใช้งานได้

สมมติว่าฉันมีหน่วยการแปลสองหน่วยที่ต่างกัน "one.cpp" และ "two.cpp"

// one.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)

// two.cpp
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 = [](){};  // a has type $_0
auto b = [](){};  // b has type $_1
auto f(int x) {
    return [x](int y) { return x+y; };  // f(1) and f(2) both have type $_2
} 
auto g(float x) {
    return [x](int y) { return x+y; };  // g(1) and g(2) both have type $_3
} 

แน่นอนว่าชื่อเหล่านี้มีความหมายเฉพาะในหน่วยการแปลนี้ ม ธ . นี้$_0จะแตกต่างจากม ธ . อื่น ๆเสมอ$_0แม้ว่าม ธ . นี้struct Aจะเป็นประเภทเดียวกับม ธ . อื่น ๆstruct Aก็ตาม

อย่างไรก็ตามโปรดสังเกตว่าแนวคิด "เข้ารหัสข้อความของแลมบ์ดา" ของเรามีปัญหาที่ละเอียดอ่อนอีกประการหนึ่งนั่นคือแลมบ์ดา$_2และ$_3ประกอบด้วยข้อความที่เหมือนกันทุกประการแต่ไม่ควรถือว่าเหมือนกันอย่างชัดเจนประเภท !


โดยวิธีการที่ C ++ ต้องการให้คอมไพเลอร์รู้วิธีการแยกข้อความของนิพจน์ C ++ โดยพลการเช่นเดียวกับใน

template<class T> void foo(decltype(T())) {}
template void foo<int>(int);  // _Z3fooIiEvDTcvT__EE, not _Z3fooIiEvT_

แต่ c ++ ไม่ได้ (ยัง) ต้องใช้คอมไพเลอร์จะทราบวิธีการฉีกกฎเกณฑ์ c ++ คำสั่ง decltype([](){ ...arbitrary statements... })ยังคงมีรูปแบบไม่ถูกต้องแม้ใน C ++ 20


นอกจากนี้ยังแจ้งให้ทราบว่ามันเป็นเรื่องง่ายที่จะให้นามแฝงในท้องถิ่นที่จะไม่มีชื่อประเภทใช้/typedef usingฉันรู้สึกว่าคำถามของคุณอาจเกิดขึ้นจากการพยายามทำบางสิ่งที่สามารถแก้ไขได้เช่นนี้

auto f(int x) {
    return [x](int y) { return x+y; };
}

// Give the type an alias, so I can refer to it within this translation unit
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จะเป็นตัวชี้ฟังก์ชันเริ่มต้นที่เป็นค่าเริ่มต้นและคุณอาจจะเลือกผิด ที่เหมือนไม่มีประโยชน์


" อันที่จริงมันต้องแน่ใจว่ามันคนละประเภทกัน แต่ตอนนี้มันไม่สำคัญเท่า " ฉันสงสัยว่ามีเหตุผลที่ดีไหมที่จะบังคับให้มีเอกลักษณ์
Deduplicator

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

ไม่ว่าฟังก์ชันที่เทียบเท่าสองฟังก์ชัน (ยกเว้นในกรณีที่) อาจเหมือนกันก็เป็นคำถามที่ดีเช่นกัน ภาษาในมาตรฐานไม่ชัดเจนนักสำหรับฟังก์ชันฟรีและ / หรือแบบคงที่ แต่มันอยู่นอกขอบเขตที่นี่
Deduplicator

ในเดือนนี้มีการอภิปรายเกี่ยวกับรายชื่อผู้รับจดหมาย LLVM เกี่ยวกับการรวมฟังก์ชันในเดือนนี้ codegen ของ Clang จะทำให้ฟังก์ชันที่มีเนื้อความว่างเปล่าถูกรวมเข้าด้วยกันเกือบ "โดยบังเอิญ": godbolt.org/z/obT55bนี่ไม่เป็นไปตามเทคนิคในทางเทคนิคและฉันคิดว่าพวกเขาน่าจะแก้ไข LLVM เพื่อหยุดการดำเนินการนี้ แต่ใช่ตกลงการรวมที่อยู่ของฟังก์ชันก็เป็นสิ่งที่ดีเช่นกัน
Quuxplusone

ตัวอย่างนั้นมีปัญหาอื่น ๆ คือไม่มีคำสั่ง return พวกเขาไม่ได้ทำให้รหัสไม่เป็นไปตามมาตรฐานแล้วหรือ? นอกจากนี้ฉันจะมองหาการสนทนา แต่พวกเขาแสดงหรือถือว่าการรวมฟังก์ชันที่เทียบเท่ากันนั้นไม่เป็นไปตามมาตรฐานพฤติกรรมที่บันทึกไว้กับ gcc หรือเพียงแค่ว่าบางคนพึ่งพามันไม่ได้เกิดขึ้น
Deduplicator

9

C ++ lambdas ต้องการประเภทที่แตกต่างกันสำหรับการดำเนินการที่แตกต่างกันเนื่องจาก C ++ ผูกแบบคงที่ สามารถคัดลอก / ย้ายได้เท่านั้นดังนั้นส่วนใหญ่คุณไม่จำเป็นต้องตั้งชื่อประเภท แต่นั่นเป็นรายละเอียดการใช้งานทั้งหมด

ฉันไม่แน่ใจว่า C # lambdas มีประเภทหรือไม่เนื่องจากเป็น "นิพจน์ฟังก์ชันที่ไม่ระบุชื่อ" และจะถูกแปลงเป็นประเภทผู้รับมอบสิทธิ์ที่เข้ากันได้ทันทีหรือประเภทแผนภูมิของนิพจน์ ถ้าเป็นเช่นนั้นอาจเป็นประเภทที่ไม่สามารถออกเสียงได้

C ++ ยังมีโครงสร้างที่ไม่ระบุตัวตนซึ่งแต่ละคำจำกัดความจะนำไปสู่ประเภทที่ไม่ซ้ำกัน ที่นี่ชื่อนี้ไม่สามารถออกเสียงไม่ได้ แต่ก็ไม่มีอยู่เท่าที่มาตรฐานเกี่ยวข้อง

C # มีประเภทข้อมูลที่ไม่ระบุตัวตนซึ่งห้ามไม่ให้หลีกหนีจากขอบเขตที่กำหนดไว้อย่างระมัดระวัง การใช้งานจะให้ชื่อที่ไม่ซ้ำกันและไม่สามารถออกเสียงให้กับผู้นั้นได้

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

นอกเหนือจาก:

คุณสามารถตั้งชื่อให้กับประเภทของแลมบ์ดาได้

auto foo = []{}; 
using Foo_t = decltype(foo);

หากคุณไม่มีการจับภาพคุณสามารถใช้ประเภทตัวชี้ฟังก์ชันได้

void (*pfoo)() = foo;

1
โค้ดตัวอย่างแรกยังคงไม่อนุญาตให้ใช้โค้ดที่ตามมาFoo_t = []{};เท่านั้นFoo_t = fooและไม่มีอะไรอื่น
cmaster - คืนสถานะ monica

1
@ cmaster-reinstatemonica นั่นเป็นเพราะประเภทนี้ไม่สามารถสร้างได้ตามค่าเริ่มต้นไม่ใช่เพราะการไม่เปิดเผยตัวตน การคาดเดาของฉันมีมากพอ ๆ กับการหลีกเลี่ยงการมีกรณีมุมที่ใหญ่กว่าที่คุณต้องจำเป็นเหตุผลทางเทคนิคใด ๆ
Caleth

6

ทำไมต้องใช้ประเภทนิรนาม

สำหรับชนิดที่คอมไพลเลอร์สร้างขึ้นโดยอัตโนมัติทางเลือกคือ (1) ปฏิบัติตามคำขอของผู้ใช้สำหรับชื่อประเภทหรือ (2) ปล่อยให้คอมไพลเลอร์เลือกเอง

  1. ในกรณีก่อนหน้านี้ผู้ใช้คาดว่าจะต้องระบุชื่ออย่างชัดเจนทุกครั้งที่โครงสร้างดังกล่าวปรากฏขึ้น (C ++ / Rust: เมื่อใดก็ตามที่มีการกำหนดแลมบ์ดา Rust: เมื่อใดก็ตามที่มีการกำหนดฟังก์ชัน) นี่เป็นรายละเอียดที่น่าเบื่อสำหรับผู้ใช้ในการให้แต่ละครั้งและในกรณีส่วนใหญ่จะไม่มีการอ้างถึงชื่ออีก ดังนั้นจึงควรให้คอมไพเลอร์คิดชื่อโดยอัตโนมัติและใช้คุณสมบัติที่มีอยู่เช่นdecltypeหรือพิมพ์การอนุมานเพื่ออ้างอิงประเภทในไม่กี่แห่งที่จำเป็น

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

เหตุใดจึงใช้ประเภทที่ไม่ซ้ำกัน (แตกต่างกัน)

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

ตัวอย่างเช่นในขณะที่คอมไพเลอร์เห็น:

let f: __UniqueFunc042 = || { ... };  // definition of __UniqueFunc042 (assume it has a nontrivial closure)

/* ... intervening code */

let g: __UniqueFunc042 = /* some expression */;
g();

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

จำเป็นต้อง constrains fนี้สิ่งที่ผู้ใช้สามารถทำอะไรกับ ผู้ใช้ไม่มีเสรีภาพในการเขียน:

let q = if some_condition { f } else { || {} };  // ERROR: type mismatch

เนื่องจากจะนำไปสู่การรวม (ผิดกฎหมาย) ของสองประเภทที่แตกต่างกัน

การทำงานรอบนี้ผู้ใช้สามารถ upCast __UniqueFunc042ชนิดที่ไม่ซ้ำกัน&dyn Fn(),

let f2 = &f as &dyn Fn();  // upcast
let q2 = if some_condition { f2 } else { &|| {} };  // OK

การแลกเปลี่ยนที่ทำโดยการลบประเภทนี้คือการใช้&dyn Fn()เหตุผลที่ซับซ้อนสำหรับคอมไพเลอร์ ให้:

let g2: &dyn Fn() = /*expression */;

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

สิ่งนี้จะชัดเจนยิ่งขึ้นเมื่อวัตถุดังกล่าวส่งไปยังฟังก์ชันทั่วไป (เทมเพลต):

fn h<F: Fn()>(f: F);

หากหนึ่งในสายh(f)ที่f: __UniqueFunc042แล้วhมีความเชี่ยวชาญในการอินสแตนซ์ที่ไม่ซ้ำกัน:

h::<__UniqueFunc042>(f);

สิ่งนี้ช่วยให้คอมไพลเลอร์สามารถสร้างรหัสพิเศษสำหรับการhปรับแต่งสำหรับอาร์กิวเมนต์เฉพาะของfและการจัดส่งไปfมีแนวโน้มที่จะเป็นแบบคงที่หากไม่ได้อยู่ในบรรทัด

ในสถานการณ์ตรงกันข้ามโดยที่หนึ่งเรียกh(f)ด้วยf2: &Fn()ค่าhจะถูกสร้างอินสแตนซ์เป็น

h::<&Fn()>(f);

&Fn()ซึ่งจะใช้ร่วมกันระหว่างฟังก์ชั่นทุกชนิด จากภายในhคอมไพลเลอร์รู้น้อยมากเกี่ยวกับฟังก์ชันทึบแสงของประเภท&Fn()ดังนั้นจึงสามารถเรียกfด้วยการจัดส่งเสมือนอย่างระมัดระวังเท่านั้น ในการจัดส่งแบบคงที่คอมไพลเลอร์จะต้องต่อสายไปh::<&Fn()>(f)ที่ไซต์การโทรซึ่งไม่รับประกันว่าhซับซ้อนเกินไปหรือไม่


ส่วนแรกเกี่ยวกับการเลือกชื่อพลาดประเด็น: ประเภทที่ชอบvoid(*)(int, double)อาจไม่มีชื่อ แต่ฉันสามารถเขียนลงไปได้ ฉันจะเรียกมันว่าประเภทนิรนามไม่ใช่ประเภทนิรนาม และฉันจะเรียกสิ่งที่เป็นความลับเช่น__namespace1_module1_func1_AnonymousFunction042ชื่อ mangling ซึ่งไม่อยู่ในขอบเขตของคำถามนี้แน่นอน คำถามนี้เกี่ยวกับประเภทที่ได้รับการรับรองโดยมาตรฐานว่าไม่สามารถจดบันทึกได้ซึ่งต่างจากการแนะนำประเภทไวยากรณ์ที่สามารถแสดงประเภทเหล่านี้ในลักษณะที่เป็นประโยชน์
cmaster - คืนสถานะโมนิกา

3

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

แล้วทำไม lambdas ที่มีการจับภาพไม่สามารถแปลงเป็นตัวชี้ได้? เนื่องจากฟังก์ชันต้องเข้าถึงสถานะของแลมบ์ดาดังนั้นสถานะนี้จึงต้องปรากฏเป็นอาร์กิวเมนต์ของฟังก์ชัน


การจับภาพควรกลายเป็นส่วนหนึ่งของแลมด้าเองไม่ใช่หรือ? เช่นเดียวกับที่ห่อหุ้มภายในstd::function<>ไฟล์.
cmaster - คืนสถานะโมนิกา

3

เพื่อหลีกเลี่ยงการชนชื่อกับรหัสผู้ใช้

แม้แต่แลมบ์ดาสองตัวที่มีการใช้งานเดียวกันก็จะมีประเภทที่แตกต่างกัน ซึ่งก็โอเคเพราะฉันสามารถมีประเภทต่างๆสำหรับวัตถุได้เช่นกันแม้ว่าเค้าโครงหน่วยความจำจะเท่ากันก็ตาม


ประเภท like int (*)(Foo*, int, double)จะไม่เสี่ยงต่อการชนชื่อกับรหัสผู้ใช้
cmaster - คืนสถานะ monica

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

อีกครั้งคำถามนี้เกี่ยวกับการออกแบบภาษาไม่ใช่เกี่ยวกับ C ++ ฉันสามารถกำหนดภาษาได้อย่างแน่นอนโดยที่ประเภทของแลมบ์ดานั้นคล้ายกับชนิดของตัวชี้ฟังก์ชันมากกว่าประเภทโครงสร้างข้อมูล ไวยากรณ์ตัวชี้ฟังก์ชันใน C ++ และไวยากรณ์ประเภทอาร์เรย์แบบไดนามิกใน C พิสูจน์ว่าเป็นไปได้ และนั่นทำให้เกิดคำถามทำไม lambdas ไม่ใช้วิธีการที่คล้ายกัน?
cmaster - คืนสถานะ monica

1
ไม่คุณทำไม่ได้เพราะแกงกะหรี่ที่หลากหลาย (การจับภาพ) คุณต้องมีทั้งฟังก์ชันและข้อมูลเพื่อให้ทำงานได้
Blindy

@ บลินดี้โอ้ใช่ฉันทำได้ ฉันสามารถกำหนดแลมบ์ดาให้เป็นอ็อบเจกต์ที่มีพอยน์เตอร์สองตัวตัวหนึ่งสำหรับอ็อบเจกต์การจับและอีกอันสำหรับโค้ด วัตถุแลมบ์ดาเช่นนี้จะส่งผ่านมูลค่าได้ง่าย หรือฉันสามารถดึงลูกเล่นด้วยส่วนท้ายของโค้ดที่จุดเริ่มต้นของวัตถุการจับภาพที่ใช้ที่อยู่ของมันเองก่อนที่จะข้ามไปยังโค้ดแลมบ์ดาจริง นั่นจะทำให้ตัวชี้แลมบ์ดากลายเป็นที่อยู่เดียว แต่นั่นไม่จำเป็นเนื่องจากแพลตฟอร์ม PPC ได้พิสูจน์แล้ว: บน PPC ตัวชี้ฟังก์ชันเป็นคู่ของพอยน์เตอร์ นั่นเป็นเหตุผลที่คุณไม่สามารถทิ้งvoid(*)(void)ไปvoid*และกลับมาอยู่ในมาตรฐาน C / C ++
cmaster - คืนสถานะ monica
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.