Hầu hết các câu trả lời ở đây không giải quyết được sự mơ hồ vốn có trong việc có một con trỏ thô trong chữ ký hàm, về mặt thể hiện ý định. Các vấn đề như sau:
Người gọi không biết liệu con trỏ trỏ đến một đối tượng hay bắt đầu một "mảng" của các đối tượng.
Người gọi không biết liệu con trỏ có "sở hữu" bộ nhớ mà nó trỏ tới hay không. IE, có hay không chức năng sẽ giải phóng bộ nhớ. ( foo(new int)
- Đây có phải là rò rỉ bộ nhớ không?).
Người gọi không biết liệu nullptr
có thể được truyền an toàn vào chức năng hay không.
Tất cả những vấn đề này được giải quyết bằng cách tham khảo:
Tài liệu tham khảo luôn đề cập đến một đối tượng duy nhất.
Tài liệu tham khảo không bao giờ sở hữu bộ nhớ mà chúng đề cập đến, chúng chỉ đơn thuần là một cái nhìn vào bộ nhớ.
Tài liệu tham khảo không thể rỗng.
Điều này làm cho tài liệu tham khảo một ứng cử viên tốt hơn nhiều cho sử dụng chung. Tuy nhiên, tài liệu tham khảo không hoàn hảo - có một vài vấn đề lớn cần xem xét.
- Không có quyết định rõ ràng. Đây không phải là vấn đề với một con trỏ thô, vì chúng ta phải sử dụng
&
toán tử để cho thấy rằng chúng ta thực sự đang vượt qua một con trỏ. Ví dụ, int a = 5; foo(a);
không rõ ràng ở đây rằng a đang được thông qua tham chiếu và có thể được sửa đổi.
- Tính không ổn định. Điểm yếu này của con trỏ cũng có thể là một điểm mạnh, khi chúng ta thực sự muốn các tài liệu tham khảo của mình là null. Xem như
std::optional<T&>
không hợp lệ (vì lý do chính đáng), con trỏ cung cấp cho chúng tôi tính vô hiệu mà bạn muốn.
Vì vậy, có vẻ như khi chúng ta muốn một tài liệu tham khảo vô giá trị với sự gián tiếp rõ ràng, chúng ta nên đạt được một T*
quyền? Sai lầm!
Trừu tượng
Trong sự tuyệt vọng của chúng tôi về sự vô hiệu, chúng tôi có thể tiếp cận T*
và chỉ cần bỏ qua tất cả những thiếu sót và sự mơ hồ ngữ nghĩa được liệt kê trước đó. Thay vào đó, chúng ta nên tiếp cận với những gì C ++ làm tốt nhất: một sự trừu tượng hóa. Nếu chúng ta chỉ đơn giản viết một lớp bao quanh một con trỏ, chúng ta sẽ có được tính biểu cảm, cũng như tính vô hiệu và không rõ ràng.
template <typename T>
struct optional_ref {
optional_ref() : ptr(nullptr) {}
optional_ref(T* t) : ptr(t) {}
optional_ref(std::nullptr_t) : ptr(nullptr) {}
T& get() const {
return *ptr;
}
explicit operator bool() const {
return bool(ptr);
}
private:
T* ptr;
};
Đây là giao diện đơn giản nhất tôi có thể nghĩ ra, nhưng nó thực hiện công việc một cách hiệu quả. Nó cho phép khởi tạo tham chiếu, kiểm tra xem một giá trị có tồn tại và truy cập giá trị đó không. Chúng ta có thể sử dụng nó như vậy:
void foo(optional_ref<int> x) {
if (x) {
auto y = x.get();
// use y here
}
}
int x = 5;
foo(&x); // explicit indirection here
foo(nullptr); // nullability
Chúng tôi đã đạt được mục tiêu của chúng tôi! Bây giờ chúng ta hãy xem các lợi ích, so với con trỏ thô.
- Giao diện hiển thị rõ ràng rằng tham chiếu chỉ nên tham chiếu đến một đối tượng.
- Rõ ràng nó không sở hữu bộ nhớ mà nó đề cập đến, vì nó không có hàm hủy do người dùng định nghĩa và không có phương thức để xóa bộ nhớ.
- Người gọi biết
nullptr
có thể được thông qua, vì tác giả hàm rõ ràng đang yêu cầu mộtoptional_ref
Chúng ta có thể làm cho giao diện phức tạp hơn từ đây, chẳng hạn như thêm toán tử đẳng thức, giao diện đơn get_or
và map
giao diện, một phương thức nhận giá trị hoặc ném ngoại lệ,constexpr
hỗ trợ. Điều đó có thể được thực hiện bởi bạn.
Tóm lại, thay vì sử dụng các con trỏ thô, hãy suy luận về ý nghĩa của những con trỏ đó trong mã của bạn và tận dụng sự trừu tượng của thư viện tiêu chuẩn hoặc viết riêng của bạn. Điều này sẽ cải thiện mã của bạn đáng kể.
new
tạo con trỏ và các vấn đề về quyền sở hữu.