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


104

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

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

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에도 관심이 있습니다.


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


(& 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.