null 참조가 가능합니까?


102

이 코드 조각이 유효합니까 (그리고 정의 된 동작)?

int &nullReference = *(int*)0;

둘 다 g ++ 및 그 소리 ++ 컴파일을 경고없이 사용하는 경우에도 -Wall, -Wextra, -std=c++98, -pedantic, -Weffc++...

물론 참조는 액세스 할 수 없기 때문에 실제로는 null이 아니지만 (null 포인터를 역 참조하는 것을 의미합니다) 주소를 확인하여 null인지 여부를 확인할 수 있습니다.

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

1
이것이 실제로 유용 할 경우를 말씀해 주시겠습니까? 즉, 이것은 단지 이론적 인 질문입니까?
cdhowie

음, 참고 문헌이 없어서는 안 될까요? 포인터는 항상 대신 사용할 수 있습니다. 이러한 null 참조참조 할 개체가 없을 때에도 참조를 사용할 수 있도록합니다. 얼마나 더 러웠는지 모르지만 생각하기 전에 합법성에 관심이있었습니다.
peoro


22
"우리는 확인할 수 있습니다"-아니, 당신은 할 수 없습니다. 구문을로 바꾸고 if (false)검사를 제거하는 컴파일러가 있습니다. 참조는 어쨌든 null이 될 수 없기 때문입니다. 더 나은 문서화 된 버전이 Linux 커널에 존재했으며, 매우 유사한 NULL 검사가 최적화되었습니다. isc.sans.edu/diary.html?storyid=6820
MSalters

2
"포인터 대신 참조를 사용하는 주된 이유 중 하나는 유효한 객체를 참조하는지 확인하기 위해 테스트해야하는 부담에서 벗어나기위한 것입니다."Default의 링크에서이 대답은 꽤 좋은 것 같습니다!
peoro

답변:


75

참조는 포인터가 아닙니다.

8.3.2 / 1 :

참조는 유효한 객체 또는 함수를 참조하도록 초기화되어야합니다. [참고 : 특히 널 참조는 잘 정의 된 프로그램에 존재할 수 없습니다. 이러한 참조를 만드는 유일한 방법은 널 포인터를 역 참조하여 얻은 "객체"에 바인딩하는 것이므로 정의되지 않은 동작이 발생하기 때문입니다. 9.6에 설명 된대로 참조는 비트 필드에 직접 바인딩 될 수 없습니다. ]

1.9 / 4 :

다른 특정 연산은이 국제 표준에서 정의되지 않은 것으로 설명되어 있습니다 (예 : 널 포인터 역 참조의 효과).

Johannes가 삭제 된 답변에서 말했듯이 "널 포인터를 비정의하는"이 정의되지 않은 동작으로 분류되어야하는지 여부는 의심의 여지가 있습니다. 그러나 null 포인터는 확실히 "유효한 개체 또는 함수"를 가리 키지 않고 표준위원회 내에서 null 참조를 도입하려는 욕구가 없기 때문에 이것은 의심을 불러 일으키는 사례 중 하나가 아닙니다.


나는 당신이 언급했듯이 널 포인터를 역 참조하고 그것을 참조하는 lvalue를 얻는 것이 실제로 참조를 바인딩하는 것과는 다른 문제라는 것을 깨달았 기 때문에 내 대답을 제거했습니다. lvalue는 객체 나 함수도 참조한다고 말하지만 (따라서이 시점에서는 참조 바인딩과는 차이가 없습니다)이 두 가지 사항은 여전히 ​​별개의 문제입니다. 단순한 역 참조를위한 링크는 다음과 같습니다. open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1102
Johannes Schaub-litb

1
@MSalters (삭제 된 답변에 대한 댓글에 대한 답변, 여기 관련) 여기에 제시된 논리에 특히 동의 할 수 없습니다. 이 생략하다하는 것이 편리 할 수 있지만 &*p같은 p보편적으로, 그 (그것의 본질 "제대로 작동"수) 정의되지 않은 동작을 배제하지 않는다; 그리고 나는 typeid"역 참조 된 널 포인터"의 유형을 결정하려는 표현식이 실제로 널 포인터를 역 참조 한다는 것에 동의하지 않습니다 . 나는 사람들이 믿을 &a[size_of_array]수없고 믿을 수 없다고 진지하게 주장하는 것을 보았고 , 어쨌든 그냥 작성하는 것이 더 쉽고 안전하다 a + size_of_array.
Karl Knechtel 2010

[c ++] 태그의 @Default Standards는 높아야합니다. 내 대답은 두 행위가 하나이고 같은 것 같았습니다. :) "객체 없음"을 참조하는 전달하지 않는 lvalue를 역 참조하고 가져 오는 것은 실현 가능할 수 있지만, 참조에 저장하면 제한된 범위를 벗어나 갑자기 영향을 미칠 수 있습니다. 훨씬 더 많은 코드.
Johannes Schaub-litb

@Karl은 C ++에서 잘 작동합니다. "dereferencing"은 값을 읽는 것을 의미하지 않습니다. 어떤 사람들은 "역 참조"가 저장된 값에 실제로 액세스하거나 수정하는 것을 의미한다고 생각하지만 사실이 아닙니다. 논리는 C ++에서 lvalue가 "객체 또는 함수"를 참조한다고 말하는 것입니다. 그렇다면 질문은 lvalue가 *p나타내는 것이 무엇인지 , 언제 pnull 포인터인지입니다. C ++에는 현재 이슈 232가 소개하고자하는 빈 lvalue 개념이 없습니다.
Johannes Schaub-litb

typeid의미론을 기반으로하지 않고 구문을 기반으로 하는 작업 에서 역 참조 된 널 포인터 감지 . 즉, 당신이 할 경우, typeid(0, *(ostream*)0)당신은 어떻게 정의되지 않은 동작을 - 아니 bad_typeid당신이 의미 널 포인터 역 참조로 인한 좌변을 통과하더라도, 슬로우 보장됩니다. 그러나 구문 상 최상위 수준에서는 역 참조가 아니라 쉼표 연산자 표현식입니다.
Johannes Schaub-litb

26

대답은 귀하의 관점에 따라 다릅니다.


C ++ 표준으로 판단하면 먼저 정의되지 않은 동작이 발생하므로 null 참조를 얻을 수 없습니다. 정의되지 않은 동작이 처음 발생한 후 표준은 모든 일이 발생하도록 허용합니다. 따라서을 작성 *(int*)0하면 언어 표준 관점에서 널 포인터를 역 참조하는 그대로 정의되지 않은 동작이 이미 있습니다. 프로그램의 나머지 부분은 무관합니다. 일단이 표현식이 실행되면 게임에서 벗어나게됩니다.


그러나 실제로는 null 포인터에서 null 참조를 쉽게 만들 수 있으며 실제로 null 참조 뒤에있는 값에 액세스하려고 할 때까지 알 수 없습니다. 좋은 최적화 컴파일러는 정의되지 않은 동작을 확인하고 이에 의존하는 모든 것을 최적화하기 때문에 예제가 너무 간단 할 수 있습니다 (널 참조는 생성되지 않고 최적화됩니다).

그러나 이러한 최적화는 컴파일러가 정의되지 않은 동작을 증명하는 데 달려 있으며 이는 불가능할 수 있습니다. 파일 내에서 다음과 같은 간단한 함수를 고려하십시오 converter.cpp.

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

컴파일러는이 함수를 볼 때 포인터가 널 포인터인지 여부를 알지 못합니다. 따라서 포인터를 해당 참조로 바꾸는 코드를 생성합니다. (Btw : 포인터와 참조가 어셈블러에서 똑같은 짐승이기 때문에 이것은 noop입니다.) 이제 user.cpp코드 가있는 다른 파일이 있다면

#include "converter.h"

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

컴파일러는 toReference()전달 된 포인터를 역 참조 할 것인지 알지 못하며 실제로는 null 참조가 될 유효한 참조를 반환한다고 가정합니다. 호출은 성공하지만 참조를 사용하려고하면 프로그램이 충돌합니다. 바라건대. 표준은 분홍 코끼리의 모습을 포함하여 모든 일이 일어날 수 있도록 허용합니다.

이것이 왜 관련이 있는지 물어볼 수 있습니다. 결국 정의되지 않은 동작이 이미 내부에서 트리거되었습니다 toReference(). 대답은 디버깅입니다. Null 참조는 null 포인터처럼 전파되고 확산 될 수 있습니다. 널 참조가 존재할 수 있다는 것을 모르고 생성을 피하는 방법을 배우면, 단순한 이전 int멤버 를 읽으려고 할 때 멤버 함수가 충돌하는 이유를 파악하는 데 상당한 시간을 소비 할 수 있습니다 (답 : 인스턴스 멤버 호출에서 null 참조가 있었으므로 thisnull 포인터도 마찬가지 이며 멤버는 주소 8로 위치하도록 계산됩니다.


그렇다면 null 참조를 확인하는 것은 어떻습니까? 당신은 줄을 주었다

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

귀하의 질문에. 음, 작동하지 않습니다. 표준에 따르면 널 포인터를 역 참조하면 정의되지 않은 동작이 있고, 널 ​​포인터를 역 참조하지 않고는 널 참조를 만들 수 없으므로 널 참조는 정의되지 않은 동작의 영역 내에 만 존재합니다. 컴파일러는 정의되지 않은 동작을 트리거하지 않는다고 가정 할 수 있으므로 null 참조와 같은 것이 없다고 가정 할 수 있습니다 ( Null 참조 를 생성하는 코드를 쉽게 생성하더라도!). 따라서 if()조건을 확인하고 사실 일 수 없다고 결론을 내리고 전체 if()진술을 버립니다 . 링크 시간 최적화가 도입됨에 따라 강력한 방식으로 null 참조를 확인하는 것이 불가능 해졌습니다.


TL; DR :

Null 참조는 다소 무시 무시한 존재입니다.

그들의 존재는 불가능 해 보이지만 (= 표준에 의해)
존재하지만 (= 생성 된 기계 코드에 의해)
존재하는지 볼 수는 없지만 (= 당신의 시도는 최적화 될 것입니다),
어쨌든 그들은 당신을 모르게 죽일 수 있습니다 (= 프로그램이 이상한 지점에서 충돌하거나 더 나쁜 경우).
당신의 유일한 희망은 그것들이 존재하지 않는다는 것입니다 (= 그것들을 만들지 않도록 프로그램을 작성하십시오).

나는 그것이 당신을 괴롭히지 않기를 바랍니다!


2
"핑 코끼리"란 정확히 무엇입니까?
Pharap

2
@Pharap 나는 단서가 없습니다, 단지 오타였습니다. 그러나 C ++ 표준은 어쨌든 나타나는 분홍색 코끼리인지 핑 코끼리인지 상관하지 않습니다 ;-)
cmaster-monica reinstate monica

9

만약 당신의 의도가 싱글 톤 객체의 열거에서 null을 표현하는 방법을 찾는 것이라면, null을 (de) 참조하는 것은 나쁜 생각입니다 (그것은 C ++ 11, nullptr).

다음과 같이 클래스 내에서 NULL을 나타내는 정적 싱글 톤 객체를 선언하고 nullptr을 반환하는 캐스트-포인터 연산자를 추가하지 않는 이유는 무엇입니까?

편집 : 몇 가지 잘못된 유형을 수정하고 실제로 작동하는 캐스트-포인터 연산자를 테스트하기 위해 main ()에 if- 문을 추가했습니다.

// 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;
  }
}

느리기 때문에
wandalen

6

clang ++ 3.5는 이에 대해 경고합니다.

/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.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.