Một số mã "thực tế" (cách hài hước để đánh vần "lỗi") đã bị hỏng trông như thế này:
void foo(X* p) {
p->bar()->baz();
}
và nó đã quên tính đến thực tế là p->bar()
đôi khi trả về một con trỏ null, điều đó có nghĩa là việc hủy bỏ cuộc gọi mà nó gọi baz()
là không xác định.
Không phải tất cả các mã đã bị hỏng có chứa rõ ràng if (this == nullptr)
hoặc if (!p) return;
kiểm tra. Một số trường hợp chỉ đơn giản là các hàm không truy cập bất kỳ biến thành viên nào và do đó đã xuất hiện hoạt động tốt. Ví dụ:
struct DummyImpl {
bool valid() const { return false; }
int m_data;
};
struct RealImpl {
bool valid() const { return m_valid; }
bool m_valid;
int m_data;
};
template<typename T>
void do_something_else(T* p) {
if (p) {
use(p->m_data);
}
}
template<typename T>
void func(T* p) {
if (p->valid())
do_something(p);
else
do_something_else(p);
}
Trong mã này khi bạn gọi func<DummyImpl*>(DummyImpl*)
bằng một con trỏ null, có một sự quy định "khái niệm" của con trỏ để gọi p->DummyImpl::valid()
, nhưng thực tế là hàm thành viên chỉ trả về false
mà không truy cập *this
. Điều đó return false
có thể được nội tuyến và vì vậy trong thực tế, con trỏ không cần phải được truy cập. Vì vậy, với một số trình biên dịch có vẻ như nó hoạt động tốt: không có segfault cho dereferences null, p->valid()
là sai, vì vậy các cuộc gọi mã do_something_else(p)
, kiểm tra các con trỏ null, và do đó không có gì. Không có sự cố hoặc hành vi bất ngờ được quan sát.
Với GCC 6, bạn vẫn nhận được cuộc gọi đến p->valid()
, nhưng trình biên dịch bây giờ xâm nhập vào biểu thức đó p
phải là null (nếu không p->valid()
sẽ là hành vi không xác định) và ghi chú thông tin đó. Thông tin được suy luận đó được sử dụng bởi trình tối ưu hóa để nếu cuộc gọi do_something_else(p)
được nội tuyến, if (p)
kiểm tra hiện được coi là dư thừa, bởi vì trình biên dịch nhớ rằng nó không phải là null và do đó, nội tuyến mã sẽ:
template<typename T>
void func(T* p) {
if (p->valid())
do_something(p);
else {
// inlined body of do_something_else(p) with value propagation
// optimization performed to remove null check.
use(p->m_data);
}
}
Điều này bây giờ thực sự không có con trỏ null, và do đó mã mà trước đây xuất hiện để làm việc ngừng hoạt động.
Trong ví dụ này, lỗi đã xảy ra func
, trước tiên nên kiểm tra null (hoặc người gọi không bao giờ nên gọi nó bằng null):
template<typename T>
void func(T* p) {
if (p && p->valid())
do_something(p);
else
do_something_else(p);
}
Một điểm quan trọng cần nhớ là hầu hết các tối ưu hóa như thế này không phải là trường hợp trình biên dịch nói rằng "ah, lập trình viên đã kiểm tra con trỏ này với null, tôi sẽ loại bỏ nó chỉ để gây phiền nhiễu". Điều gì xảy ra là các tối ưu hóa khác nhau như truyền nội tuyến và lan truyền phạm vi giá trị kết hợp với nhau để làm cho các kiểm tra đó trở nên dư thừa, bởi vì chúng xuất hiện sau một kiểm tra trước đó, hoặc một quy định. Nếu trình biên dịch biết rằng một con trỏ không rỗng tại điểm A trong một hàm và con trỏ không thay đổi trước một điểm B sau đó trong cùng một hàm, thì nó biết rằng nó cũng không phải là null tại B. Khi xảy ra nội tuyến các điểm A và B thực sự có thể là các đoạn mã ban đầu ở các hàm riêng biệt, nhưng giờ được kết hợp thành một đoạn mã và trình biên dịch có thể áp dụng kiến thức của mình rằng con trỏ không rỗng ở nhiều nơi hơn.