Bỏ qua để đến nội dung

Static Initialization Order Fiasco

Đây là một trong những cạm bẫy nổi tiếng nhất của C++ (có hẳn một cái tên riêng: “Static Initialization Order Fiasco”): thứ tự khởi tạo của các biến toàn cục/static ở các file .cpp khác nhau không được đảm bảo.

file_a.cpp
int a = 10;
file_b.cpp
extern int a;
int b = a + 5; // NGUY HIỂM! Giả sử "a" đã được khởi tạo trước khi chạy dòng này - nhưng KHÔNG CHẮC CHẮN!

Nếu file_b.cpp được khởi tạo trước file_a.cpp, biến a khi đó vẫn đang ở giá trị mặc định (0, vì là biến toàn cục), khiến b nhận giá trị sai (5 thay vì 15) - mà không có bất kỳ cảnh báo hay lỗi biên dịch nào.

Trong cùng một file, các biến toàn cục được khởi tạo theo đúng thứ tự khai báo trong file đó - điều này được đảm bảo. Nhưng thứ tự khởi tạo giữa các file .cpp khác nhau hoàn toàn phụ thuộc vào trình biên dịch và trình liên kết (linker) - không có gì trong chuẩn C++ quy định file nào được khởi tạo trước.

Trường hợp thực tế hay gặp: singleton toàn cục phức tạp

Phần tiêu đề “Trường hợp thực tế hay gặp: singleton toàn cục phức tạp”
logger.cpp
class Logger {
public:
Logger() { std::cout << "Logger duoc khoi tao" << std::endl; }
void log(std::string msg) { std::cout << msg << std::endl; }
};
Logger global_logger; // Biến toàn cục kiểu class
config.cpp
extern Logger global_logger;
class Config {
public:
Config() {
global_logger.log("Config dang khoi tao"); // NGUY HIỂM nếu global_logger CHƯA được khởi tạo!
}
};
Config global_config; // Nếu file này khởi tạo TRƯỚC logger.cpp -> lỗi khó lường

Giải pháp: “Construct On First Use” (Meyer’s Singleton)

Phần tiêu đề “Giải pháp: “Construct On First Use” (Meyer’s Singleton)”

Cách khắc phục kinh điển là thay biến toàn cục bằng một hàm trả về tham chiếu đến biến static cục bộ - biến static cục bộ chỉ được khởi tạo lần đầu tiên hàm được gọi, không phải lúc chương trình khởi động, nên hoàn toàn tránh được vấn đề thứ tự:

#include <iostream>
#include <string>
class Logger {
public:
Logger() { std::cout << "Logger duoc khoi tao" << std::endl; }
void log(const std::string& msg) { std::cout << msg << std::endl; }
};
Logger& get_logger() {
static Logger instance; // Chỉ khởi tạo LẦN ĐẦU hàm này được gọi, dù được gọi từ file nào
return instance;
}
int main() {
get_logger().log("Hello!"); // An toàn - Logger chắc chắn đã được khởi tạo trước khi dùng
return 0;
}

Vì biến static cục bộ được khởi tạo lười (lazy initialization) ngay tại lần gọi hàm đầu tiên, không có vấn đề về thứ tự giữa các file - bất kể get_logger() được gọi từ file .cpp nào trước, Logger luôn được đảm bảo khởi tạo đúng lúc cần.

Thứ tự khởi tạo biến toàn cục giữa các file .cpp khác nhau không được C++ đảm bảo - có thể dẫn đến việc dùng một biến toàn cục trước khi nó thực sự được khởi tạo.

  • Tránh để một biến toàn cục ở file này phụ thuộc vào giá trị của biến toàn cục ở file khác lúc khởi tạo
  • Dùng mẫu “Construct On First Use” (hàm trả về tham chiếu đến biến static cục bộ) để đảm bảo thứ tự khởi tạo an toàn