깨진 "실제"( "버기"철자를 쓰는 재미있는 방법) 코드 중 일부는 다음과 같습니다.
void foo(X* p) {
p->bar()->baz();
}
p->bar()
때로는 널 포인터를 리턴 한다는 사실을 설명하는 것을 잊어 버렸습니다. 즉, 호출하기 위해 역 참조 baz()
가 정의되지 않았 음을 의미합니다 .
깨진 모든 코드에 명시 적이 if (this == nullptr)
거나 if (!p) return;
검사가 포함 된 것은 아닙니다 . 어떤 경우에는 단순히 멤버 변수에 액세스하지 않은 함수이므로 정상적으로 작동하는 것처럼 보입니다 . 예를 들면 다음과 같습니다.
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);
}
이 코드에서 전화 할 때 func<DummyImpl*>(DummyImpl*)
널 포인터로 포인터에 대한 "개념적"역 참조가 p->DummyImpl::valid()
있지만 실제로 해당 멤버 함수는 false
액세스하지 않고 리턴합니다 *this
. 그것은 return false
인라인 될 수 있으므로 실제로 포인터에 전혀 액세스 할 필요가 없습니다. 따라서 일부 컴파일러에서는 정상적으로 작동하는 것처럼 보입니다. null을 역 참조하는 segfault가없고 p->valid()
false이므로 코드가 do_something_else(p)
null 포인터를 확인 하는 코드를 호출 하고 아무것도 수행하지 않습니다. 충돌 또는 예기치 않은 동작이 관찰되지 않습니다.
GCC 6을 사용하면 여전히 p->valid()
하지만 컴파일러는 이제 p
null이 아닌 (그렇지 않으면 p->valid()
정의되지 않은 동작) 표현식을 유추하고 해당 정보를 기록합니다. 추론 된 정보는 옵티 마이저에 의해 사용되므로, 호출 do_something_else(p)
이 인라인되면 if (p)
검사가 중복되지 않은 것으로 간주됩니다. 컴파일러는 그것이 null이 아니라는 것을 기억하고 코드를 다음과 같이 인라인하기 때문입니다.
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);
}
}
이것은 실제로 널 포인터를 역 참조하므로 이전에 작동했던 것으로 보이는 코드는 작동을 멈 춥니 다.
이 예제에서 버그는에 있으며 func
, 먼저 null을 확인 했어야합니다 (또는 호출자가 null을 사용하여 호출 한 적이 없어야 함).
template<typename T>
void func(T* p) {
if (p && p->valid())
do_something(p);
else
do_something_else(p);
}
기억해야 할 중요한 점은 이와 같은 대부분의 최적화는 컴파일러가 "아, 프로그래머가 널에 대해이 포인터를 테스트했다면 성가 시게하기 위해 제거 할 것"이라고 말하는 컴파일러의 경우가 아니라는 것입니다. 결과적으로 인라인 및 값 범위 전파와 같은 다양한 수준의 최적화가 결합되어 이러한 검사가 중복 검사를 수행합니다. 이는 이전 검사 또는 역 참조 이후에 발생하기 때문입니다. 컴파일러가 함수의 A 지점에서 포인터가 널이 아님을 알고 동일한 함수의 B 지점 이전에 포인터가 변경되지 않으면 B에서도 널이 아님을 알게됩니다. 점 A와 B는 실제로는 별도의 함수에 있었지만 이제는 하나의 코드로 결합 된 코드 일 수 있으며 컴파일러는 포인터가 더 많은 위치에서 널이 아님에 대한 지식을 적용 할 수 있습니다.