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

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

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.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)을 참조하십시오.


답변

귀하의 예에서 *(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 그런 다음 배열의 첫 번째 요소에 대한 포인터로 사용할 수 있습니다.


답변

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

경고 : 정의되지 않은 동작

#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]’에 대한 유효한 포인터라고 말합니다. 그것은 좋을 수 없으며 결코 의도가 아닙니다.


답변