IT이야기

유니언 및 유형 퍼닝

cyworld 2022. 5. 18. 21:58
반응형

유니언 및 유형 퍼닝

한참 뒤져 보았지만 뚜렷한 답을 찾을 수가 없다.

많은 사람들이 노조를 이용해 활자를 돌리는 것은 정의되지 않고 나쁜 관행이라고 말한다.왜 이러한가?나는 당신이 원래 정보를 쓰는 메모리가 단지 그것 자체의 합의만을 바꾸지 않을 것이라는 것을 고려하면 정의되지 않은 일을 할 이유가 전혀 보이지 않는다. (그것이 스택의 범위를 벗어나지 않는 한, 그러나 그것이 조합의 문제가 아니라, 그것은 나쁜 설계가 될 것이다.)

사람들은 엄격한 가명 규칙을 인용하지만, 내가 보기에 그것은 당신이 그것을 할 수 없기 때문에 그것을 할 수 없다고 말하는 것과 같다.

또한 만약 말장난을 타이핑하지 않는다면 조합의 목적은 무엇인가?나는 어딘가에서 그들이 다른 시간에 다른 정보를 위해 동일한 메모리 위치를 사용하도록 되어 있는 것을 보았는데, 그것을 다시 사용하기 전에 왜 그냥 정보를 삭제하지 않는가?

요약하기

  1. 노조를 유형 펀칭에 사용하는 것은 왜 나쁜가?
  2. 이것이 아니라면 그들이 무슨 소용이 있겠는가?

추가 정보:나는 주로 C++를 사용하고 있는데, 그것과 C에 대해 알고 싶어.특히 나는 부유물과 생 육각형 사이의 변환을 위해 유니언으로 CAN 버스를 통해 보내고 있다.

다시 반복하려면 C에서는 (C++에서는) 유니언을 통한 타이핑이 완벽히 가능하다.이와는 대조적으로 포인터 캐스트를 사용하여 C99의 엄격한 앨리어싱을 위반하고 유형별로 정렬 요건이 다를 수 있으며 잘못하면 SIGBUS를 올릴 수 있기 때문에 문제가 있다.노조와 함께라면 이것은 결코 문제가 되지 않는다.

C 표준의 관련 인용구는 다음과 같다.

C89 섹션 3.3.2.3 §5:

값이 개체의 다른 멤버에 저장된 후 유니언 개체의 멤버에 액세스할 경우 동작이 구현 정의됨

C11 섹션 6.5.2.3 §3:

. 연산자와 식별자가 뒤따르는 사후 수식(postfix)은 구조물 또는 유니온 객체의 구성원을 지정한다.값은 명명된 멤버의 값이다.

다음 각주 95와 함께:

조합 오브젝트의 내용을 읽을 때 사용한 멤버가 오브젝트에 값을 저장하기 위해 마지막으로 사용한 멤버와 동일하지 않을 경우, 해당 값의 오브젝트 표현 중 적절한 부분을 6.2.6('형 펀닝'이라고도 하는 프로세스)에서 설명한 새로운 타입의 오브젝트 표현으로 재해석한다.이것은 함정 표현일 수도 있다.

이것은 완벽하게 명확해야 한다.


제임스는 C11 섹션 6.7.2.1 §16을 읽기 때문에 혼란스러워 한다.

조합원 중 적어도 한 명의 가치는 언제든지 조합 오브젝트에 저장할 수 있다.

이것은 모순된 것처럼 보이지만, 다음과 같은 것은 아니다.C++와는 대조적으로 C에서는 활성 멤버라는 개념이 없고 호환이 안 되는 유형의 표현을 통해 단일 저장 값에 접근하는 것이 완벽히 좋다.

C11 부록 J.1 §1을 참조하십시오.

[지정되지 않음]에 마지막으로 저장된 값 이외의 조합원에 해당하는 바이트 값.

C99에서는 이것을 읽곤 했다.

[지정되지 않음]에 저장된 마지막 조합원 이외의 조합원 값

이것은 틀렸다.부속문서는 규범적인 것이 아니기 때문에 자체 TC 등급을 매기지 않았고 다음 표준 개정 때까지 기다려야 고칠 수 있었다.


표준 C++(및 C90까지)에 대한 GNU 확장은 유니언과의 형식 연산을 명시적으로 허용한다.GNU 확장을 지원하지 않는 다른 컴파일러도 유니언 유형 연산을 지원할 수 있지만, 기본 언어 표준의 일부는 아니다.

유니언의 원래 목적은 다양한 유형을 나타낼 수 있는 공간을 절약하는 것이었습니다. 우리가 부스트라고 부르는 변형 유형(Boost)을 참조하십시오.이것의 좋은 예로서 변형된 것.

또 다른 일반적인 용도는 이것의 타당성에 대해 논증하는 유형이지만 실질적으로 대부분의 컴파일러가 이를 지지하고 있으며, 우리는 gcc가 그 지원을 문서화한다는 것을 알 수 있다.

가장 최근에 작성된 조합원이 아닌 다른 조합원으로부터 읽는 관행('형식 퍼닝')이 일반적이다.-fstrct-alliasing을 사용하더라도 유니온 유형을 통해 메모리에 액세스할 경우 유형 연산이 허용된다.그래서 위의 코드는 예상대로 작동한다.

보고서는 -fstrct-alliasing을 사용하더라도 형식 변환이 허용되며, 이는 재생 중인 앨리어싱 문제가 있음을 나타낸다.

Pascal Cuoq는 결함 보고서 283이 이것이 C에서 허용되었음을 명확히 했다고 주장해왔다.결함 보고서 283에는 다음과 같은 각주가 설명으로 추가되었다.

조합 오브젝트의 내용에 접근하는 데 사용된 멤버가 오브젝트에 값을 저장하기 위해 마지막으로 사용한 멤버와 동일하지 않을 경우, 해당 값의 오브젝트 표현 중 적절한 부분은 6.2.6("타입 펀닝"이라고도 하는 프로세스)에서 설명한 새로운 타입의 오브젝트 표현으로 재해석된다.이것은 함정 표현일 수도 있다.

C11에서 그것은 각주일 것이다.95.

비록 그 때는std-discussion메일 그룹 주제 Type Punning an Union을 통한 punning 인수는 충분히 구체화되지 않았으며, 이 인수는 이후 타당해 보인다.DR 283새로운 규범적 표현은 추가하지 않고 단지 각주만 추가했다.

내 생각에 이것은 C에서 지나치게 구체화된 의미론적 쿼그미어다.어떤 사례가 행동을 정의하고 어떤 사례가 정의되지 않는지에 대해서는 구현자와 C 위원회 간에 합의가 이루어지지 않았다[...]

C++에서는 정의된 동작인지 여부가 불분명하다.

이 논의는 또한 조합을 통해 유형 펀닝을 허용하는 것이 바람직하지 않은 이유를 최소한 하나 이상 다룬다.

[...]C 표준의 규칙은 현재 구현이 수행하는 유형 기반 별칭 분석 최적화를 깨뜨린다.

최적화를 깨는 겁니다.이에 대한 두 번째 주장은 memcpy를 사용하면 동일한 코드가 생성되어야 하며 최적화 및 잘 정의된 동작이 깨지지 않는다는 것이다. 예를 들어 다음과 같다.

std::int64_t n;
std::memcpy(&n, &d, sizeof d);

대신:

union u1
{
  std::int64_t n;
  double d ;
} ;

u1 u ;
u.d = d ;

godbolt를 사용하면 동일한 코드가 생성되고 컴파일러에서 동일한 코드가 생성되지 않으면 버그로 간주해야 하는 인수가 만들어지는 것을 알 수 있다.

만약 이것이 당신의 구현에 맞는다면, 나는 당신이 그것에 버그를 제기하는 것을 제안한다.특정 컴파일러를 사용하여 성능 문제를 해결하기 위해 현실 최적화(유형 기반 별칭 분석 기반)를 깨는 것은 좋지 않은 생각처럼 보인다.

블로그 포스트 Type Punning, Strate Aliasing, Optimization도 비슷한 결론에 도달한다.

정의되지 않은 동작 메일 목록 토론:복사를 피하기 위한 타이프 펀칭은 같은 땅을 많이 덮고 우리는 그 영역이 얼마나 회색일 수 있는지 알 수 있다.

이 정의되지 않은 행동을 하는 데는 두 가지 변조가 있다(또는 적어도 C90에 있었다).첫 번째는 컴파일러가 조합에 무엇이 있는지 추적하는 추가 코드를 생성할 수 있도록 허용하고 잘못된 구성원에 접근했을 때 신호를 생성한다는 것이었습니다.실제로, 나는 아무도 (아마도 센터라인?) 하지 않았다고 생각한다.다른 하나는 이것이 열리게 된 최적화 가능성이었고, 이것들이 사용된다.필자는 (변수가 범위를 벗어나거나 후속적으로 다른 값의 쓰기가 있기 때문에) 필요하지 않을 수 있다는 이유로 가능한 마지막 순간까지 쓰기를 연기하는 컴파일러를 사용해 왔다.논리적으로 노동조합이 보이면 이 최적화가 꺼질 것이라고 예상하겠지만, 마이크로소프트 C의 초기 버전에서는 그렇지 않았다.

활자 펀칭의 문제는 복잡하다.1980년대 후반의 C 위원회(백)는 당시 두 가지 기법이 모두 널리 퍼져 있었지만, 조합이 아닌, 이것에 대해 깁스(C++, 재해석_cast)를 사용해야 한다는 입장을 취하였다.이후 일부 컴파일러(g++, 예를 들어)는 조합의 사용을 지지하면서 반대 입장을 취했지만, 출연자의 사용은 지지하지 않았다.그리고 실제로, 활자 퍼닝이 있다는 것이 즉각적으로 분명하지 않다면, 두 가지 모두 효과가 없다.이것이 g++의 관점을 뒷받침하는 동기일 것이다.조합원에 접속하면 곧바로 활자 퍼닝이 있을 수 있다는 것이 명백해진다.하지만 물론, 다음과 같은 것을 고려했을 때:

int f(const int* pi, double* pd)
{
    int results = *pi;
    *pd = 3.14159;
    return results;
}

다음 항목으로 호출됨:

union U { int i; double d; };
U u;
u.i = 1;
std::cout << f( &u.i, &u.d );

표준의 엄격한 규칙에 따라 완벽하게 합법적이지만, g++(그리고 아마도 많은 다른 컴파일러)로 실패한다; 컴파일을 작성할 때f, 컴파일러는 다음과 같이 가정한다.pi그리고pd가명을 지정할 수 없으며, 쓰기 순서를 변경할 수 없음*pd그리고 에서 읽은 것.*pi. (나는 이것이 보장될 의도가 결코 아니었다고 믿는다.그러나 현재 표준의 문구는 그것을 보증한다.)

편집:

다른 답변들은 그 행동이 실제로 정의되어 있다고 주장해왔기 때문에(대부분 문맥에서 추출된 비규범적 주석을 인용하는 것에 기초한다).

여기서 정답은 파블로1977의 답이다: 표준은 펀칭이 관련되었을 때 행동을 정의하려고 시도하지 않는다.그 이유는 그것이 정의할 수 있는 휴대용 동작이 없기 때문이다.이것은 특정 구현이 그것을 정의하는 것을 방해하지 않는다; 비록 이 문제에 대한 어떤 구체적인 논의도 기억나지 않지만, 나는 구현이 무언가를 정의한다는 것이 목적이었다고 거의 확신한다.

유형 연산을 위한 조합 사용에 관하여: C 위원회가 C90을 개발할 때(1980년대 후반), 추가 점검을 하는 디버깅 구현을 허용하려는 명확한 의도가 있었다(예: 경계 검사에 지방 포인터를 사용하는 것).당시 논의를 통해, 디버깅 구현이 조합에서 초기화된 마지막 가치에 관한 정보를 캐싱하고, 다른 어떤 것에 접근하려고 하면 트랩할 수 있다는 것이 분명했다.이는 제6.7.2.1/16조에 분명히 명시되어 있다: "최소한의 조합원 중 한 명의 가치는 언제든지 조합의 물건에 저장될 수 있다."정의되지 않은 값에 액세스하는 것은 정의되지 않은 동작이며, 초기화되지 않은 변수에 액세스하는 것과 동일화될 수 있다.(같은 타입의 다른 회원에 접근하는 것이 합법적인 것인지에 대한 논의가 당시 일부 있었다.최종 결의안이 무엇인지는 모르겠지만 1990년 전후로 C++로 넘어갔다.)

C89의 인용구에 관해서, 그 행동이 구현에 정의되어 있다고 말하는 것은, 섹션 3(단어, 정의 및 기호)에서 그것을 발견하는 것은 매우 이상한 것처럼 보인다.나는 그것을 집에서 C90의 복사본에서 찾아봐야 할 것이다; 그것이 표준의 후반 버전에서 제거되었다는 사실은 그것이 위원회에 의해 오류로 여겨졌음을 시사한다.

표준이 지원하는 결합을 유도 시뮬레이션 수단으로 사용하는 것이다.다음을 정의할 수 있다.

struct NodeBase
{
    enum NodeType type;
};

struct InnerNode
{
    enum NodeType type;
    NodeBase* left;
    NodeBase* right;
};

struct ConstantNode
{
    enum NodeType type;
    double value;
};
//  ...

union Node
{
    struct NodeBase base;
    struct InnerNode inner;
    struct ConstantNode constant;
    //  ...
};

그리고 합법적인 접근 기지.다음을 통해 노드가 초기화되었음에도 불구하고inner. (제6.5.2.3/6조의 사실은 "특별한 보증이 하나 만들어진다." 그리고 이를 명시적으로 허용하는 것은 다른 모든 경우들이 정의되지 않은 행동이라는 것을 보여주는 매우 강력한 증거다.그리고 물론 §4/2의 "정의되지 않은 행동은 "미정의 행동"이라는 단어 또는 행동에 대한 명시적인 정의의 누락에 의해 본 국제 표준에 달리 표시된다"는 진술이 있다. 그 행동이 정의되지 않았다고 주장하기 위해서는 표준에서 정의되는 위치를 보여줘야 한다.)

마지막으로, 유형 설정과 관련하여, 모든(또는 적어도 내가 사용한 모든) 구현은 어떤 방식으로든 이를 지원한다.그 당시 나의 인상은 포인터 캐스팅이 구현을 지원하는 방식이라는 것이었다; C++ 표준에는, 심지어 (비규범적) 텍스트가 있다.reinterpret_cast기반 구조에 정통한 사람에게 「놀라운」이다.그러나 실무에서 대부분의 구현은 접속이 조합원을 통한 것이라면 형식연결을 위한 조합의 사용을 지원한다.대부분의 구현(g++는 아님)도 포인터 캐스트를 지원하며, 포인터 캐스트가 컴파일러(일부 지정되지 않은 포인터 캐스트 정의의 경우)에 선명하게 보인다면 지원된다.그리고 기반 하드웨어의 "표준화"는 다음과 같은 것을 의미한다.

int
getExponent( double d )
{
    return ((*(uint64_t*)(&d) >> 52) & 0x7FF) + 1023;
}

사실 휴대성이 꽤 좋아(물론 메인프레임에서는 효과가 없을 겁니다.)안 되는 것은 나의 첫 번째 예와 같은 것으로, 앨리어싱은 컴파일러에게 보이지 않는 것이다.(이것은 표준의 결함이라고 나는 거의 확신한다.나는 심지어 그것에 관한 DR을 본 적이 있는 것 같다.

C99는 합법이다.

표준으로부터 : 6.5.2.3 구조조합원

조합 오브젝트의 내용에 접근하는 데 사용된 멤버가 오브젝트에 값을 저장하기 위해 마지막으로 사용한 멤버와 동일하지 않을 경우, 해당 값의 오브젝트 표현 중 적절한 부분은 6.2.6("타입 펀닝"이라고도 하는 프로세스)에서 설명한 새로운 타입의 오브젝트 표현으로 재해석된다.이것은 함정 표현일 수도 있다.

간단한 답변: 유형 펀칭은 몇 가지 상황에서 안전할 수 있다.반면 매우 잘 알려진 관행인 것 같지만, 이를 공식화하는 데는 표준이 큰 관심이 없는 것으로 보인다.

는 C에 대해서만 말할 것이다(C++는 말할 수 없다.

1. 유형 펀닝 및 표준

사람들이 이미 지적했지만, 타입 펀닝표준 C99와 6.5.2.3항 C11에서 허용된다.그러나 나는 이 문제에 대한 나만의 인식으로 사실을 다시 쓰겠다.

  • 표준문서 C99와 C11의 6.5절표현의 주제를 개발한다.
  • 항목 6.5.2항을 사후 처리 표현식에 참조한다.
  • 하위섹션 6.5.2.3에서는 구조와 조합에 대해 설명한다.
  • 제6.5.2.3(3)항은 도트 연산자가 a에 적용된 것을 설명한다.struct또는union객체, 그리고 어떤 값을 얻을 것인가.
    바로 거기 95 각주가 나타난다.이 각주에는 다음과 같이 적혀 있다.

조합 오브젝트의 내용에 접근하는 데 사용된 멤버가 오브젝트에 값을 저장하기 위해 마지막으로 사용한 멤버와 동일하지 않을 경우, 해당 값의 오브젝트 표현 중 적절한 부분은 6.2.6("타입 펀닝"이라고도 하는 프로세스)에서 설명한 새로운 타입의 오브젝트 표현으로 재해석된다.이것은 함정 표현일 수도 있다.

활자 펀칭이 거의 나타나지 않고, 각주로서 C프로그래밍에서 관련 이슈가 아니라는 단서를 준다.
사실, 주된 사용 목적은 (메모리에 있는) 공간을 절약하는 것이다.여러 멤버가 같은 주소를 공유하고 있기 때문에, 각 멤버가 프로그램의 다른 부분을 사용하게 될 것을 알고 있다면, 결코 동시에 사용하지 않는다.union대신 사용할 수 있다.struct메모리 저장용.

  • 항목 6.2.6항을 언급한다.
  • 6.2.6항에서는 물체가 어떻게 표현되는지에 대해 설명한다(기억으로, 말하자면).

2. 유형 및 문제의 표현

표준의 다른 측면에 주의를 기울인다면 거의 아무것도 확신할 수 없다.

  • 포인터의 표현은 명확하게 명시되어 있지 않다.
  • 최악의 경우, 다른 유형의 포인터는 (메모리의 개체로서) 다른 표현을 할 수 있다.
  • union회원들은 메모리에 있는 동일한 머리글 주소를 공유하며, 그것은 같은 주소와 같은 주소다.union목적 그 자체
  • struct멤버는 정확히 동일한 메모리 주소에서 시작하여 상대 주소를 증가시킨다.struct목적 그자그 단, 할 수 그러나 패딩 바이트는 모든 멤버의 끝에 추가할 수 있다.몇 개나?예측할 수 없는 일이다.패딩 바이트는 주로 메모리 정렬 용도로 사용된다.
  • 산술적 유형(정수, 부동소수 실제 및 복합수)은 여러 가지 방법으로 나타낼 수 있다.실행 여부에 따라 달라진다.
  • 특히 정수형에는 패딩 비트가 있을 수 있다.내 생각에 이것은 데스크톱 컴퓨터에 대한 사실이 아니다.그러나 이 기준은 이 가능성을 열어두었다.패딩 비트는 수학적 값을 보유하는 것이 아니라 척추 목적(패리티, 신호, 아는 사람)에 사용된다.
  • signed유형에는 3가지 표현 방식이 있을 수 있다: 1의 보완, 2의 보완, 단지 신호 비트.
  • char타입은 단지 1바이트를 차지하지만, 1바이트는 8의 다른 비트 수를 가질 수 있다. (그러나 결코 8보다 작지 않다)
  • 그러나 우리는 몇 가지 세부사항에 대해 확신할 수 있다.

    a. a. Thechar종류에는 패딩 비트가 없다.
    b. Theunsigned정수형은 2진수 형태로 정확히 표현된다.
    c.unsigned char패딩 비트가 없는 정확히 1바이트를 차지하며, 모든 비트가 사용되기 때문에 트랩 표현은 없다.더욱이 정수 숫자의 이진 형식을 따르는, 모호성이 없는 값을 나타낸다.

3. 유형 펀닝 vs 유형 표현

이 모든 관찰 결과를 보면, 만약 우리가 다음과 같은 유형으로 말장난을 하려고 한다면,union를 가지고 있다.unsigned char애매한 부분이 많을 수도 있고.그것은 휴대용 코드가 아니며, 특히 우리 프로그램의 예측 가능한 행동을 할 수 있다.
그러나 이 표준은 이런 종류의 접근을 허용한다.

모든 유형이 우리의 구현에서 표현된다는 점에서 구체적인 방법에 대해 확신하더라도, 우리는 다른 유형에서는 전혀 의미 없는 일련의 비트(트랩 표현)를 가질 수 있다.이런 경우에는 우리는 아무것도 할 수 없다.

4. 안전 케이스: 서명되지 않은 문자

유형 펀닝을 사용하는 유일한 안전한 방법은unsigned char아니든 아니든unsigned char를 배열(배열)으로할 때 이다.sizeof()).

  union {
     TYPE data;
     unsigned char type_punning[sizeof(TYPE)];
  } xx;  

우리가 그것을 알고 있기 때문에.unsigned char엄격한 바이너리 형태로 표현되며, 패딩 비트가 없는 유형 펀닝을 사용하여 멤버의 바이너리 대표성을 살펴볼 수 있다.data.
이 도구는 특정 구현에서 특정 유형의 값이 어떻게 표현되는지를 분석하는 데 사용할 수 있다.

나는 표준 사양에 따라 또 다른 안전하고 유용한 유형의 펀칭 적용을 볼 수 없다.

5. 출연자에 대한 코멘트...

타입을 가지고 놀고 싶다면, 자신만의 변형 기능을 정의하거나, 아니면 그냥 깁스를 사용하는 것이 좋다.우리는 다음과 같은 간단한 예를 기억할 수 있다.

  union {
     unsigned char x;  
     double t;
  } uu;

  bool result;

  uu.x = 7;
  (uu.t == 7.0)? result = true: result = false;
  // You can bet that result == false

  uu.t = (double)(uu.x);
  (uu.t == 7.0)? result = true: result = false;
  // result == true

참조URL: https://stackoverflow.com/questions/25664848/unions-and-type-punning

반응형