1 điểm bởi GN⁺ 2024-07-06 | 1 bình luận | Chia sẻ qua WhatsApp

Tôi không có constructor nhưng vẫn cần được khởi tạo

  • Mở đầu

    • Khi mới học C++, người viết đã tìm hiểu về những trường hợp compiler cung cấp constructor mặc định.
    • Từ đó nảy sinh băn khoăn về rủi ro đối tượng có thể không được khởi tạo trong một số tình huống nhất định.
  • Khởi tạo mặc định và khởi tạo giá trị

    • T t; thực hiện khởi tạo mặc định.
      • Nếu T là kiểu lớp và có constructor mặc định thì nó sẽ được chạy.
      • Nếu T là kiểu mảng thì từng phần tử sẽ được khởi tạo mặc định.
      • Nếu không thì sẽ không làm gì cả.
    • T t{}; thực hiện khởi tạo giá trị.
      • Nếu T là kiểu lớp thì sẽ khởi tạo mặc định nếu không có constructor mặc định hoặc có constructor mặc định do người dùng cung cấp hay đã bị xóa.
      • Nếu không thì trước tiên sẽ khởi tạo bằng 0 rồi mới khởi tạo mặc định.
      • Nếu T là kiểu mảng thì từng phần tử sẽ được khởi tạo giá trị.
      • Nếu không thì sẽ khởi tạo bằng 0.
  • Constructor mặc định

    • Nếu không khai báo constructor mặc định thì compiler sẽ ngầm khai báo một constructor mặc định.
    • Constructor mặc định được khai báo ngầm sẽ có phần thân rỗng và danh sách khởi tạo thành viên rỗng.
    • Ví dụ:
      struct T {
        int x;
        T() = default;
      };
      T t{};
      std::cout << t.x << std::endl; // kết quả in ra là 0
      
  • Constructor mặc định được định nghĩa ngầm

    • Nếu constructor mặc định được khai báo ngầm hoặc được khai báo tường minh là mặc định, compiler sẽ cung cấp constructor mặc định được định nghĩa ngầm.
    • Ví dụ:
      struct T {
        T();
      };
      T::T() = default;
      T t{};
      std::cout << t.x << std::endl; // kết quả in ra là giá trị rác
      
  • Các trường hợp không thể cung cấp constructor mặc định

    • T có thành viên tham chiếu không tĩnh
    • T có thành viên không tĩnh hoặc lớp cơ sở không trừu tượng không thể default construct hoặc không thể hủy
    • T có thành viên không tĩnh const mà không có default member initializer
  • Khởi tạo đúng cách

    • T t{}; thực hiện list-initialization.
    • List-initialization được chia thành direct-list-initialization và copy-list-initialization.
    • Ví dụ:
      struct S {
        int a;
        float b;
        char c;
      };
      S s{3, 4.0f, 'S'}; // không gọi constructor
      
  • List-initialization và aggregate initialization

    • Aggregate initialization là một dạng đặc biệt của list-initialization, trong đó từng phần tử của lớp hoặc mảng được copy-initialize từ từng phần tử tương ứng trong danh sách khởi tạo.
    • Ví dụ:
      struct A {
        const int x;
      };
      A a{}; // a.x được khởi tạo thành 0
      
  • Khởi tạo bằng dấu ngoặc tròn

    • Khởi tạo bằng dấu ngoặc tròn thực hiện direct non-list initialization.
    • Ví dụ:
      struct T {
        const int& r;
      };
      T t(42); // t.r là tham chiếu trỏ tới 42
      
  • Tóm tắt

    • Các quy tắc khởi tạo khá phức tạp, nhưng nếu tự viết constructor thì có thể tránh được phần lớn vấn đề.
    • Thay vì phó mặc cho compiler, tốt hơn là nên tự viết constructor.

Ý kiến của GN⁺

  • Bài viết giải thích khá rõ sự phức tạp của các quy tắc khởi tạo trong C++.
  • Việc hiểu các quy tắc khởi tạo của C++ là rất quan trọng, vì chúng ảnh hưởng lớn đến độ ổn định và hiệu năng của mã.
  • Tự viết constructor là cách tốt nhất để tránh các vấn đề khởi tạo.
  • Một ngôn ngữ khác có tính năng tương tự là Rust, và Rust có các quy tắc khởi tạo rõ ràng hơn.
  • Khi áp dụng công nghệ mới, điều quan trọng là phải hiểu rõ và sử dụng đúng những chi tiết như quy tắc khởi tạo.

1 bình luận

 
GN⁺ 2024-07-06
Ý kiến trên Hacker News
  • Kết quả khởi tạo của t sẽ là 0

    • Điều này là do t được khởi tạo theo giá trị, và vì T không có constructor mặc định do người dùng định nghĩa, đối tượng sẽ được zero-initialize rồi mới gọi default constructor
  • Default constructor khởi tạo các thành viên theo kiểu default-initialize, khác với value-initialize

  • Có vẻ GCC cũng đồng ý với điều này

  • Tác giả đã bỏ sót việc thực ra đang value-initialize x

    • Kết quả cho ra khác với kỳ vọng
  • Chi tiết của các quy tắc này rất phức tạp và đôi khi có phần phi lý

    • Tuy vậy, trong đa số trường hợp vẫn có thể nhận được kết quả như mong đợi
  • Nếu có cách biểu đạt tường minh cho default initialization thì sẽ là một cải tiến lớn

    • Vì value initialization là trường hợp phổ biến, nên khi muốn default initialization lại phải viết chú thích
    • Cú pháp như std::array<int, 100> = void; sẽ tốt hơn
  • Mối liên hệ giữa list initialization và aggregate initialization là khi list initialization được áp dụng cho aggregate thì aggregate initialization sẽ được thực hiện

    • Tuy nhiên, nếu danh sách chỉ có một đối số thì direct initialization sẽ được dùng
  • Trường hợp chỉ có một phần tử hoạt động khác với trường hợp có từ hai phần tử trở lên

    • Điều này xuất hiện trong một ngôn ngữ ngày càng khiến việc tạo struct từ parameter pack trở nên đơn giản hơn
  • Có thể tự viết constructor của mình và khởi tạo tuple hoặc array chỉ với một phần tử được cung cấp

    • Nhưng trong các trường hợp đặc biệt, constructor sai có thể bị gọi
  • Khi danh sách khởi tạo của C++11 mới xuất hiện, đã phát hiện ra điều này và thấy nó thật điên rồ

  • Có nhắc đến "I Have No Mouth, and I Must Scream" (1967)

  • Sử dụng cú pháp T::T() = default;

  • Có thể sẽ kỳ vọng kết quả in ra là 0, nhưng thực tế lại là giá trị rác

    • Không phải thứ gì cũng có thể hoàn hảo
  • Cho phép người dùng thư viện thay đổi hành vi của thư viện

  • Nếu muốn thêm độ phức tạp của C++, có thể tìm đọc C++ FQA

    • Dù đã 15 năm trôi qua, nó vẫn còn hợp thời vì C++ hầu như không loại bỏ các tính năng hay hành vi cũ
  • Chủ đề giao diện của blog lấy cảm hứng từ máy tính thời DEC nhưng vẫn gọn gàng và tối giản

    • Cảm giác mới mẻ
  • Đọc nội dung này dễ thấy chóng mặt

    • Gợi nhớ đến việc từng cố hiểu constructor Java và quá trình khởi tạo đối tượng
  • Go và Rust không có constructor đặc biệt, nên nhiều thứ trở nên đơn giản hơn

    • Tò mò không biết có ai từng ngừng dùng constructor rồi lại thấy nhớ nó không
  • Tò mò liệu có công cụ C++ nào cho thấy mọi hành vi ngầm định hay không

    • Ví dụ như tất cả constructor được thêm vào, implicit copy constructor, v.v.
  • Bài viết cung cấp thông tin sai về lớp

    • Nếu đã khai báo một constructor thì sẽ không còn default constructor được cung cấp, và default initialization sẽ thất bại kèm chẩn đoán từ compiler
  • Khẳng định rằng T t; là "không làm gì cả" là sai

    • Trong đoạn mã ví dụ, T t; sẽ thất bại
  • Phần đầu blog có bảng điều khiển mặt trước của DEC