문제

대해 물을 때 C의 일반적인 정의되지 않은 동작, 엄격한 앨리어싱 규칙을 언급한 것보다 더 깨달은 영혼이 있습니다.
그들은 무엇에 대한 이야기?

도움이 되었습니까?

해결책

엄격한 앨리어싱 문제가 발생하는 일반적인 상황은 구조체(예: 장치/네트워크 메시지)를 시스템의 워드 크기 버퍼(예: 포인터)에 오버레이할 때입니다. uint32_t또는 uint16_t에스).그러한 버퍼에 구조체를 오버레이하거나 포인터 캐스팅을 통해 그러한 구조체에 버퍼를 오버레이하면 엄격한 앨리어싱 규칙을 쉽게 위반할 수 있습니다.

따라서 이런 종류의 설정에서 무언가에 메시지를 보내려면 동일한 메모리 덩어리를 가리키는 두 개의 호환되지 않는 포인터가 있어야 합니다.그런 다음 다음과 같이 순진하게 코딩할 수 있습니다.

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));

    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);

    // Send a bunch of messages    
    for (int i =0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

엄격한 앨리어싱 규칙으로 인해 이 설정이 불법이 됩니다.객체에 속하지 않는 객체의 별칭을 지정하는 포인터 역참조 호환 유형 또는 C 2011 6.5 단락 7에서 허용하는 다른 유형 중 하나1 정의되지 않은 동작입니다.불행하게도 여전히 이런 식으로 코딩할 수 있습니다. 아마도 몇 가지 경고를 받고 컴파일이 잘 되지만 코드를 실행할 때 예상치 못한 이상한 동작이 발생합니다.

(GCC는 앨리어싱 경고를 제공하는 능력이 다소 일관성이 없는 것처럼 보이며 때로는 친근한 경고를 제공하기도 하고 때로는 그렇지 않기도 합니다.)

이 동작이 정의되지 않은 이유를 확인하려면 엄격한 앨리어싱 규칙이 컴파일러를 구매하는 것이 무엇인지 생각해야 합니다.기본적으로 이 규칙을 사용하면 내용을 새로 고치기 위해 명령을 삽입하는 것에 대해 생각할 필요가 없습니다. buff 루프의 모든 실행.대신, 최적화할 때 앨리어싱에 대해 성가시게 적용되지 않는 가정을 사용하여 해당 지침을 생략하고 로드할 수 있습니다. buff[0] 그리고 buff[1]를 루프가 실행되기 전에 한 번 CPU 레지스터에 저장하고 루프 본문의 속도를 높입니다.엄격한 앨리어싱이 도입되기 전에 컴파일러는 편집증 상태에 있어야 했습니다. buff 누구든지 언제 어디서나 바뀔 수 있습니다.따라서 추가 성능 우위를 확보하고 대부분의 사람들이 포인터를 입력하지 않는다고 가정하고 엄격한 앨리어싱 규칙이 도입되었습니다.

예제가 조작된 것이라고 생각한다면, 전송을 수행하는 다른 함수에 버퍼를 전달하는 경우에도 이런 일이 발생할 수 있습니다.

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

그리고 이 편리한 기능을 활용하기 위해 이전 루프를 다시 작성했습니다.

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

컴파일러는 SendMessage를 인라인하려고 시도할 만큼 똑똑할 수도 있고 그렇지 않을 수도 있으며 버프를 다시 로드할지 여부를 결정할 수도 있고 그렇지 않을 수도 있습니다.만약에 SendMessage 별도로 컴파일되는 다른 API의 일부이므로 버프의 콘텐츠를 로드하는 지침이 있을 수 있습니다.그렇다면 다시 C++를 사용하고 있을 수도 있으며 이는 컴파일러가 인라인할 수 있다고 생각하는 일부 템플릿 헤더 전용 구현입니다.아니면 단지 사용자의 편의를 위해 .c 파일에 작성한 내용일 수도 있습니다.어쨌든 정의되지 않은 동작이 계속 발생할 수 있습니다.내부적으로 무슨 일이 일어나고 있는지 일부 알고 있더라도 여전히 규칙을 위반하므로 잘 정의된 동작이 보장되지 않습니다.따라서 단어로 구분된 버퍼를 사용하는 함수를 래핑하는 것만으로는 반드시 도움이 되는 것은 아닙니다.

그렇다면 이 문제를 어떻게 해결할 수 있나요?

  • 노동조합을 사용하세요.대부분의 컴파일러는 엄격한 앨리어싱에 대해 불평하지 않고 이를 지원합니다.이는 C99에서 허용되며 C11에서는 명시적으로 허용됩니다.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
    
  • 컴파일러에서 엄격한 앨리어싱을 비활성화할 수 있습니다(f[no-]엄격한 앨리어싱 gcc에서))

  • 당신이 사용할 수있는 char* 시스템 단어 대신 별칭을 사용합니다.규칙은 다음에 대한 예외를 허용합니다. char* (포함 signed char 그리고 unsigned char).항상 다음과 같이 가정합니다. char* 다른 유형의 별칭.그러나 이것은 다른 방식으로는 작동하지 않습니다:귀하의 구조체가 문자 버퍼의 별칭을 지정한다고 가정하지 않습니다.

초보자는 조심하세요

이는 두 가지 유형을 서로 중첩할 때 하나의 잠재적인 지뢰밭일 뿐입니다.당신은 또한에 대해 배워야합니다 엔디안, 단어 정렬, 그리고 다음을 통해 정렬 문제를 처리하는 방법 패킹 구조체 바르게.

각주

1 C 2011 6.5 7에서 lvalue의 액세스를 허용하는 유형은 다음과 같습니다.

  • 객체의 유효 유형과 호환되는 유형,
  • 객체의 유효 유형과 호환되는 유형의 한정된 버전,
  • 객체의 유효 유형에 해당하는 부호 있는 유형 또는 부호 없는 유형인 유형,
  • 객체의 유효 유형의 한정된 버전에 해당하는 부호 있는 유형 또는 부호 없는 유형인 유형,
  • 멤버(재귀적으로 하위 집합 또는 포함된 공용체의 멤버 포함) 중에 앞서 언급한 유형 중 하나를 포함하는 집계 또는 공용체 유형, 또는
  • 문자형.

다른 팁

내가 찾은 가장 좋은 설명은 Mike Acton이 쓴 것입니다. 엄격한 앨리어싱 이해.PS3 개발에 약간 초점을 맞추고 있지만 기본적으로는 GCC일 뿐입니다.

기사에서 :

"엄격한 앨리어싱은 C(또는 C++) 컴파일러에서 다른 유형의 객체에 대한 역참조 포인터가 동일한 메모리 위치를 참조하지 않는다는 가정입니다(예:서로 별칭을 붙입니다.)"

따라서 기본적으로 int* 다음을 포함하는 일부 메모리를 가리키고 있습니다. int 그런 다음 당신은 float* 그 메모리에 저장하고 그것을 float 당신은 규칙을 어겼습니다.코드가 이를 준수하지 않으면 컴파일러의 최적화 프로그램이 코드를 손상시킬 가능성이 높습니다.

규칙의 예외는 다음과 같습니다. char*, 이는 모든 유형을 가리킬 수 있습니다.

이는 엄격한 앨리어싱 규칙으로, 섹션 3.10에 나와 있습니다. C++03 표준 (다른 답변은 좋은 설명을 제공하지만 규칙 자체는 제공하지 않음):

프로그램이 다음 유형 중 하나가 아닌 lvalue를 통해 객체의 저장된 값에 액세스하려고 시도하는 경우 동작은 정의되지 않습니다.

  • 객체의 동적 유형,
  • 객체의 동적 유형에 대한 cv 한정 버전,
  • 객체의 동적 유형에 해당하는 부호 있는 유형 또는 부호 없는 유형인 유형,
  • 객체의 동적 유형에 대한 cv 한정 버전에 해당하는 부호 있는 유형 또는 부호 없는 유형인 유형,
  • 멤버(재귀적으로 하위 집합 또는 포함된 공용체의 멤버 포함) 중에 앞서 언급한 유형 중 하나를 포함하는 집계 또는 공용체 유형,
  • 객체의 동적 유형의 (아마도 cv-한정된) 기본 클래스 유형인 유형,
  • char 또는 unsigned char 유형.

C++11 그리고 C++14 문구(변경 사항 강조):

프로그램이 객체의 저장된 값에 접근하려고 시도하는 경우 glvalue 다음 유형 중 하나 이외의 동작은 정의되지 않습니다.

  • 객체의 동적 유형,
  • 객체의 동적 유형에 대한 cv 한정 버전,
  • 객체의 동적 유형과 유사한 유형(4.4에 정의됨)
  • 객체의 동적 유형에 해당하는 부호 있는 유형 또는 부호 없는 유형인 유형,
  • 객체의 동적 유형에 대한 cv 한정 버전에 해당하는 부호 있는 유형 또는 부호 없는 유형인 유형,
  • 위에서 언급한 유형 중 하나를 포함하는 집계 또는 통합 유형 요소 또는 비정적 데이터 멤버 (재귀적으로 요소 또는 비정적 데이터 멤버 하위 집계 또는 포함된 통합의 경우)
  • 객체의 동적 유형의 (아마도 cv-한정된) 기본 클래스 유형인 유형,
  • char 또는 unsigned char 유형.

두 가지 변경 사항은 작았습니다. glvalue 대신에 l값, 집계/통합 사례의 설명.

세 번째 변경 사항은 더 강력한 보장을 제공합니다(강한 앨리어싱 규칙 완화).새로운 개념 유사한 유형 이제 별칭을 지정해도 안전합니다.


또한 문구(C99;ISO/IEC 9899:1999 6.5/7;ISO/IEC 9899:2011 §6.5 ¶7)에서도 똑같은 표현이 사용됩니다.

물체는 다음 유형 중 하나를 갖는 lvalue 표현식에 의해서만 액세스 된 저장된 값을 가져야합니다. 73) 또는 88):

  • 객체의 유효 유형과 호환되는 유형,
  • 효과적인 유형의 객체와 호환되는 유형의 자격을 갖춘 버전
  • 유효한 유형의 객체에 해당하는 서명 또는 서명되지 않은 유형 인 유형
  • 유효 객체의 유효 유형의 자격을 갖춘 버전에 해당하는 서명 또는 서명되지 않은 유형 인 유형
  • 회원들 사이에 상기 언급 된 유형 중 하나 (재귀 적으로, 하위 응집체 또는 포함 된 노동 조합 구성원 포함)를 포함하는 집계 또는 노동 유형
  • 문자형.

73) 또는 88) 이 목록의 목적은 개체의 별칭이 지정되거나 지정되지 않을 수 있는 상황을 지정하는 것입니다.

메모

내 글에서 발췌한 내용입니다 "엄격한 앨리어싱 규칙은 무엇이며 우리가 관심을 갖는 이유는 무엇입니까?" 글쓰기.

엄격한 앨리어싱이란 무엇입니까?

C 및 C++에서 앨리어싱은 저장된 값에 액세스할 수 있는 표현식 유형과 관련이 있습니다.C와 C++ 모두에서 표준은 어떤 표현식 유형이 어떤 유형의 별칭을 지정할 수 있는지 지정합니다.컴파일러와 최적화 프로그램은 우리가 앨리어싱 규칙을 엄격하게 따른다고 가정할 수 있습니다. 엄격한 앨리어싱 규칙.허용되지 않는 유형을 사용하여 값에 액세스하려고 하면 다음과 같이 분류됩니다. 정의되지 않은 동작(UB).정의되지 않은 동작이 발생하면 모든 베팅이 취소되고 프로그램 결과는 더 이상 신뢰할 수 없습니다.

불행하게도 엄격한 앨리어싱 위반으로 인해 기대했던 결과를 얻을 수 있는 경우가 많으며, 새로운 최적화 기능을 갖춘 향후 버전의 컴파일러에서 우리가 유효하다고 생각했던 코드가 손상될 가능성이 남아 있습니다.이는 바람직하지 않으며 엄격한 앨리어싱 규칙과 이를 위반하지 않는 방법을 이해하는 것이 가치 있는 목표입니다.

우리가 관심을 갖는 이유에 대해 더 자세히 이해하기 위해 엄격한 앨리어싱 규칙을 위반할 때 발생하는 문제, 유형 말장난에 사용되는 일반적인 기술이 엄격한 앨리어싱 규칙을 위반하는 경우가 많기 때문에 유형 말장난을 올바르게 입력하는 방법에 대해 논의할 것입니다.

예비 예

몇 가지 예를 살펴본 다음 표준에서 말하는 내용을 정확하게 설명하고 몇 가지 추가 예를 검토한 다음 엄격한 앨리어싱을 피하고 우리가 놓친 위반을 포착하는 방법을 알아볼 수 있습니다.다음은 놀랄 일이 아닌 예입니다(실제 사례):

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

우리는 정수* 가 차지하는 메모리를 가리킨다. 정수 이는 유효한 앨리어싱입니다.최적화 프로그램은 다음을 통한 할당을 가정해야 합니다. 아이피 가 차지하는 값을 업데이트할 수 있습니다. 엑스.

다음 예에서는 정의되지 않은 동작으로 이어지는 앨리어싱을 보여줍니다(실제 사례):

int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast<float*>(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}

기능에서 우리는 정수* 그리고 뜨다*, 이 예에서는 그리고 두 매개변수가 이 예에서 포함된 동일한 메모리 위치를 가리키도록 설정합니다. 정수.참고로 재해석_캐스트 이는 마치 템플릿 매개변수에 의해 지정된 유형이 있는 것처럼 표현식을 처리하도록 컴파일러에 지시하는 것입니다.이 경우 우리는 표현식을 처리하라고 말하고 있습니다. &엑스 마치 유형이 있는 것처럼 뜨다*.우리는 두 번째 결과를 순진하게 기대할 수도 있습니다. 시합 장차 ~ 가 되는 0 하지만 다음을 사용하여 최적화를 활성화한 경우 -O2 gcc와 clang 모두 다음과 같은 결과를 생성합니다.

0
1

예상할 수는 없지만 정의되지 않은 동작을 호출했기 때문에 완벽하게 유효합니다.ㅏ 뜨다 유효하게 별칭을 지정할 수 없습니다. 정수 물체.따라서 옵티마이저는 다음을 가정할 수 있습니다. 상수 1 역참조할 때 저장됨 저장소를 통해 반환 값이 됩니다. 에프 유효하게 영향을 미칠 수 없습니다 정수 물체.컴파일러 탐색기에 코드를 연결하면 이것이 정확히 무슨 일이 일어나고 있는지 알 수 있습니다(실제 사례):

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret

옵티마이저를 사용하는 유형 기반 별칭 분석(TBAA) 가정하다 1 반환되어 상수 값을 레지스터로 직접 이동합니다. eax 반환 값을 전달합니다.TBAA는 로드 및 저장을 최적화하기 위해 별칭을 허용하는 유형에 대한 언어 규칙을 사용합니다.이 경우 TBAA는 다음을 알고 있습니다. 뜨다 별칭을 지정할 수 없으며 정수 부하를 최적화하여 .

이제 룰북으로

표준에서는 우리가 할 수 있는 것과 허용되지 않는 것이 정확히 무엇이라고 말합니까?표준 언어는 간단하지 않으므로 각 항목에 대해 의미를 보여주는 코드 예제를 제공하려고 노력할 것입니다.

C11 표준은 무엇을 말합니까?

그만큼 C11 표준은 섹션에서 다음을 말합니다. 6.5 표현 단락 7:

객체는 다음 유형 중 하나를 가진 lvalue 표현식을 통해서만 액세스되는 저장된 값을 가집니다.88)— 객체의 유효 유형과 호환되는 유형,

int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int

— 객체의 유효 유형과 호환되는 유형의 한정된 버전,

int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int

— 객체의 유효 유형에 해당하는 부호 있는 유형 또는 부호 없는 유형인 유형,

int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                     // the effective type of the object

gcc/clang에는 확장자가 있습니다. 그리고 또한 할당을 허용하는 부호 없는 정수* 에게 정수* 호환되는 유형이 아닌 경우에도 마찬가지입니다.

— 객체의 유효 유형의 한정된 버전에 해당하는 부호 있는 유형 또는 부호 없는 유형인 유형,

int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                     // that corresponds with to a qualified verison of the effective type of the object

— 멤버(재귀적으로 하위 집합 또는 포함된 공용체의 멤버 포함) 중에 앞서 언급한 유형 중 하나를 포함하는 집계 또는 공용체 유형, 또는

struct foo {
  int x;
};

void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                         // can alias with *ip

foo f;
foobar( &f, &f.x );

— 문자 유형.

int x = 65;
char *p = (char *)&x;
printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                      // The results are not portable due to endianness issues.

C++17 초안 표준의 내용

섹션의 C++17 초안 표준 [basic.lval] 단락 11 말한다:

프로그램이 다음 유형 중 하나가 아닌 glvalue를 통해 객체의 저장된 값에 액세스하려고 시도하면 동작이 정의되지 않습니다.63(11.1) - 객체의 동적 유형,

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                  // of the allocated object

(11.2) — 객체의 동적 유형에 대한 cv 한정 버전,

int x = 1;
const int *cip = &x;
std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                            // version of the dynamic type of x

(11.3) - 객체의 동적 유형과 유사한 유형(7.5에 정의됨)

(11.4) — 객체의 동적 유형에 해당하는 부호 있는 유형 또는 부호 없는 유형인 유형,

// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
  si = 1;
  ui = 2;

  return si;
}

(11.5) — 객체의 동적 유형에 대한 cv 정규화 버전에 해당하는 부호 있는 유형 또는 부호 없는 유형인 유형,

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing

(11.6) — 요소 또는 비정적 데이터 멤버(하위 집계 또는 포함된 공용체의 요소 또는 비정적 데이터 멤버를 재귀적으로 포함) 중에 앞서 언급한 유형 중 하나를 포함하는 집계 또는 공용체 유형,

struct foo {
 int x;
};

// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
 fp.x = 1;
 ip = 2;

 return fp.x;
}

foo f; 
foobar( f, f.x ); 

(11.7) — 객체의 동적 유형의 (아마도 cv 한정) 기본 클래스 유형인 유형,

struct foo { int x ; };

struct bar : public foo {};

int foobar( foo &f, bar &b ) {
  f.x = 1;
  b.x = 2;

  return f.x;
}

(11.8) — char, unsigned char 또는 std::byte 유형.

int foo( std::byte &b, uint32_t &ui ) {
  b = static_cast<std::byte>('a');
  ui = 0xFFFFFFFF;                   

  return std::to_integer<int>( b );  // b gives us a glvalue expression of type std::byte which can alias
                                     // an object of type uint32_t
}

가치가 없다 서명된 문자 위 목록에는 포함되어 있지 않습니다. 이는 다음과 눈에 띄는 차이점입니다. 그것은 말한다 문자 유형.

유형 Punning이란 무엇입니까?

우리는 이 지점에 이르렀고 왜 별칭을 사용하고 싶은지 궁금할 것입니다.대답은 일반적으로 말장난을 타자하다, 종종 사용되는 방법은 엄격한 앨리어싱 규칙을 위반합니다.

때때로 우리는 유형 시스템을 우회하고 객체를 다른 유형으로 해석하고 싶습니다.이것은 ... 불리운다 유형 말장난, 메모리 세그먼트를 다른 유형으로 재해석합니다. 유형 말장난 보거나 전송하거나 조작하기 위해 객체의 기본 표현에 액세스하려는 작업에 유용합니다.유형 말장난이 사용되는 일반적인 영역은 컴파일러, 직렬화, 네트워킹 코드 등입니다.

전통적으로 이는 객체의 주소를 가져와 이를 재해석하려는 유형의 포인터로 캐스팅한 다음 값에 액세스하는 방식, 즉 별칭을 통해 수행되었습니다.예를 들어:

int x =  1 ;

// In C
float *fp = (float*)&x ;  // Not a valid aliasing

// In C++
float *fp = reinterpret_cast<float*>(&x) ;  // Not a valid aliasing

printf( "%f\n", *fp ) ;

앞에서 본 것처럼 이는 유효한 앨리어싱이 아니므로 정의되지 않은 동작을 호출합니다.그러나 전통적으로 컴파일러는 엄격한 앨리어싱 규칙을 활용하지 않았으며 이러한 유형의 코드는 일반적으로 작동했지만 개발자는 불행하게도 이런 방식으로 작업하는 데 익숙해졌습니다.유형 말장난을 위한 일반적인 대체 방법은 공용체를 이용하는 것입니다. 이는 C에서는 유효하지만 정의되지 않은 동작 C++에서 (실제 예시 보기):

union u1
{
  int n;
  float f;
} ;

union u1 u;
u.f = 1.0f;

printf( "%d\n”, u.n );  // UB in C++ n is not the active member

이는 C++에서는 유효하지 않으며 일부는 변형 유형을 구현하는 것만으로 공용체의 목적을 고려하고 유형 말장난을 위해 공용체를 사용하는 것은 남용이라고 생각합니다.

Pun을 올바르게 입력하려면 어떻게 해야 하나요?

표준 방법 유형 말장난 C와 C++ 모두에서 밈피.이것은 약간 무거운 것처럼 보일 수 있지만 최적화 프로그램은 다음의 사용을 인식해야 합니다. 밈피 ~을 위한 유형 말장난 그것을 최적화하고 레지스터를 생성하여 이동을 등록합니다.예를 들어 우리가 알고 있다면 int64_t 와 크기가 같다 더블:

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

우리는 사용할 수 있습니다 밈피:

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

충분한 최적화 수준에서 적절한 최신 컴파일러는 이전에 언급한 것과 동일한 코드를 생성합니다. 재해석_캐스트 방법이나 노동 조합 방법 유형 말장난.생성된 코드를 검토해 보면 mov 등록(라이브 컴파일러 탐색기 예).

C++20 및 bit_cast

C++20에서는 다음과 같은 이점을 얻을 수 있습니다. 비트캐스트 (제안서의 링크에서 구현 가능) 이는 간단하고 안전한 타이핑 방법을 제공할 뿐만 아니라 constexpr 컨텍스트에서도 사용할 수 있습니다.

다음은 사용 방법의 예입니다. 비트캐스트 말장난을 치다 부호 없는 정수 에게 뜨다, (라이브로 보세요):

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

경우에 에게 그리고 에서 유형의 크기가 동일하지 않으므로 중간 struct15를 사용해야 합니다.우리는 다음을 포함하는 구조체를 사용할 것입니다. 크기(부호 없는 정수) 문자 배열(4바이트 unsigned int로 가정)이 되려면 에서 유형과 부호 없는 정수 으로 에게 유형.:

struct uint_chars {
 unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
};

// Assume len is a multiple of 4 
int bar( unsigned char *p, size_t len ) {
 int result = 0;

 for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
   uint_chars f;
   std::memcpy( f.arr, &p[index], sizeof(unsigned int));
   unsigned int result = bit_cast<unsigned int>(f);

   result += foo( result );
 }

 return result ;
}

이 중간 유형이 필요하다는 것은 불행한 일이지만 이것이 현재의 제약 사항입니다. 비트캐스트.

엄격한 앨리어싱 위반 포착

C++에는 엄격한 앨리어싱을 포착하기 위한 좋은 도구가 많지 않습니다. 우리가 가지고 있는 도구는 엄격한 앨리어싱 위반 사례와 잘못 정렬된 로드 및 저장 사례를 포착할 것입니다.

플래그를 사용하는 gcc -fstrict-aliasing 그리고 -Wstrict 앨리어싱 오탐/음성 없이는 아니지만 일부 사례를 포착할 수 있습니다.예를 들어 다음과 같은 경우 gcc에서 경고가 생성됩니다(라이브로 보세요):

int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught 
               // it was being accessed w/ an indeterminate value below

printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));

이 추가 사례를 포착하지는 못하지만(라이브로 보세요):

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

clang은 이러한 플래그를 허용하지만 실제로 경고를 구현하지는 않습니다.

우리가 사용할 수 있는 또 다른 도구는 잘못 정렬된 로드와 저장을 포착할 수 있는 ASan입니다.이는 직접적인 엄격한 앨리어싱 위반은 아니지만 엄격한 앨리어싱 위반의 일반적인 결과입니다.예를 들어 다음의 경우는 clang을 사용하여 빌드할 때 런타임 오류를 생성합니다. -fsanitize=주소

int *x = new int[2];               // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
*u = 1;                            // Access to range [6-9]
printf( "%d\n", *u );              // Access to range [6-9]

제가 추천할 마지막 도구는 C++ 전용이며 엄밀히 말하면 도구가 아니라 코딩 방법이므로 C 스타일 캐스트를 허용하지 않습니다.gcc와 clang은 모두 다음을 사용하여 C 스타일 캐스트에 대한 진단을 생성합니다. -Wold 스타일 캐스팅.이렇게 하면 정의되지 않은 유형의 말장난이 reinterpret_cast를 사용하도록 강제됩니다. 일반적으로 reinterpret_cast는 더 자세한 코드 검토를 위한 플래그여야 합니다.또한 감사를 수행하기 위해 reinterpret_cast에 대한 코드 베이스를 검색하는 것이 더 쉽습니다.

C의 경우 이미 다룬 모든 도구가 있으며 C 언어의 대규모 하위 집합에 대한 프로그램을 철저하게 분석하는 정적 분석기인 tis-interpreter도 있습니다.이전 예제의 C 버전을 사용하면 -fstrict-aliasing 한 가지 경우를 놓쳤습니다(라이브로 보세요)

int a = 1;
short j;
float f = 1.0 ;

printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));

int *p; 

p=&a;
printf("%i\n", j = *((short*)p));

tis-interpeter는 세 가지를 모두 포착할 수 있습니다. 다음 예에서는 tis-interpreter로 tis-kernal을 호출합니다(간결성을 위해 출력이 편집됨).

./bin/tis-kernel -sa example1.c 
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
              rules by accessing a cell with effective type int.
...

example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
              accessing a cell with effective type float.
              Callstack: main
...

example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
              accessing a cell with effective type int.

마지막으로 타이산 현재 개발 중입니다.이 새니타이저는 섀도우 메모리 세그먼트에 유형 검사 정보를 추가하고 액세스를 검사하여 앨리어싱 규칙을 위반하는지 확인합니다.이 도구는 잠재적으로 모든 앨리어싱 위반을 포착할 수 있어야 하지만 런타임 오버헤드가 클 수 있습니다.

엄격한 앨리어싱은 포인터에만 적용되는 것이 아니라 참조에도 영향을 줍니다. 저는 부스트 개발자 위키에 이에 대한 논문을 썼고 반응이 너무 좋아서 제 컨설팅 웹 사이트의 페이지로 만들었습니다.그것은 그것이 무엇인지, 왜 사람들을 그토록 혼란스럽게 하는지, 그리고 그것에 대해 무엇을 해야 하는지를 완전히 설명합니다. 엄격한 앨리어싱 백서.특히 공용체가 C++에서 위험한 동작인 이유와 memcpy를 사용하는 것이 C와 C++ 모두에서 이식 가능한 유일한 수정 방법인 이유를 설명합니다.이것이 도움이 되기를 바랍니다.

Doug T.이미 썼습니다. 여기에 GCC로 트리거하는 간단한 테스트 사례가 있습니다.

check.c

#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

다음으로 컴파일 gcc -O2 -o check check.c .일반적으로 (내가 시도한 대부분의 gcc 버전에서) 컴파일러는 "check" 함수에서 "h"가 "k"와 동일한 주소가 될 수 없다고 가정하기 때문에 "엄격한 앨리어싱 문제"를 출력합니다.그 때문에 컴파일러는 if (*h == 5) 떨어져서 항상 printf를 호출합니다.

여기에 관심이 있는 분들을 위해 gcc 4.6.3에서 생성되고 x64용 ubuntu 12.04.2에서 실행되는 x64 어셈블러 코드가 있습니다.

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

따라서 if 조건은 어셈블러 코드에서 완전히 사라졌습니다.

유형 말장난 (공용체를 사용하는 것과 반대되는) 포인터 캐스트를 통한 것은 엄격한 앨리어싱을 깨는 주요 예입니다.

C89 근거에 따르면 표준 작성자는 컴파일러에 다음과 같은 코드를 제공하도록 요구하고 싶지 않았습니다.

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

값을 다시 로드해야 합니다. x 가능성을 허용하기 위해 할당문과 반환문 사이에 p 다음을 가리킬 수도 있다 x, 그리고 할당 *p 결과적으로 값이 변경될 수 있습니다. x.컴파일러가 앨리어싱이 없을 것이라고 가정할 자격이 있어야 한다는 개념 위와 같은 상황에서 논란의 여지가 없었습니다.

불행하게도 C89의 작성자는 문자 그대로 읽으면 다음 함수도 정의되지 않은 동작을 호출하게 만드는 방식으로 규칙을 작성했습니다.

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

유형의 lvalue를 사용하기 때문입니다. int 유형의 객체에 액세스하려면 struct S, 그리고 int 액세스하는 데 사용할 수 있는 유형이 아닙니다. struct S.문자 유형이 아닌 구조체 및 공용체 멤버의 모든 사용을 정의되지 않은 동작으로 처리하는 것은 터무니없기 때문에 거의 모든 사람은 한 유형의 lvalue가 다른 유형의 객체에 액세스하는 데 사용될 수 있는 상황이 적어도 몇 가지 있다는 것을 인식합니다. .불행하게도 C 표준 위원회는 이러한 상황이 무엇인지 정의하지 못했습니다.

문제의 대부분은 다음과 같은 프로그램의 동작에 대해 묻는 결함 보고서 #028의 결과입니다.

int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}

결함 보고서 #28에서는 "double" 유형의 공용체 멤버를 작성하고 "int" 유형 중 하나를 읽는 작업이 구현 정의 동작을 호출하기 때문에 프로그램이 정의되지 않은 동작을 호출한다고 명시합니다.이러한 추론은 무의미하지만 원래 문제를 해결하기 위해 아무 것도 하지 않으면서 언어를 불필요하게 복잡하게 만드는 유효 유형 규칙의 기초를 형성합니다.

원래의 문제를 해결하는 가장 좋은 방법은 규칙의 목적에 대한 각주를 규범적인 것처럼 취급하는 것일 것입니다.다음과 같은 경우:

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   s.x = 1;
   p = &s.x;
   inc_int(p);
   return s.x;
 }

내부에는 갈등이 없습니다. inc_int 스토리지에 대한 모든 액세스는 다음을 통해 액세스되기 때문입니다. *p 유형의 lvalue로 수행됩니다. int, 이며 충돌이 없습니다. test 왜냐하면 p 눈에 띄게 다음에서 파생됩니다. struct S, 그리고 다음에는 s 사용되면 해당 스토리지에 대한 모든 액세스는 다음을 통해 이루어집니다. p 이미 일어났을 것입니다.

코드가 살짝 바뀌었다면..

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }

여기서 앨리어싱 충돌이 발생합니다. p 그리고에 대한 액세스 s.x 실행 중인 해당 시점에 다른 참조가 존재하기 때문에 표시된 줄에 동일한 스토리지에 액세스하는 데 사용됩니다..

결함 보고서 028에서 원래 예제에서는 두 포인터의 생성과 사용이 겹치기 때문에 UB를 호출했다고 밝혔습니다. 그러면 "효과적인 유형"이나 기타 복잡성을 추가하지 않고도 상황이 훨씬 더 명확해졌을 것입니다.

많은 답변을 읽은 후 뭔가를 추가해야 할 필요성을 느낍니다.

엄격한 앨리어싱(조금 설명하겠습니다) 중요하기 때문에:

  1. 메모리 액세스는 비용이 많이 들 수 있습니다(성능 측면에서). 데이터는 CPU 레지스터에서 조작됩니다. 실제 메모리에 다시 기록되기 전에.

  2. 두 개의 서로 다른 CPU 레지스터의 데이터가 동일한 메모리 공간에 기록되는 경우, 어떤 데이터가 "생존"할지 예측할 수 없습니다. C로 코딩할 때

    CPU 레지스터의 로드 및 언로드를 수동으로 코딩하는 어셈블리에서는 어떤 데이터가 그대로 유지되는지 알 수 있습니다.그러나 C는 (다행히도) 이 세부 사항을 추상화합니다.

두 포인터가 메모리의 동일한 위치를 가리킬 수 있으므로 다음과 같은 결과가 발생할 수 있습니다. 가능한 충돌을 처리하는 복잡한 코드.

이 추가 코드는 느리고 성능이 저하됩니다 더 느리고 (아마도) 불필요한 추가 메모리 읽기/쓰기 작업을 수행하기 때문입니다.

그만큼 엄격한 앨리어싱 규칙을 통해 중복되는 기계어 코드를 방지할 수 있습니다. 어떤 경우에는 해야한다 두 포인터가 동일한 메모리 블록을 가리키지 않는다고 가정하는 것이 안전합니다(또한 restrict 예어).

엄격한 앨리어싱은 서로 다른 유형에 대한 포인터가 메모리의 서로 다른 위치를 가리킨다고 가정하는 것이 안전하다고 명시합니다.

컴파일러가 두 포인터가 서로 다른 유형을 가리키는 것을 알아차린 경우(예: int * 그리고 float *), 메모리 주소가 다르다고 가정합니다. ~하지 않을 것이다 메모리 주소 충돌로부터 보호하여 기계어 코드를 더 빠르게 만듭니다.

예를 들어:

다음 기능을 가정해 보겠습니다.

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

해당 사건을 처리하기 위해 a == b (두 포인터 모두 동일한 메모리를 가리킴) 메모리에서 CPU 레지스터로 데이터를 로드하는 방식을 순서대로 지정하고 테스트해야 하므로 코드는 다음과 같이 끝날 수 있습니다.

  1. a 그리고 b 기억으로부터.

  2. 추가하다 a 에게 b.

  3. 구하다 b 그리고 다시 장전하다 a.

    (CPU 레지스터에서 메모리로 저장하고 메모리에서 CPU 레지스터로 로드)

  4. 추가하다 b 에게 a.

  5. 구하다 a (CPU 레지스터에서) 메모리로.

3단계는 실제 메모리에 접근해야 하기 때문에 속도가 매우 느립니다.그러나 다음과 같은 경우로부터 보호해야 합니다. a 그리고 b 동일한 메모리 주소를 가리킵니다.

엄격한 앨리어싱을 사용하면 이러한 메모리 주소가 뚜렷하게 다르다는 것을 컴파일러에 알려줌으로써 이를 방지할 수 있습니다(이 경우 포인터가 메모리 주소를 공유하는 경우 수행할 수 없는 추가 최적화가 허용됩니다).

  1. 이는 서로 다른 유형을 사용하여 가리키는 두 가지 방법으로 컴파일러에 알릴 수 있습니다.즉.:

    void merge_two_numbers(int *a, long *b) {...}
    
  2. 사용하여 restrict 예어.즉.:

    void merge_two_ints(int * restrict a, int * restrict b) {...}
    

이제 엄격한 앨리어싱 규칙을 충족하면 3단계를 피할 수 있으며 코드 실행 속도가 훨씬 빨라집니다.

실제로, restrict 키워드를 사용하면 전체 기능을 다음과 같이 최적화할 수 있습니다.

  1. a 그리고 b 기억으로부터.

  2. 추가하다 a 에게 b.

  3. 결과를 둘 다에 저장 a 그리고 b.

충돌 가능성 때문에 이전에는 이 최적화를 수행할 수 없었습니다(여기서 a 그리고 b 2배가 아니라 3배가 됩니다.)

엄격한 앨리어싱은 동일한 데이터에 대해 다른 포인터 유형을 허용하지 않습니다.

이 기사 문제를 자세히 이해하는 데 도움이 될 것입니다.

기술적으로 C++에서는 엄격한 앨리어싱 규칙이 적용되지 않을 수 있습니다.

간접 정의(* 운영자):

단항 * 연산자는 간접 참조를 수행합니다.적용되는 표현식은 객체 유형에 대한 포인터이거나 함수 유형에 대한 포인터가되어야하며 결과는 객체를 참조하는 lvalue입니다. 또는 기능 표현이 가리키는 곳.

또한 glvalue의 정의

glvalue는 객체의 정체성을 결정하는 표현입니다 (... Snip)

따라서 잘 정의된 프로그램 추적에서 glvalue는 개체를 참조합니다. 따라서 소위 엄격한 앨리어싱 규칙은 적용되지 않습니다. 이것은 디자이너가 원하는 것이 아닐 수도 있습니다.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top