코드가 얼마나 ‘나쁜’것과 상관없이, 정렬 등이 컴파일러 / 플랫폼에서 문제가되지 않는다고 가정하면이 동작이 정의되지 않았거나 깨졌습니까?
다음과 같은 구조체가 있으면 :-
struct data
{
int a, b, c;
};
struct data thing;
그것은이다 법적 접근 a
, b
및 c
등 (&thing.a)[0]
, (&thing.a)[1]
그리고 (&thing.a)[2]
?
모든 경우에 모든 컴파일러와 플랫폼에서 시도해 보았고 모든 설정에서 ‘작동’했습니다. 컴파일러가 b 와 thing [1] 이 같은 것이고 ‘b’에 저장하는 것이 레지스터에 저장되고 thing [1]이 메모리에서 잘못된 값을 읽는다는 것을 인식하지 못할 수도 있다는 점이 걱정 됩니다 (예 :). 모든 경우에 나는 그것이 옳은 일을 시도했습니다. (물론 그게 많이 증명되지는 않음을 알고 있습니다)
이것은 내 코드가 아닙니다. 작업해야하는 코드입니다. 이것이 잘못된 코드인지 깨진 코드 인지에 관심이 있습니다. 코드
태그가 지정된 C 및 C ++. 저는 대부분 C ++에 관심이 있지만 C ++이 다르면 C에도 관심이 있습니다.
답변
불법입니다 1 . 이것은 C ++에서 정의되지 않은 동작입니다.
멤버를 배열 방식으로 취하고 있지만 다음은 C ++ 표준이 말하는 것입니다 (강조 표시).
[dcl.array / 1] : … 배열 유형의 객체에는T 유형의 N 개의 하위 객체가 연속적으로 할당 된 비어 있지 않은 집합이 포함됩니다.
그러나 회원에게는 다음과 같은 연속적인 요구 사항 이 없습니다 .
[class.mem / 17] : …; 구현 정렬 요구 사항으로 인해 인접한 두 멤버가 서로 바로 할당되지 않을 수 있습니다 …
위의 두 따옴표는 왜 a struct
로 인덱싱 하는 것이 C ++ 표준에 의해 정의 된 동작이 아닌지 힌트하기에 충분해야합니다. 한 가지 예를 선택해 보겠습니다. 표현식을보세요 (&thing.a)[2]
-아래 첨자 연산자 관련 :
[expr.post//expr.sub/1] :
접미사 식 뒤에 대괄호로 묶인식이 접미사 식입니다. 식 중 하나는 “T의 배열”유형의 glvalue이거나 “T에 대한 포인터”유형의 prvalue이고 다른 하나는 범위가 지정되지 않은 열거 또는 정수 유형의 prvalue입니다. 결과는 “T”유형입니다. 유형 “T”는 완전히 정의 된 객체 유형이어야합니다 .66
표현식E1[E2]
은 (정의상) 다음과 동일합니다.((E1)+(E2))
포인터 유형에 정수 유형을 추가하는 것과 관련하여 위 인용문의 굵은 텍스트를 파헤칩니다 (여기에서 강조 표시) ..
[expr.add / 4] : 정수형을 가진 표현식을 포인터에 더하거나 뺄 때 결과는 포인터 피연산자의 유형을 갖습니다. 표현식
이 n 개의 요소가 있는 배열 객체의 요소를가리키는 경우 , 표현식및(값이있는곳)은 (가설적인) 요소를 가리 킵니다.
if; 그렇지 않으면 동작이 정의되지 않습니다. …P
x[i]
x
P + J
J + P
J
j
x[i + j]
0 ≤ i + j ≤ n
if 절의 배열 요구 사항에 유의하십시오 . 그렇지 않으면 그렇지 않으면 위의 인용이다. 표현식은 분명히 if 절에 적합하지 않습니다 . 따라서 정의되지 않은 동작입니다.(&thing.a)[2]
보조 노트에 : 비록 나는 광범위하게 코드와 다양한 컴파일러에 미치는 변화를 실험하고 그들은 (이 여기에 패딩을 소개하지 않는 작동합니다 ) 유지 관리 관점에서 볼 때 코드는 매우 취약합니다. 이 작업을 수행하기 전에 구현이 멤버를 연속적으로 할당했다고 주장해야합니다. 그리고 인바운드 유지 :-). 그러나 여전히 정의되지 않은 동작 ….
다른 답변에서 일부 실행 가능한 해결 방법 (정의 된 동작 포함)이 제공되었습니다.
주석에서 올바르게 지적했듯이 , 이전 편집에 있던 [basic.lval / 8] 은 적용되지 않습니다. @ 2501 및 @MM 감사합니다.
1 : thing.a
이 parttern을 통해 구조체 멤버에 액세스 할 수있는 유일한 법적 사례에 대해서는이 질문에 대한 @Barry의 답변을 참조하십시오 .
답변
아니요. C에서는 패딩이 없어도 정의되지 않은 동작입니다.
정의되지 않은 동작을 유발하는 것은 범위를 벗어난 액세스 1 입니다. 스칼라 (구조체의 멤버 a, b, c)가 있고이를 배열 2 로 사용하여 다음 가상 요소에 액세스 하려고 하면 같은 유형의 다른 객체가있는 경우에도 정의되지 않은 동작이 발생합니다. 그 주소.
그러나 구조체 객체의 주소를 사용하여 오프셋을 특정 멤버로 계산할 수 있습니다.
struct data thing = { 0 };
char* p = ( char* )&thing + offsetof( thing , b );
int* b = ( int* )p;
*b = 123;
assert( thing.b == 123 );
이는 각 멤버에 대해 개별적으로 수행되어야하지만 배열 액세스와 유사한 함수에 넣을 수 있습니다.
1 (인용 : ISO / IEC 9899 : 201x 6.5.6 가산 연산자 8)
결과가 배열 객체의 마지막 요소를 하나 지나면 평가되는 단항 * 연산자의 피연산자로 사용되지 않습니다.
2 (인용 : ISO / IEC 9899 : 201x 6.5.6 가산 연산자 7)
이러한 연산자의 목적을 위해 배열의 요소가 아닌 객체에 대한 포인터는 첫 번째 요소에 대한 포인터와 동일하게 작동합니다. 객체의 유형을 요소 유형으로 갖는 길이 1의 배열.
답변
C ++에서 정말로 필요한 경우-operator [] 생성 :
struct data
{
int a, b, c;
int &operator[]( size_t idx ) {
switch( idx ) {
case 0 : return a;
case 1 : return b;
case 2 : return c;
default: throw std::runtime_error( "bad index" );
}
}
};
data d;
d[0] = 123; // assign 123 to data.a
작동이 보장 될뿐만 아니라 사용법이 더 간단하고 읽을 수없는 표현을 작성할 필요가 없습니다. (&thing.a)[0]
참고 :이 답변은 이미 필드가있는 구조가 있고 인덱스를 통해 액세스를 추가해야한다는 가정하에 제공됩니다. 속도가 문제이고 구조를 변경할 수 있다면 더 효과적 일 수 있습니다.
struct data
{
int array[3];
int &a = array[0];
int &b = array[1];
int &c = array[2];
};
이 솔루션은 구조의 크기를 변경하므로 메소드도 사용할 수 있습니다.
struct data
{
int array[3];
int &a() { return array[0]; }
int &b() { return array[1]; }
int &c() { return array[2]; }
};
답변
C ++의 경우 : 이름을 모르고 멤버에 액세스해야하는 경우 멤버 변수에 대한 포인터를 사용할 수 있습니다.
struct data {
int a, b, c;
};
typedef int data::* data_int_ptr;
data_int_ptr arr[] = {&data::a, &data::b, &data::c};
data thing;
thing.*arr[0] = 123;
답변
ISO C99 / C11에서 공용체 기반 유형 실행은 합법적이므로 비 배열에 대한 포인터를 인덱싱하는 대신 사용할 수 있습니다 (다양한 다른 답변 참조).
ISO C ++는 공용체 기반 유형 실행을 허용하지 않습니다. GNU C ++는 확장 기능으로 수행하며 일반적으로 GNU 확장을 지원하지 않는 다른 컴파일러는 공용체 유형 실행을 지원한다고 생각합니다. 그러나 그것은 엄격하게 이식 가능한 코드를 작성하는 데 도움이되지 않습니다.
현재 버전의 gcc 및 clang에서 a switch(idx)
를 사용하여 멤버 를 선택 하는 C ++ 멤버 함수를 작성하면 컴파일 시간 상수 인덱스에 대해 최적화되지만 런타임 인덱스에 대해 끔찍한 분기 asm이 생성됩니다. 이것에 switch()
대해 본질적으로 잘못된 것은 없습니다 . 이것은 현재 컴파일러에서 놓친 최적화 버그입니다. 그들은 Slava의 switch () 함수를 효율적으로 컴파일 할 수 있습니다.
이에 대한 해결책 / 해결 방법은 다른 방법으로 수행하는 것입니다. 클래스 / 구조체에 배열 멤버를 제공하고 접근 자 함수를 작성하여 특정 요소에 이름을 첨부합니다.
struct array_data
{
int arr[3];
int &operator[]( unsigned idx ) {
// assert(idx <= 2);
//idx = (idx > 2) ? 2 : idx;
return arr[idx];
}
int &a(){ return arr[0]; } // TODO: const versions
int &b(){ return arr[1]; }
int &c(){ return arr[2]; }
};
Godbolt 컴파일러 탐색기 에서 다양한 사용 사례에 대한 asm 출력을 볼 수 있습니다 . 이는 완전한 x86-64 System V 함수이며, 인라인시 얻을 수있는 내용을 더 잘 보여주기 위해 후행 RET 명령어가 생략되었습니다. ARM / MIPS / 어떤 것이 든 비슷합니다.
# asm from g++6.2 -O3
int getb(array_data &d) { return d.b(); }
mov eax, DWORD PTR [rdi+4]
void setc(array_data &d, int val) { d.c() = val; }
mov DWORD PTR [rdi+8], esi
int getidx(array_data &d, int idx) { return d[idx]; }
mov esi, esi # zero-extend to 64-bit
mov eax, DWORD PTR [rdi+rsi*4]
이에 비해 @Slava의 대답 switch()
은 C ++ 용을 사용하여 런타임 변수 인덱스에 대해 asm을 이와 같이 만듭니다. (이전 Godbolt 링크의 코드).
int cpp(data *d, int idx) {
return (*d)[idx];
}
# gcc6.2 -O3, using `default: __builtin_unreachable()` to promise the compiler that idx=0..2,
# avoiding an extra cmov for idx=min(idx,2), or an extra branch to a throw, or whatever
cmp esi, 1
je .L6
cmp esi, 2
je .L7
mov eax, DWORD PTR [rdi]
ret
.L6:
mov eax, DWORD PTR [rdi+4]
ret
.L7:
mov eax, DWORD PTR [rdi+8]
ret
이것은 C (또는 GNU C ++) 공용체 기반 유형 punning 버전에 비해 분명히 끔찍합니다.
c(type_t*, int):
movsx rsi, esi # sign-extend this time, since I didn't change idx to unsigned here
mov eax, DWORD PTR [rdi+rsi*4]
답변
C ++에서 이것은 대부분 정의되지 않은 동작입니다 (어떤 인덱스에 따라 다름).
[expr.unary.op]에서 :
포인터 산술 (5.7) 및 비교 (5.9, 5.10)를 위해 이러한 방식으로 주소를 취하는 배열 요소가 아닌 객체는 유형의 요소가 하나 인 배열에 속하는 것으로 간주됩니다
T
.
따라서 표현식 &thing.a
은 하나의 배열을 참조하는 것으로 간주됩니다 int
.
[expr.sub]에서 :
표현
E1[E2]
은 (정의상) 다음과 동일합니다.*((E1)+(E2))
그리고 [expr.add]에서 :
정수 유형이있는 표현식을 포인터에 더하거나 빼면 결과는 포인터 피연산자의 유형을 갖습니다. 발현 경우
P
소자에 점x[i]
배열 객체x
와n
요소, 표현P + J
하고J + P
(단,J
값을 갖는j
제 (아마도-가설) 소자) 포인트x[i + j]
경우0 <= i + j <= n
; 그렇지 않으면 동작이 정의되지 않습니다.
(&thing.a)[0]
는 &thing.a
크기 1의 배열로 간주되고 첫 번째 인덱스를 사용 하기 때문에 완벽하게 잘 구성 됩니다. 허용되는 인덱스입니다.
(&thing.a)[2]
전제 조건을 위반하는 0 <= i + j <= n
, 우리가 가지고 있기 때문에 i == 0
, j == 2
, n == 1
. 단순히 포인터를 구성하는 &thing.a + 2
것은 정의되지 않은 동작입니다.
(&thing.a)[1]
흥미로운 경우입니다. 실제로 [expr.add]의 어떤 것도 위반하지 않습니다. 우리는 배열의 끝을 지나서 포인터를 가져갈 수 있습니다. 여기에서 [basic.compound]의 메모를 살펴 보겠습니다.
객체의 끝을 가리키는 포인터이거나 객체의 끝을 지나는 포인터 유형의 값은 객체가 차지하는 메모리 (1.7)의 첫 번째 바이트 주소 (1.7) 또는 객체가 차지하는 스토리지의 끝 이후 메모리의 첫 번째 바이트를 나타냅니다. , 각각. [참고 : 개체 끝 (5.7)을 지나는 포인터는 해당 주소에있을 수있는 개체 유형의 관련없는 개체를 가리키는 것으로 간주되지 않습니다.
따라서 포인터를 사용하는 &thing.a + 1
것은 정의 된 동작이지만 참조를 해제하는 것은 아무 것도 가리 키지 않기 때문에 정의되지 않습니다.
답변
이것은 정의되지 않은 동작입니다.
C ++에는 여러분이하고있는 일을 이해하고 최적화 할 수 있도록 컴파일러에게 희망을 주려는 많은 규칙이 있습니다.
앨리어싱 (두 가지 다른 포인터 유형을 통해 데이터에 액세스), 배열 경계 등에 대한 규칙이 있습니다.
변수가있는 x
경우 배열의 구성원이 아니라는 사실은 컴파일러가 []
기반 배열 액세스가이를 수정할 수 없다고 가정 할 수 있음을 의미 합니다. 따라서 사용할 때마다 메모리에서 데이터를 지속적으로 다시로드 할 필요가 없습니다. 누군가가 이름을 수정할 수있는 경우에만 .
따라서 (&thing.a)[1]
컴파일러는를 참조하지 않는다고 가정 할 수 있습니다 thing.b
. 이 사실을 사용하여 읽기 및 쓰기 순서를 변경할 수 있습니다.thing.b
지정하여 실제로 수행하도록 지시 한 내용을 무효화하지 않고 원하는 작업을 무효화 할 수 있습니다.
이것의 고전적인 예는 const를 버리는 것입니다.
const int x = 7;
std::cout << x << '\n';
auto ptr = (int*)&x;
*ptr = 2;
std::cout << *ptr << "!=" << x << '\n';
std::cout << ptr << "==" << &x << '\n';
여기에서는 일반적으로 7 then 2! = 7, 두 개의 동일한 포인터를 말하는 컴파일러를 얻습니다. 를 ptr
가리키는 사실에도 불구하고 x
. 컴파일러는 x
값을 요청할 때 읽기를 방해하지 않기 위해 상수 값 이라는 사실을 취합니다 x
.
그러나의 주소를 가져 x
오면 강제로 존재하게됩니다. 그런 다음 const를 캐스트하고 수정합니다. 따라서 x
수정 된 메모리의 실제 위치 , 컴파일러는 읽을 때 실제로 읽지 않아도됩니다 x
!
컴파일러는 ptr
을 읽기 위해 따르는 것을 피하는 방법을 알아낼만큼 충분히 똑똑해질 수 *ptr
있지만, 종종 그렇지 않습니다. ptr = ptr+argc-1
옵티마이 저가 당신보다 똑똑해지면 자유롭게 가서 사용 하십시오.
operator[]
올바른 항목을 가져 오는 사용자 지정 을 제공 할 수 있습니다 .
int& operator[](std::size_t);
int const& operator[](std::size_t) const;
둘 다 있으면 유용합니다.