[c++] 널 인스턴스에서 멤버 함수를 호출하면 언제 정의되지 않은 동작이 발생합니까?

다음 코드를 고려하십시오.

#include <iostream>

struct foo
{
    // (a):
    void bar() { std::cout << "gman was here" << std::endl; }

    // (b):
    void baz() { x = 5; }

    int x;
};

int main()
{
    foo* f = 0;

    f->bar(); // (a)
    f->baz(); // (b)
}

(b)해당 멤버가 없기 때문에 충돌이 예상 됩니다.xnull 포인터에 . 실제로 포인터가 사용되지 (a)않기 때문에 충돌하지 않습니다 this.

(b)역 참조 하기 때문에this 포인터 ( (*this).x = 5;), 그리고 this널은 널 (null)를 역 참조 항상 정의되지 않은 동작이라고합니다으로,이 프로그램은 정의되지 않은 동작에 들어갑니다.

않습니다 (a)정의되지 않은 동작이 발생할? 두 함수 (및 x)가 모두 정적 이면 어떨까요?



답변

모두 (a)(b)정의되지 않은 동작이 발생할. 널 포인터를 통해 멤버 함수를 호출하는 것은 항상 정의되지 않은 동작입니다. 함수가 정적 인 경우 기술적으로도 정의되지 않지만 일부 분쟁이 있습니다.


가장 먼저 이해해야 할 것은 널 포인터를 역 참조하는 것이 정의되지 않은 동작 인 이유입니다. C ++ 03에서는 실제로 여기에 약간의 모호성이 있습니다.

비록 “정의되지 않은 동작에 널 포인터 결과를 역 참조” §1.9 / 4, §8.3.2 / 4 모두 노트에 언급, 그것은 명시 적으로 언급 한 적이 없어요. (참고는 비표준입니다.)

그러나 §3.10 / 2에서 추론 할 수 있습니다.

lvalue는 개체 또는 함수를 나타냅니다.

역 참조 할 때 결과는 lvalue입니다. 널 포인터 객체를 참조 lvalue를 사용할 때 정의되지 않은 동작이 있습니다. 문제는 이전 문장이 결코 언급되지 않았다는 것입니다. 그래서 lvalue를 “사용”한다는 것은 무엇을 의미합니까? 아예 생성하거나 lvalue에서 rvalue로 변환하는보다 형식적인 의미에서 사용 하시겠습니까?

어쨌든 rvalue (§4.1 / 1)로 변환 할 수는 없습니다.

lvalue가 참조하는 객체가 T 유형의 객체가 아니고 T에서 파생 된 유형의 객체가 아니거나 객체가 초기화되지 않은 경우이 변환을 필요로하는 프로그램은 정의되지 않은 동작을 갖습니다.

여기에는 확실히 정의되지 않은 동작이 있습니다.

모호성은 정의되지 않은 동작인지 여부에서 비롯 되지만 유효하지 않은 포인터의 값을 사용 하지 않습니다 (즉, lvalue를 가져 오지만 rvalue로 변환하지 않음). 그렇지 않다면 int *i = 0; *i; &(*i);잘 정의되어 있습니다. 이것은 활발한 문제 입니다.

따라서 우리는 엄격한 “널 포인터 역 참조, 정의되지 않은 동작 가져 오기”뷰와 약한 “비 참조 된 널 포인터 사용, 정의되지 않은 동작 얻기”뷰가 있습니다.

이제 우리는 질문을 고려합니다.


예, (a)정의되지 않은 동작이 발생합니다. 사실 this이 null이면 함수내용에 관계없이 결과는 정의되지 않습니다.

이것은 §5.2.5 / 3에서 다음과 같습니다.

E1“클래스 X에 대한 포인터”유형이있는 경우 표현식 E1->E2은 동등한 형식으로 변환됩니다.(*(E1)).E2;

*(E1)엄격한 해석으로 정의되지 않은 동작이 발생 .E2하고 rvalue로 변환되어 약한 해석에 대해 정의되지 않은 동작이됩니다.

또한 (§9.3.1 / 1)에서 직접 정의되지 않은 동작입니다.

X 유형이 아니거나 X에서 파생 된 유형의 객체에 대해 클래스 X의 비 정적 멤버 함수가 호출되면 동작이 정의되지 않습니다.


정적 함수를 사용하면 엄격한 해석과 약한 해석이 차이를 만듭니다. 엄밀히 말하면 정의되지 않았습니다.

정적 멤버는 클래스 멤버 액세스 구문을 사용하여 참조 될 수 있으며,이 경우 object-expression이 평가됩니다.

즉, 정적이 아닌 것처럼 평가되고 다시 한 번 (*(E1)).E2.

그러나 E1정적 멤버 함수 호출에서 사용되지 않기 때문에 약한 해석을 사용하면 호출이 잘 정의됩니다. *(E1)결과가 lvalue이고 정적 함수가 해결되고 *(E1)삭제되고 함수가 호출됩니다. lvalue에서 rvalue 로의 변환이 없으므로 정의되지 않은 동작이 없습니다.

C ++ 0x에서는 n3126부터 모호성이 남아 있습니다. 지금은 안전합니다 : 엄격한 해석을 사용하십시오.


답변

물론 그것의 의미 정의되지 않은 정의되지를 하지만, 경우에 따라서는 예측 될 수 있습니다. 내가 제공하려는 정보는 확실히 보장되지 않기 때문에 작업 코드에 의존해서는 안되지만 디버깅 할 때 유용 할 수 있습니다.

객체 포인터에서 함수를 호출하면 포인터가 역 참조되고 UB가 발생한다고 생각할 수 있습니다. 함수가 가상이 아닌 경우 실제로, 컴파일러는 첫 번째 매개 변수로 포인터를 전달하는 일반 함수 호출로 변환 한 것 참조 해제를 우회하고 호출 된 멤버 함수에 대한 시한 폭탄을 만들어. 멤버 함수가 멤버 변수 나 가상 함수를 참조하지 않으면 실제로 오류없이 성공할 수 있습니다. 성공은 “정의되지 않은”우주에 속한다는 것을 기억하십시오!

Microsoft의 MFC 함수 GetSafeHwnd는 실제로이 동작에 의존합니다. 나는 그들이 무엇을 피우고 있었는지 모릅니다.

가상 함수를 호출하는 경우 vtable에 도달하기 위해 포인터를 역 참조해야하며 확실히 UB를 얻을 것입니다 (아마도 충돌이 발생하지만 보장 할 수 없음을 기억하십시오).


답변