1 คะแนน โดย GN⁺ 2024-07-06 | 1 ความคิดเห็น | แชร์ทาง WhatsApp

ฉันไม่มีคอนสตรักเตอร์และต้องกำหนดค่าเริ่มต้น

  • บทนำ

    • ตอนเริ่มเรียน C++ ครั้งแรก ได้เรียนมาว่าคอมไพเลอร์จะจัดเตรียม default constructor ให้ในบางกรณี
    • จึงเริ่มคิดถึงความเสี่ยงที่อ็อบเจ็กต์อาจไม่ได้รับการกำหนดค่าเริ่มต้นในบางสถานการณ์
  • default initialization และ value initialization

    • T t; จะทำ default initialization
      • ถ้า T เป็นชนิดคลาสและมี default constructor ก็จะเรียกใช้งาน
      • ถ้า T เป็นชนิดอาร์เรย์ ก็จะทำ default initialization ให้แต่ละองค์ประกอบ
      • นอกเหนือจากนั้นจะไม่ทำอะไรเลย
    • T t{}; จะทำ value initialization
      • ถ้า T เป็นชนิดคลาส และไม่มี default constructor หรือมี default constructor ที่ผู้ใช้จัดเตรียมหรือถูกลบ ก็จะทำ default initialization
      • มิฉะนั้นจะกำหนดค่าเป็น 0 ก่อน แล้วจึงทำ default initialization
      • ถ้า T เป็นชนิดอาร์เรย์ ก็จะทำ value initialization ให้แต่ละองค์ประกอบ
      • นอกเหนือจากนั้นจะกำหนดค่าเป็น 0
  • default constructor

    • ถ้าไม่ได้ประกาศ default constructor ไว้ คอมไพเลอร์จะประกาศ default constructor แบบปริยายให้
    • default constructor ที่ประกาศแบบปริยายจะมีเนื้อความว่างและรายการกำหนดค่าเริ่มต้นของเมมเบอร์ว่าง
    • ตัวอย่าง:
      struct T {
        int x;
        T() = default;
      };
      T t{};
      std::cout << t.x << std::endl; // ผลลัพธ์คือ 0
      
  • default constructor ที่นิยามแบบปริยาย

    • ถ้า default constructor ถูกประกาศแบบปริยาย หรือประกาศเป็นค่าเริ่มต้นอย่างชัดเจน คอมไพเลอร์จะจัดเตรียม default constructor ที่นิยามแบบปริยายให้
    • ตัวอย่าง:
      struct T {
        T();
      };
      T::T() = default;
      T t{};
      std::cout << t.x << std::endl; // ผลลัพธ์เป็นค่าขยะ
      
  • กรณีที่ไม่สามารถจัดเตรียม default constructor ได้

    • เมื่อ T มีสมาชิกแบบอ้างอิงที่ไม่เป็น static
    • เมื่อ T มีสมาชิกที่ไม่เป็น static หรือคลาสฐานที่ไม่เป็น abstract ซึ่งไม่สามารถ default construct หรือ destruct ได้
    • เมื่อ T มีสมาชิก const ที่ไม่เป็น static และไม่มี default member initializer
  • การกำหนดค่าเริ่มต้นที่ถูกต้อง

    • T t{}; จะทำ list initialization
    • list initialization แบ่งเป็น direct-list initialization และ copy-list initialization
    • ตัวอย่าง:
      struct S {
        int a;
        float b;
        char c;
      };
      S s{3, 4.0f, 'S'}; // ไม่มีการเรียกคอนสตรักเตอร์
      
  • list initialization และ aggregate initialization

    • aggregate initialization เป็นรูปแบบพิเศษของ list initialization โดยจะทำ copy initialization ให้แต่ละองค์ประกอบของคลาสหรืออาร์เรย์ด้วยแต่ละค่าจากรายการกำหนดค่าเริ่มต้น
    • ตัวอย่าง:
      struct A {
        const int x;
      };
      A a{}; // a.x ถูกกำหนดค่าเริ่มต้นเป็น 0
      
  • การกำหนดค่าเริ่มต้นด้วยวงเล็บ

    • การกำหนดค่าเริ่มต้นด้วยวงเล็บจะทำ direct non-list initialization
    • ตัวอย่าง:
      struct T {
        const int& r;
      };
      T t(42); // t.r เป็นการอ้างอิงไปยัง 42
      
  • สรุป

    • กฎของการกำหนดค่าเริ่มต้นมีความซับซ้อน แต่การเขียนคอนสตรักเตอร์เองจะช่วยหลีกเลี่ยงปัญหาส่วนใหญ่ได้
    • ไม่ควรปล่อยให้คอมไพเลอร์จัดการทั้งหมด แต่ควรเขียนคอนสตรักเตอร์ด้วยตนเอง

ความเห็นของ GN⁺

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

1 ความคิดเห็น

 
GN⁺ 2024-07-06
ความคิดเห็นบน Hacker News
  • ผลลัพธ์การกำหนดค่าเริ่มต้นของ t จะเป็น 0

    • เพราะ t ถูกกำหนดค่าเริ่มต้นแบบค่า และเนื่องจาก T ไม่มีตัวสร้างเริ่มต้นแบบกำหนดเอง ออบเจ็กต์จึงถูกทำให้เป็น 0 ก่อนแล้วจึงค่อยเรียกตัวสร้างเริ่มต้น
  • ตัวสร้างเริ่มต้นจะกำหนดค่าเริ่มต้นให้สมาชิกแบบค่าเริ่มต้น ซึ่งต่างจากการกำหนดค่าเริ่มต้นแบบค่า

  • ดูเหมือนว่า GCC จะเห็นด้วยกับเรื่องนี้

  • ผู้เขียนมองข้ามไปว่าจริง ๆ แล้วกำลังกำหนดค่าเริ่มต้นให้ x แบบค่า

    • ผลลัพธ์จึงออกมาไม่ตรงกับที่คาด
  • รายละเอียดของกฎมีความซับซ้อนและบางครั้งก็มีส่วนที่ไม่สมเหตุสมผล

    • แต่ในกรณีส่วนใหญ่ก็ยังได้ผลลัพธ์ตามที่คาดหวัง
  • การทำให้การกำหนดค่าเริ่มต้นแบบดีฟอลต์เป็นสิ่งที่ระบุได้อย่างชัดเจนจะเป็นการปรับปรุงครั้งใหญ่

    • เพราะการกำหนดค่าเริ่มต้นแบบค่าเป็นเรื่องทั่วไป จึงต้องเขียนคอมเมนต์เมื่ออยากได้การกำหนดค่าเริ่มต้นแบบดีฟอลต์
    • ไวยากรณ์อย่าง std::array<int, 100> = void; น่าจะดีกว่า
  • จุดเชื่อมระหว่างการกำหนดค่าเริ่มต้นแบบลิสต์กับการกำหนดค่าเริ่มต้นแบบ aggregate คือ เมื่อทำการกำหนดค่าเริ่มต้นแบบลิสต์กับ aggregate ก็จะเกิดการกำหนดค่าเริ่มต้นแบบ aggregate

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

    • สิ่งนี้เกิดขึ้นในภาษาที่การสร้าง struct จาก parameter pack ง่ายขึ้นเรื่อย ๆ
  • สามารถเขียนตัวสร้างของตัวเองได้ และกำหนดค่าเริ่มต้นให้ทูเพิลหรืออาร์เรย์ที่มีเพียงหนึ่งองค์ประกอบได้

    • แต่ในกรณีพิเศษอาจมีการเรียกตัวสร้างผิดตัว
  • ตอนที่ C++11 initializer list ออกมาใหม่ ๆ เคยเจอเรื่องนี้และคิดว่ามันบ้าสุด ๆ

  • มีการกล่าวถึง "I Have No Mouth, and I Must Scream" (1967)

  • ใช้ไวยากรณ์ T::T() = default;

  • คาดว่าผลลัพธ์ที่พิมพ์ออกมาจะเป็น 0 แต่จริง ๆ แล้วจะได้ค่า garbage

    • บางอย่างก็สมบูรณ์แบบไม่ได้
  • เปิดให้ผู้ใช้ไลบรารีสามารถเปลี่ยนพฤติกรรมของไลบรารีได้

  • ถ้าอยากได้ความซับซ้อนของ C++ เพิ่มอีก แนะนำ C++ FQA

    • แม้จะผ่านไป 15 ปีแล้ว แต่ก็ยังใช้ได้ เพราะ C++ แทบไม่เคยถอดฟีเจอร์หรือพฤติกรรมเก่าออก
  • ธีมของบล็อกได้แรงบันดาลใจจากคอมพิวเตอร์ยุค DEC แต่ดูสะอาดตาและมินิมอล

    • ให้ความรู้สึกสดใหม่
  • อ่านเรื่องนี้แล้วรู้สึกมึนหัว

    • ทำให้นึกถึงตอนพยายามทำความเข้าใจตัวสร้างและการกำหนดค่าเริ่มต้นออบเจ็กต์ของ Java
  • Go และ Rust ไม่มีตัวสร้างแบบพิเศษ จึงทำให้หลายอย่างง่ายขึ้นมาก

    • สงสัยว่าหลังจากเลิกใช้ตัวสร้างแล้ว เคยมีใครคิดถึงมันบ้างไหม
  • สงสัยว่ามีเครื่องมือ C++ ที่แสดงพฤติกรรมโดยนัยทั้งหมดหรือไม่

    • เช่น ตัวสร้างทั้งหมดที่ถูกเพิ่มเข้ามา, implicit copy constructor เป็นต้น
  • มีการให้ข้อมูลผิดเกี่ยวกับคลาส

    • หากมีการประกาศตัวสร้างไว้แล้ว จะไม่มีตัวสร้างเริ่มต้นให้อัตโนมัติ และการกำหนดค่าเริ่มต้นแบบดีฟอลต์จะล้มเหลวพร้อมการวินิจฉัยจากคอมไพเลอร์
  • คำกล่าวที่ว่า "T t; ไม่ได้ทำอะไรเลย" นั้นไม่ถูกต้อง

    • ในโค้ดตัวอย่าง T t; จะล้มเหลว
  • ส่วนหัวของบล็อกมีแผงหน้าของ DEC