45. Luôn cung cấp new và delete cùng nhau

Estimated reading: 6 minutes 696 views

Tóm tắt

Chúng là một gói trọn bộ: Mọi hàm nạp chồng đặc thù của lớp void* operator new(parms) phải được đi kèm với một hàm nạp chồng tương ứng void operator delete(void*, parms), trong đó parms là danh sách các kiểu tham số bổ sung (trong đó tham số đầu tiên luôn là std::size_t). Điều tương tự cũng áp dụng cho các dạng mảng new[] và delete[].

Thảo luận

Bạn hiếm khi cần cung cấp new hoặc delete tùy chỉnh, nhưng nếu bạn cần một cái, bạn thường cần cả hai. Nếu bạn định nghĩa một T::operator new đặc thù của lớp để thực hiện việc cấp phát đặc biệt nào đó, rất có thể bạn cũng cần định nghĩa T::operator delete đặc thù của lớp để thực hiện việc thu hồi tương ứng.

Điều đó có thể khá cơ bản, nhưng có một lý do tinh tế hơn cho Mục này: Trình biên dịch có thể đang khao khát một bản nạp chồng của T::operator delete ngay cả khi bạn không bao giờ thực sự gọi nó. Đó là lý do tại sao bạn luôn cần cung cấp operator new và operator delete (và operator new[] cùng operator delete[]) theo cặp.

Giả sử bạn định nghĩa một lớp với việc cấp phát tùy chỉnh:

				
					class T {
    static void* operator new(std::size_t);
    static void* operator new(std::size_t, CustomAllocator&);
    static void operator delete(void*, std::size_t);
};				
			

Bạn thiết lập một giao thức đơn giản cho việc cấp phát và thu hồi:

  • Người gọi có thể cấp phát các đối tượng kiểu T bằng bộ cấp phát mặc định (dùng new T) hoặc bộ cấp phát tùy chỉnh (dùng new(alloc) T trong đó alloc là một đối tượng kiểu CustomAllocator).

  • Hàm operator delete duy nhất mà người gọi có thể gọi là hàm mặc định operator delete(size_t), vì vậy tất nhiên bạn cài đặt sao cho nó thu hồi bộ nhớ chính xác dù được cấp phát theo cách nào.

Đến đây thì mọi thứ vẫn ổn.

Tuy nhiên, trình biên dịch vẫn cần bí mật gọi một bản nạp chồng khác của delete, cụ thể là T::operator delete(size_t, CustomAllocator&). Điều này là do câu lệnh T* p = new(alloc) T; thực sự mở rộng thành một cái gì đó giống như:

				
					// mã do trình biên dịch sinh ra cho T* p = new(alloc) T;
void* _compilerTemp = T::operator new(sizeof(T), alloc);
T* p;
try {
    p = new (_compilerTemp) T; // khởi tạo một T tại địa chỉ _compilerTemp
}
catch (...) { // hàm tạo thất bại, chú ý chỗ này...
    T::operator delete(_compilerTemp, sizeof(T), alloc);
    throw;
}				
			

Vì vậy, khá logic, trình biên dịch tự động chèn mã để gọi T::operator delete tương ứng cho T::operator new đã nạp chồng nếu việc cấp phát thành công nhưng hàm tạo (constructor) thất bại. Chữ ký “tương ứng” là void operator delete(void*, bất-kỳ-tham-số-nào-new-nhận).

Đây là phần “con sâu làm rầu nồi canh”. Chuẩn C++ (trong [C++03] §5.3.4(17)) quy định rằng mã ở trên sẽ được sinh ra khi và chỉ khi bản nạp chồng đó của operator delete thực sự tồn tại. Nếu không, mã sẽ không gọi bất kỳ operator delete nào trong trường hợp hàm tạo thất bại. Nói cách khác: Nếu hàm tạo thất bại, bộ nhớ sẽ bị rò rỉ (leak). Trong số sáu trình biên dịch phổ biến được thử nghiệm tại thời điểm viết bài này, chỉ có hai đưa ra cảnh báo trong các tình huống như vậy.

Đó là lý do tại sao mọi bản nạp chồng void* operator new(parms) phải đi kèm với bản nạp chồng tương ứng void operator delete(void*, parms)—bởi vì trình biên dịch muốn tự mình gọi chúng.

Ngoại lệ

Dạng tại chỗ (in-place form) của operator new:

 
void* T::operator new(size_t, void* p) { return p; }

 

không cần một operator delete tương ứng vì không có việc cấp phát thực sự nào diễn ra. Tất cả các trình biên dịch chúng tôi đã kiểm tra đều không đưa ra cảnh báo sai nào liên quan đến sự vắng mặt của void T::operator delete(void*, size_t, void*).

Leave a Comment

Chia sẻ:

45. Luôn cung cấp new và delete cùng nhau

Or copy link

CONTENTS