올바른 주소와 유형을 가진 포인터가 C ++ 17 이후로 여전히 유효한 포인터입니까?


84

( 이 질문과 답변을 참조하십시오 .)

C ++ 17 표준 이전에는 [basic.compound] / 3 에 다음 문장이 포함되었습니다 .

유형 T의 객체가 주소 A에있는 경우 값이 주소 A 인 cv T * 유형의 포인터는 값을 획득 한 방법에 관계없이 해당 객체를 가리 킵니다.

그러나 C ++ 17 이후로이 문장은 제거되었습니다 .

예를 들어이 문장은이 예제 코드를 정의했으며 C ++ 17 이후로 정의되지 않은 동작이라고 생각합니다.

 alignas(int) unsigned char buffer[2*sizeof(int)];
 auto p1=new(buffer) int{};
 auto p2=new(p1+1) int{};
 *(p1+1)=10;

C ++ 17 전에 p1+1에 주소를 가지고 *p2있으므로, 오른쪽 유형이 *(p1+1)에 대한 포인터입니다 *p2. C ++에서 17 p1+1A는 포인터 과거 - 더 - 끝 그것이되지 않도록, 객체에 대한 포인터 와 나는 dereferencable하지 믿습니다.

표준 권리의 수정에 대한 해석입니까, 아니면 인용 된 문장의 삭제를 보상하는 다른 규칙이 있습니까?


참고 : [basic.stc.dynamic.safety] 및 [util.dynamic.safety]에 포인터 출처에 대한 새 / 업데이트 된 규칙이 있습니다
MM

@MM 빈 집합 인 엄격한 포인터 안전성을 가진 구현에서만 중요합니다 (실험 오류 내에서).
TC

4
인용 된 진술은 실제로 실제로 사실이 아닙니다. 주어진 int a, b = 0;, 당신은 *(&a + 1) = 1;체크해도 할 수 없습니다 &a + 1 == &b. 주소를 추측하여 객체에 대한 유효한 포인터를 얻을 수 있다면 레지스터에 지역 변수를 저장하는 것조차 문제가됩니다.
TC

@TC 1) 주소를 가져온 후 어떤 컴파일러가 reg에 var를 넣습니까? 2) 주소를 측정하지 않고 어떻게 정확하게 추측합니까?
curiousguy

@curiousguy 정확히 그렇기 때문에 다른 수단 (예 : 추측)으로 얻은 숫자를 객체가 발생하는 주소로 캐스팅하는 것이 문제가됩니다. 해당 객체에 별칭을 지정하지만 컴파일러는 인식하지 못합니다. 대조적으로 객체의 주소를 사용하면 컴파일러가 경고를 받고 그에 따라 동기화합니다.
피터 - 분석 재개 모니카

답변:


45

이 표준 권리의 수정에 대한 해석입니까, 아니면이 인용문의 삭제를 보상하는 다른 규칙이 있습니까?

예,이 해석은 맞습니다. 끝을 지나는 포인터는 단순히 해당 주소를 가리키는 다른 포인터 값으로 변환 할 수 없습니다.

새로운 [basic.compound] / 3 는 다음과 같이 말합니다.

포인터 유형의 모든 값은 다음 중 하나입니다.
(3.1) 개체 또는 함수에 대한 포인터 (포인터는 개체 또는 함수를 가리킨다 고 함) 또는
(3.2) 개체 끝을 지나는 포인터 ([expr .add]) 또는

그것들은 상호 배타적입니다. p1+1객체에 대한 포인터가 아니라 끝을 지나는 포인터입니다. p1+1가 아닌 x[1]에서 크기 1 배열 의 가설 을 가리 킵니다 . 이 두 개체는 포인터 상호 변환이 불가능합니다.p1p2

비표준 메모도 있습니다.

[참고 : 개체의 끝 ([expr.add])을 지나는 포인터는 해당 주소에있을 수있는 개체 유형의 관련없는 개체를 가리키는 것으로 간주되지 않습니다. [...]

의도를 명확히합니다.


TC는 다수의 의견 (에서 지적 하듯이 , 특히이 중 하나 )이 실제로 구현하는 시도와 함께 제공되는 문제의 특수한 경우 std::vector이다 - [v.data(), v.data() + v.size())아직 요구가 유효한 범위로하고 vector소위, 배열 객체를 생성하지 않습니다 정의 된 포인터 산술 만 벡터의 주어진 객체에서 가상의 단일 크기 배열의 끝을 지나서 이동합니다. 더 많은 리소스를 보려면 CWG 2182 , 이 표준 토론 및 주제에 대한 두 가지 개정판 인 P0593R0P0593R1 (특히 섹션 1.3)을 참조하십시오.


3
이 예제는 기본적으로 알려진 " vector구현 성 문제" 의 특별한 경우입니다 . +1.
TC

2
@Oliv 일반적인 경우는 C ++ 03 이후 존재했습니다. 근본 원인은 배열 객체가 없기 때문에 포인터 산술이 예상대로 작동하지 않는 것입니다.
TC

1
@TC 유일한 문제는 포인터 산술에 대한 제한에서 비롯된 것이라고 믿었습니다. 이 문장 삭제가 새로운 문제를 추가하지 않습니까? 코드 예제는 pre-C ++ 17에서도 UB입니까?
Oliv

1
@Oliv 포인터 산술이 고정되어 있으면 p1+1더 이상 끝을 지나는 포인터를 생성하지 않고 끝을 지나는 포인터에 대한 전체 논의는 논쟁의 여지가 있습니다. 당신의 특별한 두 가지 요소가있는 특별한 경우는 17 세 이전 UB가 아닐 수도 있지만 그다지 흥미롭지도 않습니다.
TC

5
@TC이 "벡터 구현 문제"에 대해 읽을 수있는 곳을 알려줄 수 있습니까?
SirGuy

8

귀하의 예에서 *(p1 + 1) = 10;UB 는 크기 1 의 배열 끝을 지나기 때문에 UB 여야합니다 . 그러나 배열이 더 큰 char 배열에서 동적으로 구성 되었기 때문에 여기서는 매우 특별한 경우입니다.

동적 객체 생성은 C ++ 표준의 n4659 초안 4.5 C ++ 객체 모델 [intro.object] §3에 설명되어 있습니다.

3 "array of N unsigned char"유형 또는 "array of N std :: byte"(21.2.1) 유형의 다른 객체 e와 연관된 스토리지에 완전한 객체가 생성 (8.3.4)되면 해당 어레이는 스토리지를 제공합니다. 생성 된 객체에 대해 다음과 같은 경우 :
(3.1) — e의 수명이 시작되고 끝나지 않았고,
(3.2) — 새 객체의 스토리지가 e 내에 완전히 들어 맞고
(3.3) — 이러한 항목을 충족하는 더 작은 배열 객체가 없습니다. 제약.

3.3은 다소 불분명 해 보이지만 아래 예제는 의도를 더 명확하게합니다.

struct A { unsigned char a[32]; };
struct B { unsigned char b[16]; };
A a;
B *b = new (a.a + 8) B; // a.a provides storage for *b
int *p = new (b->b + 4) int; // b->b provides storage for *p
// a.a does not provide storage for *p (directly),
// but *p is nested within a (see below)

실시 예에 따라서, buffer어레이는 스토리지 제공 모두 *p1*p2.

다음 단락 모두에 대한 완전한 객체 것을 증명 *p1하고 *p2있습니다 buffer:

4 다음과 같은 경우 객체 a는 다른 객체 b 내에 중첩됩니다.
(4.1) — a는 b의 하위 객체이거나
(4.2) — b는 a 또는
(4.3)에 대한 스토리지를 제공합니다. — a 가 c 내에 중첩 된 객체 c가있는 경우 , c는 b 내에 중첩됩니다.

5 모든 객체 x에 대해 다음과 같이 결정되는 x의 완전한 객체라고하는 일부 객체가 있습니다.
(5.1) — x가 완전한 객체이면 x의 완전한 객체는 그 자체입니다.
(5.2) — 그렇지 않으면 x의 완전한 객체는 x를 포함하는 (고유 한) 객체의 완전한 객체입니다.

이것이 확립되면 C ++ 17 용 n4659 초안의 다른 관련 부분은 [basic.coumpound] §3 (내 강조)입니다.

3 ... 포인터 유형의 모든 값은 다음 중 하나입니다.
(3.1) — 객체 또는 함수에 대한 포인터 (포인터는 객체 또는 함수를 가리킨다 고 함) 또는
(3.2) — 끝을 지나는 포인터 객체 (8.7) 또는
(3.3)-해당 유형에 대한 널 포인터 값 (7.11) 또는
(3.4)-유효하지 않은 포인터 값.

객체의 끝을 가리키는 포인터이거나 객체의 끝을 지나는 포인터 유형의 값은 객체가 차지하는 메모리 (4.4) 의 첫 번째 바이트 또는 객체가 차지하는 스토리지의 끝 이후 메모리의 첫 번째 바이트의 주소를 나타냅니다. , 각각. [참고 : 개체의 끝 (8.7)을 지나는 포인터는 관련없는 항목 을 가리키는 것으로 간주되지 않습니다.해당 주소에있을 수있는 개체 유형의 개체입니다. 포인터 값은 표시하는 스토리지가 스토리지 기간의 끝에 도달하면 무효화됩니다. 6.7 참조. —end note] 포인터 산술 (8.7) 및 비교 (8.9, 8.10)를 위해 n 개 요소로 구성된 배열 x의 마지막 요소 끝을 지나는 포인터는 가상 요소 x [에 대한 포인터와 동일한 것으로 간주됩니다. 엔]. 포인터 유형의 값 표현은 구현에 따라 정의됩니다. 레이아웃 호환 유형에 대한 포인터는 동일한 값 표현 및 정렬 요구 사항 (6.11)을 가져야합니다.

노트의 끝 과거 포인터는 ... 객체가 가리키는 때문에 여기에 적용되지 않습니다 p1p2하지 관련이없는 : 포인터를 arithmetics 스토리지를 제공하는 객체 내부 의미하기 때문에,하지만 같은 완전한 개체로 중첩 된 p2 - p1정의를하고있다 (&buffer[sizeof(int)] - buffer]) / sizeof(int)그것은 1입니다.

그래서 p1 + 1 대한 포인터 *p2이며 *(p1 + 1) = 10;동작을 정의하고의 값을 설정했습니다 *p2.


또한 C ++ 14와 현재 (C ++ 17) 표준 간의 호환성에 대한 C4 부록을 읽었습니다. 단일 문자 배열에서 동적으로 생성 된 객체간에 포인터 산술을 사용할 가능성을 제거하는 것은 IMHO가 일반적으로 사용되는 기능이기 때문에 여기에서 인용해야하는 중요한 변경 사항입니다. 호환성 페이지에 아무것도 없기 때문에 금지하는 것이 표준의 의도가 아님을 확인한 것 같습니다.

특히, 기본 생성자가없는 클래스에서 객체 배열의 일반적인 동적 생성을 무효화합니다.

class T {
    ...
    public T(U initialization) {
        ...
    }
};
...
unsigned char *mem = new unsigned char[N * sizeof(T)];
T * arr = reinterpret_cast<T*>(mem); // See the array as an array of N T
for (i=0; i<N; i++) {
    U u(...);
    new(arr + i) T(u);
}

arr 그런 다음 배열의 첫 번째 요소에 대한 포인터로 사용할 수 있습니다.


아하, 그래서 세상은 미쳐 있지 않았습니다. +1
StoryTeller-Unslander Monica

@StoryTeller : 저도 희망합니다. 또한 호환성 섹션에 그것에 대한 단어가 없습니다. 하지만 반대 의견이 더 많은 평판을 얻은 것 같습니다 ...
Serge Ballesta

2
비 규범 적 노트에서 "관련되지 않은"단일 단어를 포착하고 포인터 산술을 관장하는 [expr.add]의 규범 적 규칙과 모순되는 의미를 지니게됩니다. 일반적인 경우의 포인터 산술이 어떤 표준에서도 작동하지 않았기 때문에 Annex C에는 아무것도 없습니다. 깨질 것이 없습니다.
TC

3
@TC : Google은이 "벡터 구현 문제"에 대한 정보를 찾는 데 매우 도움이되지 않습니다. 도와 주시겠습니까?
Matthieu M.

6
@MatthieuM. 참조 핵심 문제 2182 , STD-토론 게시글, P0593R0P0593R1 (특히 1.3 절) . 기본적인 문제는 vector배열 객체를 생성 할 수없고 생성 할 수 없다는 것입니다.하지만 사용자가 포인터 산술을 지원하는 포인터를 얻을 수 있도록하는 인터페이스가 있습니다 (배열 객체에 대한 포인터에 대해서만 정의 됨).
TC

1

여기에 제공된 답변을 확장하기 위해 수정 된 문구에서 제외되는 내용의 예가 있습니다.

경고 : 정의되지 않은 동작

#include <iostream>
int main() {
    int A[1]{7};
    int B[1]{10};
    bool same{(B)==(A+1)};

    std::cout<<B<< ' '<< A <<' '<<sizeof(*A)<<'\n';
    std::cout<<(same?"same":"not same")<<'\n';
    std::cout<<*(A+1)<<'\n';//!!!!!  
    return 0;
}

완전히 구현에 의존하는 (그리고 깨지기 쉬운) 이유 때문에이 프로그램의 가능한 결과는 다음과 같습니다.

0x7fff1e4f2a64 0x7fff1e4f2a60 4
same
10

이 출력은 두 배열 (이 경우)이 메모리에 저장되어의 '끝을 지나서 하나' A가의 첫 번째 요소의 주소 값을 보유하는 것을 보여줍니다 B.

개정 된 사양은 관계 A+1B. '값을 얻는 방법에 관계없이'라는 구구에서는 'A + 1'이 'B [0]'을 가리키면 'B [0]'에 대한 유효한 포인터라고 말합니다. 그것은 좋을 수 없으며 결코 의도가 아닙니다.


이것은 또한 파생 클래스 또는 사용자 지정 할당 자 new가 사용자 지정 크기 배열을 지정할 수 있도록 구조체 끝에 빈 배열을 사용하는 것을 효과적으로 금지합니까? 아마도 새로운 문제는 "어떻게 상관없이"에 관한 것일 수 있습니다. 유효한 방법과 위험한 방법이 있습니까?
Gem Taylor

@Persixty 따라서 포인터 개체의 값은 개체의 바이트에 의해 결정되며 다른 것은 없습니다. 따라서 상태가 동일한 두 개체는 동일한 개체를 가리 킵니다. 하나가 유효하면 다른 것도 마찬가지입니다. 따라서 포인터 값이 숫자로 표시되는 일반적인 아키텍처에서 동일한 값을 가진 두 개의 포인터는 동일한 객체를 가리키고 끝 중 하나는 동일한 다른 객체를 가리 킵니다.
curiousguy

@Persixty 또한 사소한 유형은 유형의 가능한 값을 열거 할 수 있음을 의미합니다. 기본적으로 모든 최적화 모드의 모든 최신 컴파일러 ( -O0일부 컴파일러 에서도 )는 포인터를 사소한 유형으로 간주하지 않습니다. 컴파일러는 표준의 요구 사항을 진지하게 다루지 않으며 표준을 작성하는 사람들, 다른 언어를 꿈꾸고 기본 원칙에 직접적으로 모순되는 모든 종류의 발명품을 만드는 사람들도 마찬가지입니다. 분명히 사용자는 컴파일러 버그에 대해 불평 할 때 혼란스럽고 때로는 나쁘게 취급됩니다.
curiousguy

질문의 비 규범 적 메모는 우리가 '과거 끝'을 아무것도 가리 키지 않는 것으로 생각하기를 원합니다. 우리는 실제로 무언가를 가리키는 것이 좋을 수 있고 실제로는 그것을 역 참조 할 수 있다는 것을 알고 있습니다. 그러나 (표준에 따르면) 유효한 프로그램이 아닙니다. 우리는 포인터가 끝까지 산술을 통해 얻어 졌음을 알고 역 참조되면 예외를 발생시키는 구현을 상상할 수 있습니다 . 나는 그것을하는 플랫폼을 알고 있습니다. 나는 표준이 그것을 배제하고 싶지 않다고 생각합니다.
Persixty

@curiousguy 또한 가능한 값을 열거하여 무엇을 의미하는지 잘 모르겠습니다. 이는 C ++에서 정의한 사소한 유형의 필수 기능이 아닙니다.
Persixty
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.