구조체로 인덱싱하는 것이 합법적입니까?


104

코드가 얼마나 '나쁜'것과 상관없이, 정렬 등이 컴파일러 / 플랫폼에서 문제가되지 않는다고 가정하면이 동작이 정의되지 않았거나 깨졌습니까?

다음과 같은 구조체가 있으면 :-

struct data
{
    int a, b, c;
};

struct data thing;

그것은이다 법적 접근 a, bc(&thing.a)[0], (&thing.a)[1]그리고 (&thing.a)[2]?

모든 경우에 모든 컴파일러와 플랫폼에서 시도해 보았고 모든 설정에서 '작동'했습니다. 컴파일러가 bthing [1] 이 같은 것이고 'b'에 저장하는 것이 레지스터에 저장되고 thing [1]이 메모리에서 잘못된 값을 읽는다는 것을 인식하지 못할 수도 있다는 점이 걱정 됩니다 (예 :). 모든 경우에 나는 그것이 옳은 일을 시도했습니다. (물론 그게 많이 증명되지는 않음을 알고 있습니다)

이것은 내 코드가 아닙니다. 작업해야하는 코드입니다. 이것이 잘못된 코드인지 깨진 코드 인지에 관심이 있습니다. 코드

태그가 지정된 C 및 C ++. 저는 대부분 C ++에 관심이 있지만 C ++이 다르면 C에도 관심이 있습니다.


51
아니요, "합법적"이 아닙니다. 정의되지 않은 동작입니다.
Sam Varshavchik

10
컴파일러가 멤버 사이에 패딩을 추가하지 않기 때문에이 매우 간단한 경우에서 작동합니다. 다른 크기의 유형을 사용하는 구조로 시도하면 무너질 것입니다.
일부 프로그래머 친구

7
과거의 UB를 파헤쳐 보자 .
Adrian Colomitchi

21
좋습니다. C 태그를 따르고 질문을 읽고 C ++ 태그를 보지 못했기 때문에 C에만 적용되는 답변을 작성하기 때문에 여기에서 우연히 발견했습니다. 여기서 C와 C ++는 매우 다릅니다! C는 공용체를 사용한 유형 punning을 허용하지만 C ++는 그렇지 않습니다.
Lundin

7
요소에 배열로 액세스해야하는 경우 배열로 정의하십시오. 다른 이름이 필요하면 이름을 사용하십시오. 케이크를 먹고 그것을 먹으려 고하면 결국 소화 불량으로 이어질 것입니다. 아마도 상상할 수있는 가장 불편한시기에 말입니다. (I 인덱스 0 C 법률 생각 1 또는 2가 아닌 인덱스 단일 요소 크기 (1)의 배열로 취급되는 상황이있다.)
조나단 레플러

답변:


73

불법입니다 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; 그렇지 않으면 동작이 정의되지 않습니다. ...Px[i]xP + JJ + PJjx[i + j]0 ≤ i + j ≤ n

if 절의 배열 요구 사항에 유의하십시오 . 그렇지 않으면 그렇지 않으면 위의 인용이다. 표현식은 분명히 if 절에 적합하지 않습니다 . 따라서 정의되지 않은 동작입니다.(&thing.a)[2]


보조 노트에 : 비록 나는 광범위하게 코드와 다양한 컴파일러에 미치는 변화를 실험하고 그들은 (이 여기에 패딩을 소개하지 않는 작동합니다 ) 유지 관리 관점에서 볼 때 코드는 매우 취약합니다. 이 작업을 수행하기 전에 구현이 멤버를 연속적으로 할당했다고 주장해야합니다. 그리고 인바운드 유지 :-). 그러나 여전히 정의되지 않은 동작 ....

다른 답변에서 일부 실행 가능한 해결 방법 (정의 된 동작 포함)이 제공되었습니다.



주석에서 올바르게 지적했듯이 , 이전 편집에 있던 [basic.lval / 8] 은 적용되지 않습니다. @ 2501 및 @MM 감사합니다.

1 : thing.a이 parttern을 통해 구조체 멤버에 액세스 할 수있는 유일한 법적 사례에 대해서는이 질문에 대한 @Barry의 답변을 참조하십시오 .


1
@jcoder class.mem에 정의되어 있습니다 . 실제 텍스트는 마지막 단락을 참조하십시오.
NathanOliver

4
여기에서는 엄격한 알리 징과 관련이 없습니다. int 유형은 집계 유형 내에 포함되며이 유형은 int의 별칭을 지정할 수 있습니다. - an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
2501

1
@ downvoters, 댓글에 관심? -그리고이 답변이 잘못된 부분을 개선하거나 지적하기 위해?
WhiZTiM

4
엄격한 앨리어싱은 이와 관련이 없습니다. 패딩은 객체의 저장된 값의 일부가 아닙니다. 또한이 대답은 가장 일반적인 경우를 해결하지 못합니다. 패딩이 없을 때 발생하는 일입니다. 이 답변을 실제로 삭제하는 것이 좋습니다.
MM

1
끝난! 엄격한 앨리어싱에 대한 단락을 제거했습니다.
WhiZTiM

48

아니요. 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의 배열.


3
클래스가 표준 레이아웃 유형 인 경우에만 작동합니다. 그렇지 않다면 여전히 UB입니다.
NathanOliver

@NathanOliver 내 대답은 C. Edited에만 적용된다는 점을 언급해야합니다. 이것은 이중 태그 언어 질문의 문제 중 하나입니다.
2501

감사합니다, 그의 왜 차이를 klnow 흥미 롭다 나는이 C ++에 대해 개별적으로 물어 C
jcoder

@NathanOliver 첫 번째 멤버의 주소는 표준 레이아웃 인 경우 C ++ 클래스의 주소와 일치하도록 보장됩니다. 그러나 이는 액세스가 잘 정의되어 있음을 보장하지 않으며 다른 클래스에 대한 이러한 액세스가 정의되지 않음을 의미하지도 않습니다.
Potatoswatter

그것이 char* p = ( char* )&thing.a + offsetof( thing , b );정의되지 않은 행동으로 이어진다 고 말 하시겠습니까?
MM

43

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]; }
};

1
타입 punning을 사용하는 C 프로그램의 디스 어셈블리와 비교하여 이것의 디스 어셈블리를보고 싶습니다. 하지만 ... C ++는 C만큼 빠릅니다 ... 맞죠? 권리?
Lundin

6
@Lundin이 구성의 속도에 관심이 있다면 데이터는 처음에 별도의 필드가 아닌 배열로 구성되어야합니다.
Slava

2
@Lundin은 읽을 수없고 정의되지 않은 동작을 의미합니까? 고맙지 만 사양 할게.
Slava

1
@Lundin 연산자 오버로딩은 일반 함수에 비해 오버 헤드를 유발하지 않는 컴파일 타임 구문 기능입니다. 한 번 봐 가지고 godbolt.org/g/vqhREz를 컴파일러가 실제로는 C ++과 C 코드를 컴파일 않을 때 무엇을 볼 수 있습니다. 그들이하는 일과 기대하는 일은 놀랍습니다. 개인적으로 C ++의 형식 안전성과 표현력이 C보다 백만 번 더 선호됩니다. 그리고 패딩에 대한 가정에 의존하지 않고 항상 작동합니다.
Jens

2
이러한 참조는 최소한 사물의 크기를 두 배로 늘릴 것입니다. 그냥하세요 thing.a().
TC

14

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;

1
이것은 언어 기능을 사용하고 있으며 결과적으로 잘 정의되고 효율적입니다. 최고의 답변입니다.
Peter-Monica 복원

2
효율적이라고 생각하십니까? 나는 그 반대라고 생각한다. 생성 된 코드에서.
JDługosz

1
@ JDługosz, 당신 말이 맞습니다. 생성 된 어셈블리를 살펴보면 gcc 6.2가 offsetoffC 에서 사용하는 것과 동일한 코드를 생성하는 것 같습니다 .
StoryTeller-Unslander Monica

3
arr constexpr을 만들어서 개선 할 수도 있습니다. 이것은 즉석에서 생성하는 대신 데이터 섹션에 단일 고정 조회 테이블을 생성합니다.
Tim

10

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]

@MM : 좋은 지적입니다. 다양한 의견에 대한 답변이며 Slava의 답변에 대한 대안입니다. 나는 시작 부분을 다시 말 했으므로 적어도 원래 질문에 대한 답변으로 시작합니다. 지적 해 주셔서 감사합니다.
Peter Cordes

공용체 기반 유형 punning은 []공용체 멤버에서 직접 연산자 를 사용하는 동안 gcc 및 clang에서 작동하는 것처럼 보이지만 표준 은를 array[index]동일하게 정의 하며 *((array)+(index))gcc 또는 clang은에 대한 액세스가 *((someUnion.array)+(index))에 대한 액세스 임을 안정적으로 인식하지 않습니다 someUnion. 내가 볼 수있는 유일한 설명 은 표준에 의해 정의되지 someUnion.array[index]않았고 *((someUnion.array)+(index))표준에 의해 정의되지 않았고 단지 인기있는 확장이며 gcc / clang은 두 번째를 지원하지 않기로 선택했지만 적어도 지금은 첫 번째를 지원하는 것 같습니다.
supercat

9

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]배열 객체 xn요소, 표현 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것은 정의 된 동작이지만 참조를 해제하는 것은 아무 것도 가리 키지 않기 때문에 정의되지 않습니다.


(& thing.a) + 1을 평가 하는 것은 배열의 끝을 지나는 포인터 합법적이기 때문에 거의 합법적입니다. 거기에 저장된 데이터를 읽거나 쓰는 것은 정의되지 않은 동작이며, & thing.b와 <,>, <=,> =와 비교하는 것은 정의되지 않은 동작입니다. (& thing.a) + 2는 절대적으로 불법입니다.
gnasher729

@ gnasher729 네 대답을 좀 더 명확히 할 가치가 있습니다.
Barry

(&thing.a + 1)내가 커버하지 못한 흥미로운 경우이다. +1! ... 궁금한 점이 있습니다. ISO C ++위원회에 속해 있습니까?
WhiZTiM 2016

그렇지 않으면 포인터를 반 개방 간격으로 사용하는 모든 루프가 UB가되기 때문에 매우 중요한 경우입니다.
Jens

마지막 표준 인용에 대해. C ++는 여기서 C보다 더 잘 지정되어야합니다.
2501 '

8

이것은 정의되지 않은 동작입니다.

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;

둘 다 있으면 유용합니다.


"배열의 구성원이 아니라는 것은 컴파일러가 [] 기반 배열 액세스가이를 수정할 수 없다고 가정 할 수 있음을 의미합니다." -사실이 아님, 예를 들어 (&thing.a)[0]수정 가능
MM

나는 const 예제가 질문과 어떤 관련이 있는지 알지 못합니다. 다른 이유가 아니라 const 객체를 수정할 수 없다는 특정 규칙이 있기 때문에 실패합니다.
MM

1
@MM, 그것은 구조체에 색인의 예 아니지만, 그것은이다 매우 자신에 의해 참조 뭔가 정의되지 않은 동작을 사용하는 방법의 좋은 그림 명백한 컴파일러 수 있기 때문에, 예상보다 서로 다른 출력을 초래할 수있는 메모리의 위치에 다른 일을 함께 당신이 원했던 것보다 UB.
Wildcard

@MM 죄송합니다. 객체 자체에 대한 포인터를 통한 사소한 것 이외의 배열 액세스는 없습니다. 두 번째는 정의되지 않은 행동의 부작용을 쉽게 볼 수있는 예입니다. 컴파일러 는 정의 된 방식으로 변경할 수 없다는 것을 알고x 있기 때문에 읽기를 최적화합니다 . 컴파일러가 이를 변경할 수있는 정의 된 액세스가 없음을 증명할 수있는 경우를 통해 변경할 때 유사한 최적화가 발생할 수 있습니다 . 이러한 변경은 컴파일러, 주변 코드 등의 무해한 변경으로 인해 발생할 수 있습니다. 따라서 작동하는지 테스트 하는 것만으로는 충분하지 않습니다. b(&blah.a)[1]b
Yakk-Adam Nevraumont

6

다음은 프록시 클래스를 사용하여 멤버 배열의 요소에 이름으로 액세스하는 방법입니다. 그것은 매우 C ++이며, 구문 선호를 제외하고 ref-returning 접근 자 함수에 비해 이점이 없습니다. 이로 인해 ->연산자가 멤버로 요소에 액세스하도록 오버로드 되므로 허용되기 위해서는 접근 자 ( d.a() = 5;) 의 구문을 싫어하고 ->포인터가 아닌 개체와 함께 사용 하는 것을 허용해야 합니다. 나는 이것이 코드에 익숙하지 않은 독자들에게 혼란을 줄 수 있기를 기대한다. 그래서 이것은 당신이 프로덕션에 넣고 싶은 것보다 더 깔끔한 트릭 일 수있다.

Data이 코드 의 구조체에는 첨자 연산자에 대한 오버로드가 포함되어있어 반복을 위해 및 함수 ar뿐만 아니라 배열 멤버 내부의 인덱싱 된 요소에 액세스 할 수 있습니다 . 또한이 모든 것들은 non-const 및 const 버전으로 오버로드되어 완전성을 위해 포함되어야한다고 느꼈습니다.beginend

경우 Data'는 s는 ->(이 같은 이름의 요소를 액세스하기 위해 사용된다 my_data->b = 5;)와, Proxy객체가 반환된다. 그러면이 Proxyrvalue가 포인터가 아니기 때문에 자체 ->연산자가 자동 ​​체인으로 호출되어 자신에 대한 포인터를 반환합니다. 이렇게하면 Proxy개체가 인스턴스화되고 초기 식을 평가하는 동안 유효한 상태로 유지됩니다.

(A)의 제휴 Proxy객체는 3 기준 부재를 채우고 a, b그리고 c입력 템플릿 파라미터로 주어진 최소 3 값을 포함하는 버퍼 점 가정 생성자로 전달되는 포인터에 따라 T. 따라서 Data클래스의 구성원 인 명명 된 참조를 사용하는 대신 액세스 지점에서 참조를 채움으로써 메모리를 절약 할 수 있습니다 (그러나 불행히도 연산자 ->는 사용 하지 않고 사용 .).

컴파일러의 옵티마이 저가를 사용하여 발생하는 모든 간접적 요소를 얼마나 잘 제거하는지 테스트하기 위해 Proxy아래 코드에는 main(). #if 1버전을 사용 ->하고 []운영자 및 #if 0버전 만에 직접 액세스하여, 절차의 등가 세트를 행한다 Data::ar.

Nci()함수는 배열 요소를 초기화하기 위해 런타임 정수 값을 생성하여 최적화 프로그램이 상수 값을 각 std::cout <<호출에 직접 연결하지 못하도록합니다 .

-03을 사용 GCC 6.2 들어, 두 버전 main()(간에 전환 동일한 어셈블리를 생성 #if 1하고 #if 0상기 제 전에 main()비교할) https://godbolt.org/g/QqRWZb

#include <iostream>
#include <ctime>

template <typename T>
class Proxy {
public:
    T &a, &b, &c;
    Proxy(T* par) : a(par[0]), b(par[1]), c(par[2]) {}
    Proxy* operator -> () { return this; }
};

struct Data {
    int ar[3];
    template <typename I> int& operator [] (I idx) { return ar[idx]; }
    template <typename I> const int& operator [] (I idx) const { return ar[idx]; }
    Proxy<int>       operator -> ()       { return Proxy<int>(ar); }
    Proxy<const int> operator -> () const { return Proxy<const int>(ar); }
    int* begin()             { return ar; }
    const int* begin() const { return ar; }
    int* end()             { return ar + sizeof(ar)/sizeof(int); }
    const int* end() const { return ar + sizeof(ar)/sizeof(int); }
};

// Nci returns an unpredictible int
inline int Nci() {
    static auto t = std::time(nullptr) / 100 * 100;
    return static_cast<int>(t++ % 1000);
}

#if 1
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d->b << "\n";
    d->b = -5;
    std::cout << d[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd->c << "\n";
    //cd->c = -5;  // error: assignment of read-only location
    std::cout << cd[2] << "\n";
}
#else
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d.ar[1] << "\n";
    d->b = -5;
    std::cout << d.ar[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd.ar[2] << "\n";
    //cd.ar[2] = -5;
    std::cout << cd.ar[2] << "\n";
}
#endif

맵시 있는. 주로 이것이 최적화된다는 것을 증명했기 때문에 찬성되었습니다. BTW, main()타이밍 함수 로 전체 가 아닌 매우 간단한 함수를 작성하면 훨씬 더 쉽게 할 수 있습니다 ! 예를 들어 / ( godbolt.org/g/89d3Np )로 int getb(Data *d) { return (*d)->b; }컴파일됩니다 . (예, 구문을 더 쉽게 만들 수 있지만, 이런 식 으로 오버로딩의 이상 함을 강조하기 위해 ref 대신 포인터를 사용했습니다 .)mov eax, DWORD PTR [rdi+4]retData &d->
Peter Cordes

어쨌든 이것은 멋지다. 같은 다른 아이디어 int tmp[] = { a, b, c}; return tmp[idx];는 최적화되지 않으므로이 아이디어는 깔끔합니다.
Peter Cordes

operator.C ++ 17에서 내가 놓친 또 하나의 이유 .
Jens 2011

2

값을 읽는 것으로 충분하고 효율성이 문제가되지 않거나 컴파일러가 잘 최적화 할 수 있다고 신뢰하거나 struct가 3 바이트에 불과한 경우 다음과 같이 안전하게 수행 할 수 있습니다.

char index_data(const struct data *d, size_t index) {
  assert(sizeof(*d) == offsetoff(*d, c)+1);
  assert(index < sizeof(*d));
  char buf[sizeof(*d)];
  memcpy(buf, d, sizeof(*d));
  return buf[index];
}

C ++ 전용 버전의 경우 표준 레이아웃이 static_assert있는지 확인하는 데 사용 하고 struct data대신 잘못된 인덱스에서 예외를 throw 할 수 있습니다.


1

불법이지만 해결 방법이 있습니다.

struct data {
    union {
        struct {
            int a;
            int b;
            int c;
        };
        int v[3];
    };
};

이제 v를 인덱싱 할 수 있습니다.


6
많은 C ++ 프로젝트는 모든 곳에서 다운 캐스팅이 괜찮다고 생각합니다. 우리는 여전히 나쁜 습관을 전파해서는 안됩니다.
StoryTeller-Unslander Monica

2
유니온은 두 언어 모두에서 엄격한 앨리어싱 문제를 해결합니다. 그러나 공용체를 통한 유형 punning은 C ++가 아닌 C에서만 괜찮습니다.
Lundin

1
그래도 이것이 모든 C ++ 컴파일러에서 작동한다고해도 놀라지 않을 것입니다. 이제까지.
Sven Nilsson

1
가장 적극적인 최적화 설정을 사용하여 gcc에서 시도해 볼 수 있습니다.
Lundin

1
@Lundin : 공용체 유형 punning은 ISO C ++에 대한 확장으로서 GNU C ++ 에서 합법적입니다 . 매뉴얼에 명확하게 언급되어 있지 않은 것 같지만 나는 이것에 대해 확신합니다. 그럼에도 불구 하고이 답변은 그것이 유효한 곳과 그렇지 않은 곳을 설명해야합니다.
Peter Cordes 2016
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.