이 포인터가 null 인 이유

Oct 01 2020

Visual Studio에서 멤버 변수에 대한 포인터는 장면 뒤에서 32 비트 부호있는 정수인 것처럼 보이며 (64 비트 모드에서도) 해당 컨텍스트에서 널 포인터는 -1입니다. 그래서 다음과 같은 수업이 있다면 :

#include <iostream>
#include <cstdint>

struct Foo
{
    char arr1[INT_MAX];
    char arr2[INT_MAX];
    char ch1;
    char ch2;
};


int main()
{
    auto p = &Foo::ch2;
    std::cout << (p?"Not null":"null") << '\n';
}

컴파일하고 "null"을 인쇄합니다. 그래서 내가 어떤 종류의 정의되지 않은 동작을 일으키고 있습니까, 아니면 컴파일러 가이 코드를 거부해야했는데 이것이 컴파일러의 버그입니까?

편집하다:

"2 INT_MAX배열 + 2 문자"패턴을 유지할 수있는 것으로 보이며이 경우에만 컴파일러에서 원하는만큼 멤버를 추가 할 수 있으며 두 번째 문자는 항상 null로 간주됩니다. 데모를 참조하십시오 . 패턴을 약간 변경하면 (어떤 시점에서 2 대신 1 또는 3 문자와 같이) 클래스가 너무 크다고 불평합니다.

답변

8 computerquip Oct 08 2020 at 08:52

객체의 크기 제한은 표준 [1]의 부록 B에 따라 정의 된 구현입니다. 당신의 구조체는 터무니없는 크기입니다.

구조체가 다음과 같은 경우 :

struct Foo
{
    char arr1[INT_MAX];
    //char arr2[INT_MAX];
    char ch1;
    char ch2;
};

... 비교적 최신 버전의 64 비트 MSVC에서 구조체 크기는 약 2147483649 바이트 인 것으로 보입니다. 그런 다음 arr2를 추가하면 갑자기 sizeof가 Foo의 크기가 1임을 알려줍니다.

C ++ 표준 (부록 B)은 컴파일러가 MSVC가하는 제한 사항을 문서화해야한다고 명시하고 있습니다 [2]. 권장 제한을 따릅니다. 부록 B, 섹션 2.17은 객체 크기에 대해 권장되는 262144 (?) 제한을 제공합니다. MSVC가 그 이상을 처리 할 수 ​​있다는 것은 분명하지만, 최소 권장 사항을 따른다는 것을 문서화하므로 개체 크기가 그 이상일 때주의해야한다고 가정합니다.

[1] http://eel.is/c++draft/implimits

[2] https://docs.microsoft.com/en-us/cpp/cpp/compiler-limits?view=vs-2019

3 BenVoigt Oct 08 2020 at 23:20

이는 포인터 대 멤버 표현에 대한 최적화 (가상 기반이없는 경우 4 바이트의 스토리지 만 사용)와 피전 홀 원칙 사이의 충돌입니다.

유형의 하위 객체를 X포함 하는 유형 의 경우 ... 유형의 멤버에 대한 유효한 포인터 가 있습니다. 각 하위 객체에 대해 하나씩, 멤버에 대한 널 포인터에 대해 하나씩.NcharN+1char X::*

이는 멤버 포인터 표시에 최소한 N + 1 개의 고유 값이있을 때 작동하며, 4 바이트 표시의 경우 N+1<= 2 32를 의미 하므로 최대 오브젝트 크기는 2 32-1 입니다.

불행히도 문제의 컴파일러는 최대 객체 유형 크기 (프로그램을 거부하기 전)를 2 32로 만들었는데, 이는 하나가 너무 커서 비둘기 구멍 문제를 일으 킵니다. 최소 한 쌍의 멤버에 대한 포인터는 구별 할 수 없어야합니다. 멤버에 대한 널 포인터가이 쌍의 절반 일 필요는 없지만이 구현에서 관찰 한 바와 같이 그렇습니다.

1 Swift-FridayPie Oct 08 2020 at 15:44

표현식 &Foo::ch2은 char Foo::*클래스 멤버에 대한 포인터 인 유형 Foo입니다. 규칙에 따라 변환 된 멤버에 대한 포인터 bool는 널 포인터 인 경우에만 거짓으로 평가되어야 nullptr합니다.

여기서 결함은 구현의 결함으로 보입니다. 즉, -march = x86-64를 사용 하는 gcc 컴파일러 에서 다음 코드로 nullptr이 할당되지 않은 경우 멤버에 할당 된 모든 포인터는 null이 아닌 (1)로 평가됩니다.

struct foo
{
    char arr1[LLONG_MAX];
    char arr2[LLONG_MAX];
    char ch1;
    char ch2;
};

int main()
{
    char  foo::* p1 = &foo::ch1;
    char  foo::* p2 = &foo::ch2;
    std::cout << (p1?"Not null ":"null ") << '\n';
    std::cout << (p2?"Not null ":"null ") << '\n';
    
    std::cout << LLONG_MAX + LLONG_MAX << '\n';
    std::cout << ULLONG_MAX << '\n';
    std::cout << offsetof(foo, ch1) << '\n';
}

산출:

Not null 
null 
-2
18446744073709551615
18446744073709551614

클래스 크기가 플랫폼 제한을 초과하여 멤버 오프셋이 0 (nullptr의 내부 값)으로 둘러싸여 있다는 사실과 관련이있을 수 있습니다. 컴파일러가이를 감지하지 못하는 이유는 서명 된 값이있는 정수 오버플로와 서명 된 리터럴을 배열 크기로 사용하여 컴파일러 내에서 UB를 발생시키는 것은 프로그래머의 잘못입니다 . LLONG_MAX + LLONG_MAX = -2는 2의 "크기"가됩니다. 결합 된 배열.

기본적으로 처음 두 멤버의 크기는 음수 로 계산되고 ch1의 오프셋은 -2unsigned 로 표시됩니다 18446744073709551614. 그리고 -2그러므로 포인터는 널 (null)이 아닙니다. 다른 컴파일러는 nullptr을 생성하는 값을 0으로 고정하거나 실제로 clang이 수행하는 것처럼 기존 문제를 감지 할 수 있습니다.

오프셋하면 ch1되고 -2, 다음의 오프셋 (offset) ch2IS -1? 이것을 추가합시다 :

std::cout << reinterpret_cast<signed long long&&> (offsetof(foo, ch1)) << '\n';
std::cout << reinterpret_cast<signed long long&&> (offsetof(foo, ch2)) << '\n';

추가 출력 :

-2
-1

그리고 첫 번째 멤버의 오프셋은 분명히 0이며 포인터가 오프셋을 나타내는 경우을 나타내는 다른 값이 필요합니다 nullptr. 이 특정 컴파일러 -1가 다른 구현의 경우 일 수도 있고 아닐 수도있는 null 값으로 만 간주 한다고 가정하는 것이 논리적 입니다.

BarrnetChou Oct 01 2020 at 09:29

코드를 테스트 할 때 VS는 Foo: the class is too large.

추가 char arr3[INT_MAX]하면 Visual Studio에서 Error C2089 'Foo': 'struct' too large. Microsoft Docs는이를 다음과 같이 설명합니다.The specified structure or union exceeds the 4GB limit.