Tham chiếu rỗng có khả thi không?


102

Đoạn mã này có hợp lệ (và hành vi được xác định) không?

int &nullReference = *(int*)0;

Cả hai g ++ và kêu vang ++ biên dịch nó mà không có bất kỳ cảnh báo, ngay cả khi sử dụng -Wall, -Wextra, -std=c++98, -pedantic, -Weffc++...

Tất nhiên tham chiếu không thực sự là null, vì nó không thể được truy cập (có nghĩa là bỏ tham chiếu đến một con trỏ null), nhưng chúng tôi có thể kiểm tra xem nó có null hay không bằng cách kiểm tra địa chỉ của nó:

if( & nullReference == 0 ) // null reference

1
Bạn có thể đưa ra bất kỳ trường hợp nào mà điều này thực sự hữu ích không? Nói cách khác, đây chỉ là một câu hỏi lý thuyết?
cdhowie

Chà, tài liệu tham khảo có bao giờ không thể thiếu? Con trỏ luôn có thể được sử dụng thay thế cho chúng. Một tham chiếu rỗng như vậy sẽ cho phép bạn sử dụng một tham chiếu khi bạn không thể có đối tượng để tham chiếu. Không biết nó bẩn đến mức nào, nhưng trước khi nghĩ đến nó tôi đã quan tâm đến tính hợp pháp của nó.
peoro

8
Tôi nghĩ rằng nó đã làm phiền
Mặc định

22
"we could check" - không, bạn không thể. Có những trình biên dịch biến câu lệnh thành if (false), loại bỏ dấu kiểm, chính xác là vì dù sao thì các tham chiếu cũng không thể rỗng. Một phiên bản tài liệu tốt hơn đã tồn tại trong nhân Linux, nơi kiểm tra NULL tương tự đã được tối ưu hóa: isc.sans.edu/diary.html?storyid=6820
MSalters

2
"một trong những lý do chính để sử dụng tham chiếu thay vì con trỏ là để giải phóng bạn khỏi gánh nặng phải kiểm tra xem nó có tham chiếu đến đối tượng hợp lệ hay không" câu trả lời này, trong liên kết của Default, nghe khá hay!
peoro

Câu trả lời:


75

Tài liệu tham khảo không phải là con trỏ.

8.3.2 / 1:

Một tham chiếu phải được khởi tạo để tham chiếu đến một đối tượng hoặc chức năng hợp lệ. [Lưu ý: đặc biệt, một tham chiếu null không thể tồn tại trong một chương trình được xác định rõ ràng, bởi vì cách duy nhất để tạo một tham chiếu như vậy là liên kết nó với “đối tượng” thu được bằng cách tham chiếu đến một con trỏ null, điều này gây ra hành vi không xác định. Như được mô tả trong 9.6, một tham chiếu không thể được liên kết trực tiếp với một trường bit. ]

1,9 / 4:

Một số hoạt động khác được mô tả trong tiêu chuẩn này là không xác định (ví dụ, ảnh hưởng của việc tham chiếu đến con trỏ null)

Như Johannes nói trong một câu trả lời đã xóa, có một số nghi ngờ rằng liệu "tham chiếu đến một con trỏ null" có nên được phân loại là hành vi không xác định hay không. Nhưng đây không phải là một trong những trường hợp làm dấy lên nghi ngờ, vì con trỏ null chắc chắn không trỏ đến một "đối tượng hoặc chức năng hợp lệ", và ủy ban tiêu chuẩn không muốn giới thiệu tham chiếu rỗng.


Tôi đã xóa câu trả lời của mình vì tôi nhận ra rằng vấn đề đơn thuần là bỏ tham chiếu đến một con trỏ null và nhận được một giá trị tham chiếu đến đó là một điều khác với thực sự ràng buộc một tham chiếu đến nó, như bạn đề cập. Mặc dù các giá trị được cho là cũng tham chiếu đến các đối tượng hoặc chức năng (vì vậy về điểm này, thực sự không có sự khác biệt đối với ràng buộc tham chiếu), hai điều này vẫn là những mối quan tâm riêng biệt. Đối với hành động đơn thuần là tham khảo, đây là liên kết: open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1102
Johannes Schaub - litb

1
@MSalters (trả lời bình luận về câu trả lời đã xóa; có liên quan ở đây) Tôi không thể đặc biệt đồng ý với logic được trình bày ở đó. Mặc dù có thể thuận tiện để &*plàm sáng tỏ như trên ptoàn cầu, nhưng điều đó không loại trừ hành vi không xác định (về bản chất của nó có thể "có vẻ hoạt động"); và tôi không đồng ý rằng một typeidbiểu thức tìm cách xác định loại "con trỏ null được tham chiếu đến" thực sự bỏ tham chiếu đến con trỏ null. Tôi đã thấy mọi người tranh luận nghiêm túc rằng &a[size_of_array]không thể và không nên dựa vào, và dù sao thì việc viết thư cũng dễ dàng và an toàn hơn a + size_of_array.
Karl Knechtel

@Default Tiêu chuẩn trong thẻ [c ++] phải cao. Câu trả lời của tôi nghe có vẻ như cả hai hành vi đều là một và giống nhau :) Trong khi tham khảo và nhận được giá trị, bạn không chuyển xung quanh mà đề cập đến "không có đối tượng" có thể khả thi, việc lưu trữ nó thành một tham chiếu sẽ thoát khỏi phạm vi giới hạn đó và đột nhiên có thể ảnh hưởng nhiều mã hơn.
Johannes Schaub - litb

@Karl tốt trong C ++, "dereferencing" không có nghĩa là đọc một giá trị. Một số người nghĩ rằng "dereference" có nghĩa là thực sự truy cập hoặc sửa đổi giá trị được lưu trữ, nhưng điều đó không đúng. Logic là C ++ nói rằng một lvalue đề cập đến "một đối tượng hoặc chức năng". Nếu đúng như vậy, thì câu hỏi là giá trị *ptham chiếu đến là gì , khi nào plà con trỏ null. C ++ hiện không có khái niệm về giá trị trống, mà số báo 232 muốn giới thiệu.
Johannes Schaub - litb

Việc phát hiện các con trỏ rỗng được tham chiếu trong typeidhoạt động dựa trên cú pháp, thay vì dựa trên ngữ nghĩa. Nghĩa là, nếu bạn làm typeid(0, *(ostream*)0)bạn làm có hành vi không xác định - không bad_typeidđảm bảo sẽ được ném ra, ngay cả khi bạn vượt qua một giá trị trái phát sinh từ một null pointer dereference ngữ nghĩa. Nhưng về mặt cú pháp ở cấp cao nhất, nó không phải là một tham chiếu, mà là một biểu thức toán tử dấu phẩy.
Johannes Schaub - litb

26

Câu trả lời tùy thuộc vào quan điểm của bạn:


Nếu bạn đánh giá theo tiêu chuẩn C ++, bạn không thể nhận được tham chiếu rỗng vì trước tiên bạn nhận được hành vi không xác định. Sau lần xuất hiện đầu tiên của hành vi không xác định, tiêu chuẩn cho phép mọi thứ xảy ra. Vì vậy, nếu bạn viết *(int*)0, bạn đã có hành vi không xác định như hiện tại, theo quan điểm tiêu chuẩn của ngôn ngữ, tham chiếu đến một con trỏ null. Phần còn lại của chương trình là không liên quan, một khi biểu thức này được thực thi, bạn đã ra khỏi trò chơi.


Tuy nhiên, trong thực tế, tham chiếu null có thể dễ dàng được tạo từ con trỏ null và bạn sẽ không nhận thấy cho đến khi bạn thực sự cố gắng truy cập giá trị đằng sau tham chiếu null. Ví dụ của bạn có thể hơi quá đơn giản, vì bất kỳ trình biên dịch tối ưu hóa tốt nào sẽ thấy hành vi không xác định và chỉ cần tối ưu hóa bất kỳ thứ gì phụ thuộc vào nó (tham chiếu null thậm chí sẽ không được tạo, nó sẽ được tối ưu hóa đi).

Tuy nhiên, việc tối ưu hóa đi phụ thuộc vào trình biên dịch để chứng minh hành vi không xác định, điều này có thể không thực hiện được. Hãy xem xét chức năng đơn giản này bên trong tệp converter.cpp:

int& toReference(int* pointer) {
    return *pointer;
}

Khi trình biên dịch nhìn thấy hàm này, nó không biết liệu con trỏ có phải là con trỏ null hay không. Vì vậy, nó chỉ tạo mã biến bất kỳ con trỏ nào thành tham chiếu tương ứng. (Btw: Đây là một vấn đề vì con trỏ và tham chiếu là cùng một con thú trong trình hợp dịch.) Bây giờ, nếu bạn có một tệp khác user.cppcó mã

#include "converter.h"

void foo() {
    int& nullRef = toReference(nullptr);
    cout << nullRef;    //crash happens here
}

trình biên dịch không biết điều đó toReference()sẽ bỏ qua con trỏ được truyền và giả sử rằng nó trả về một tham chiếu hợp lệ, điều này sẽ xảy ra là một tham chiếu rỗng trong thực tế. Cuộc gọi thành công, nhưng khi bạn cố gắng sử dụng tham chiếu, chương trình bị treo. Hy vọng. Tiêu chuẩn cho phép bất cứ điều gì xảy ra, bao gồm cả sự xuất hiện của những con voi màu hồng.

Bạn có thể hỏi tại sao điều này lại có liên quan, rốt cuộc, hành vi không xác định đã được kích hoạt bên trong toReference(). Câu trả lời là gỡ lỗi: Tham chiếu rỗng có thể lan truyền và phát triển giống như con trỏ null. Nếu bạn không biết rằng các tham chiếu rỗng có thể tồn tại và học cách tránh tạo chúng, bạn có thể mất khá nhiều thời gian để tìm ra lý do tại sao hàm thành viên của bạn dường như bị lỗi khi nó chỉ cố đọc một intthành viên cũ đơn thuần (answer: the instance trong lời gọi của thành viên là tham chiếu thisnull, con trỏ null cũng vậy, và thành viên của bạn được tính là nằm ở địa chỉ 8).


Vậy làm thế nào về việc kiểm tra các tham chiếu rỗng? Bạn đã cho dòng

if( & nullReference == 0 ) // null reference

trong câu hỏi của bạn. Chà, điều đó sẽ không hoạt động: Theo tiêu chuẩn, bạn có hành vi không xác định nếu bạn tham chiếu đến con trỏ null và bạn không thể tạo tham chiếu null mà không tham chiếu tới con trỏ null, vì vậy tham chiếu null chỉ tồn tại trong lĩnh vực hành vi không xác định. Vì trình biên dịch của bạn có thể giả định rằng bạn không kích hoạt hành vi không xác định, nên nó có thể giả định rằng không có cái gọi là tham chiếu null (mặc dù nó sẽ dễ dàng phát ra mã tạo ra tham chiếu null!). Như vậy, nó nhìn thấy if()điều kiện, kết luận rằng nó không thể đúng, và chỉ cần loại bỏ toàn bộ if()câu lệnh. Với sự ra đời của tối ưu hóa thời gian liên kết, việc kiểm tra các tham chiếu rỗng một cách rõ ràng đã trở nên rõ ràng.


TL; DR:

Tham chiếu rỗng có phần tồn tại kinh khủng:

Sự tồn tại của chúng dường như là không thể (= theo tiêu chuẩn),
nhưng chúng tồn tại (= bởi mã máy được tạo),
nhưng bạn không thể nhìn thấy chúng nếu chúng tồn tại (= nỗ lực của bạn sẽ được tối ưu hóa),
nhưng chúng có thể giết chết bạn mà không biết (= chương trình của bạn bị treo ở những điểm lạ hoặc tệ hơn).
Hy vọng duy nhất của bạn là chúng không tồn tại (= viết chương trình của bạn để không tạo ra chúng).

Tôi hy vọng điều đó sẽ không đến ám ảnh bạn!


2
Chính xác thì "ping voi" là gì?
Pharap

2
@Pharap Tôi không có manh mối, đó chỉ là lỗi đánh máy. Nhưng tiêu chuẩn C ++ sẽ không quan tâm đến việc nó có xuất hiện những chú voi màu hồng hay ping hay không ;-)
cmaster - phục hồi monica

9

Nếu ý định của bạn là tìm cách biểu diễn null trong một bảng liệt kê các đối tượng singleton, thì bạn nên dùng (de) tham chiếu null (nó là C ++ 11, nullptr).

Tại sao không khai báo đối tượng singleton tĩnh đại diện cho NULL trong lớp như sau và thêm toán tử ép kiểu con trỏ trả về nullptr?

Chỉnh sửa: Đã sửa một số lỗi sai và thêm câu lệnh if trong main () để kiểm tra toán tử truyền thành con trỏ thực sự hoạt động (mà tôi đã quên .. lỗi của tôi) - ngày 10 tháng 3 năm 2015 -

// Error.h
class Error {
public:
  static Error& NOT_FOUND;
  static Error& UNKNOWN;
  static Error& NONE; // singleton object that represents null

public:
  static vector<shared_ptr<Error>> _instances;
  static Error& NewInstance(const string& name, bool isNull = false);

private:
  bool _isNull;
  Error(const string& name, bool isNull = false) : _name(name), _isNull(isNull) {};
  Error() {};
  Error(const Error& src) {};
  Error& operator=(const Error& src) {};

public:
  operator Error*() { return _isNull ? nullptr : this; }
};

// Error.cpp
vector<shared_ptr<Error>> Error::_instances;
Error& Error::NewInstance(const string& name, bool isNull = false)
{
  shared_ptr<Error> pNewInst(new Error(name, isNull)).
  Error::_instances.push_back(pNewInst);
  return *pNewInst.get();
}

Error& Error::NOT_FOUND = Error::NewInstance("NOT_FOUND");
//Error& Error::NOT_FOUND = Error::NewInstance("UNKNOWN"); Edit: fixed
//Error& Error::NOT_FOUND = Error::NewInstance("NONE", true); Edit: fixed
Error& Error::UNKNOWN = Error::NewInstance("UNKNOWN");
Error& Error::NONE = Error::NewInstance("NONE");

// Main.cpp
#include "Error.h"

Error& getError() {
  return Error::UNKNOWN;
}

// Edit: To see the overload of "Error*()" in Error.h actually working
Error& getErrorNone() {
  return Error::NONE;
}

int main(void) {
  if(getError() != Error::NONE) {
    return EXIT_FAILURE;
  }

  // Edit: To see the overload of "Error*()" in Error.h actually working
  if(getErrorNone() != nullptr) {
    return EXIT_FAILURE;
  }
}

vì nó chậm
wandalen

6

clang ++ 3.5 thậm chí còn cảnh báo về nó:

/tmp/a.C:3:7: warning: reference cannot be bound to dereferenced null pointer in well-defined C++ code; comparison may be assumed to
      always evaluate to false [-Wtautological-undefined-compare]
if( & nullReference == 0 ) // null reference
      ^~~~~~~~~~~~~    ~
1 warning generated.
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.