IT박스

유니온과 타입 -punning

itboxs 2020. 12. 2. 08:15
반응형

유니온과 타입 -punning


한동안 검색했지만 명확한 답을 찾을 수 없습니다.

많은 사람들이 공용체를 사용하여 유형을 말장난하는 것은 정의되지 않고 나쁜 습관이라고 말합니다. 왜 이런거야? 원래 정보를 작성하는 메모리가 자체적으로 변경되는 것이 아니라는 점을 고려할 때 정의되지 않은 작업을 수행하는 이유를 알 수 없습니다 (스택에서 범위를 벗어나지 않는 한, 이는 통합 문제가 아닙니다.) , 그것은 나쁜 디자인 일 것입니다).

사람들은 엄격한 앨리어싱 규칙을 인용하지만 그것은 당신이 할 수 없기 때문에 할 수 없다고 말하는 것과 같습니다.

또한 말장난을 입력하지 않으면 노조의 요점은 무엇입니까? 다른 시간에 다른 정보에 대해 동일한 메모리 위치를 사용하는 데 사용되어야하는 어딘가에서 보았지만 다시 사용하기 전에 정보를 삭제하지 않는 이유는 무엇입니까?

요약하면 :

  1. 유형 punning에 공용체를 사용하는 것이 왜 나쁜가요?
  2. 이것이 아니라면 그들의 요점은 무엇입니까?

추가 정보 : 저는 주로 C ++를 사용하고 있지만 이것과 C에 대해 알고 싶습니다. 특히 CAN 버스를 통해 보낼 수있는 부동 소수점과 원시 16 진수 사이를 변환하기 위해 공용체를 사용하고 있습니다.


다시 말하면, 공용체를 통한 타입 -punning은 C에서 완벽하게 괜찮습니다 (C ++에서는 아님). 반대로 포인터 캐스트를 사용하면 C99 엄격한 앨리어싱을 위반하고 유형에 따라 정렬 요구 사항이 다를 수 있고 잘못 수행하면 SIGBUS를 발생시킬 수 있기 때문에 문제가됩니다. 노조를 사용하면 이것은 결코 문제가되지 않습니다.

C 표준의 관련 인용문은 다음과 같습니다.

C89 섹션 3.3.2.3 §5 :

값이 객체의 다른 멤버에 저장된 후 공용체 객체의 멤버에 액세스하는 경우 동작은 구현에서 정의됩니다.

C11 섹션 6.5.2.3 §3 :

접미사 표현식 뒤에. 연산자와 식별자는 구조체 또는 공용체 개체의 구성원을 지정합니다. 값은 명명 된 멤버의 값입니다.

다음 각주 95 포함 :

공용체 개체의 내용을 읽는 데 사용 된 멤버가 개체에 값을 저장하는 데 마지막으로 사용 된 멤버와 동일하지 않은 경우 값의 개체 표현 중 적절한 부분은 다음과 같이 새 형식의 개체 표현으로 재 해석됩니다. 6.2.6에 설명되어 있습니다 ( "유형 제거"라고도하는 프로세스). 이것은 트랩 표현 일 수 있습니다.

이것은 완전히 명확해야합니다.


James는 C11 섹션 6.7.2.1 §16이 읽기 때문에 혼란 스럽습니다.

멤버 중 최대 하나의 값은 언제든지 공용체 개체에 저장할 수 있습니다.

이것은 모순되는 것처럼 보이지만 그렇지 않습니다. C ++와 달리 C에서는 활성 멤버의 개념이 없으며 호환되지 않는 유형의 표현식을 통해 단일 저장된 값에 액세스하는 것이 완벽합니다.

C11 부속서 J.1 §1 참조 :

[지정되지 않음]에 마지막으로 저장된 유니온 멤버 이외의 유니온 멤버에 해당하는 바이트 값입니다.

C99에서 이것은

에 저장된 마지막 멤버 이외의 유니온 멤버의 값은 [지정되지 않음]

이것은 올바르지 않습니다. 부속서는 규범 적이 지 않기 때문에 자체 TC를 평가하지 않았으며 다음 표준 개정이 수정 될 때까지 기다려야했습니다.


표준 C ++ (및 C90에 대한)에 대한 GNU 확장 은 공용체를 사용한 유형 실행을 명시 적으로 허용합니다 . GNU 확장을 지원하지 않는 다른 컴파일러도 공용체 유형 실행을 지원할 수 있지만 기본 언어 표준의 일부는 아닙니다.


노동 조합은 원래의 목적은 우리가 부르는 다른 종류의 표현 할 수 할 수 있도록 할 때 공간을 절약하는 것이었다 변형 유형이 참조 Boost.Variant을 이의 좋은 예로한다.

다른 일반적인 용도는 타입 punning 으로 이것의 유효성은 논쟁의 여지가 있지만 거의 대부분의 컴파일러가 지원합니다. gcc가 지원을 문서화 한다는 것을 알 수 있습니다 .

가장 최근에 쓴 조합원이 아닌 다른 조합원으로부터 읽는 관행 ( "타입 -punning"이라고 함)이 일반적입니다. -fstrict-aliasing을 사용하더라도 공용체 유형을 통해 메모리에 액세스하면 type-punning이 허용됩니다. 따라서 위의 코드는 예상대로 작동합니다.

-fstrict-aliasing을 사용해도 type-punning이 허용된다는 점에 유의하십시오. 이는 플레이시 앨리어싱 문제가 있음을 나타냅니다.

Pascal Cuoq는 결함 보고서 283 이 이것이 C에서 허용됨을 명확히 한다고 주장 했습니다. 결함 보고서 283 은 설명으로 다음 각주를 추가했습니다.

공용체 개체의 내용에 액세스하는 데 사용 된 멤버가 개체에 값을 저장하는 데 마지막으로 사용 된 멤버와 같지 않은 경우 값의 개체 표현 중 적절한 부분은 다음과 같이 새 형식의 개체 표현으로 재 해석됩니다. 6.2.6에 설명되어 있습니다 ( "유형 제거"라고도하는 프로세스). 이것은 트랩 표현 일 수 있습니다.

C11에서는 각주가 95됩니다.

에서 비록 std-discussion메일 그룹 주제 유니온을 통해 유형 말장난 때문에 합리적인 것으로 보인다 인수가이 underspecified되어 만든, 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, Strict Aliasing 및 Optimization 도 비슷한 결론을 내립니다.

정의되지 않은 행동 메일 링리스트 토론 : 복사를 피하기위한 유형 punning 은 많은 동일한 영역을 다루며 영역이 얼마나 회색 일 수 있는지 볼 수 있습니다.


C99에서는 합법적입니다.

표준에서 : 6.5.2.3 구조 및 조합 구성원

공용체 개체의 내용에 액세스하는 데 사용 된 멤버가 개체에 값을 저장하는 데 마지막으로 사용 된 멤버와 같지 않은 경우 값의 개체 표현 중 적절한 부분은 다음과 같이 새 형식의 개체 표현으로 재 해석됩니다. 6.2.6에 설명되어 있습니다 ( "유형 제거"라고도하는 프로세스). 이것은 트랩 표현 일 수 있습니다.


간단한 답변 : 유형 punning 은 몇 가지 상황에서 안전 할 수 있습니다. 다른 한편으로는 매우 잘 알려진 관행 인 것처럼 보이지만 표준이 공식화하는 데는별로 관심이없는 것 같습니다.

C (C ++ 아님)에 대해서만 이야기하겠습니다 .

1. 유형 퍼닝 및 표준

사람들이 이미 지적했듯이 유형 punning 은 표준 C99 및 C11, 하위 섹션 6.5.2.3 에서 허용됩니다 . 그러나 문제에 대한 내 자신의 인식으로 사실을 다시 작성하겠습니다.

  • 표준 문서 C99 및 C11 의 섹션 6.5표현식 주제를 개발합니다 .
  • 서브 섹션 6.5.2접미사 표현식을 참조합니다 .
  • 하위 섹션 6.5.2.3에서는 구조체와 공용체 에 대해 설명 합니다 .
  • 단락 6.5.2.3 (3) 설명 도트 연산자 (A)에 도포 struct또는 union개체를 어떤 값이 얻어진다.
    바로 거기에 각주 95 가 나타납니다. 이 각주는 다음과 같이 말합니다.

공용체 개체의 내용에 액세스하는 데 사용 된 멤버가 개체에 값을 저장하는 데 마지막으로 사용 된 멤버와 같지 않은 경우 값의 개체 표현 중 적절한 부분은 다음과 같이 새 형식의 개체 표현으로 재 해석됩니다. 6.2.6에 설명되어 있습니다 ( "유형 제거"라고도하는 프로세스). 이것은 트랩 표현 일 수 있습니다.

사실 형은 말장난 거의가 나타나고 각주로, 그것은 C 프로그래밍에 관련된 문제가 아니라고 단서를 제공합니다.
실제로 사용의 주된 목적은unions (메모리에서) 공간을 절약 하는 것입니다 . 여러 멤버가 동일한 주소를 공유하기 때문에 각 멤버가 프로그램의 다른 부분을 동시에 사용하지 않는다는 것을 안다면 메모리를 절약하기 위해 union대신 a를 사용할 수 있습니다 struct.

  • 6.2.6 항이 언급됩니다.
  • 하위 섹션 6.2.6 은 객체가 (예를 들어 메모리에서) 표현되는 방법에 대해 설명합니다.

2. 유형 및 문제의 표현

표준의 여러 측면에주의를 기울이면 거의 아무것도 확신 할 수 없습니다.

  • 포인터의 표현이 명확하게 지정되지 않았습니다.
  • 최악의 경우 유형이 다른 포인터는 (메모리의 객체로) 다른 표현을 가질 수 있습니다.
  • union멤버는 메모리에서 동일한 제목 주소를 공유하며 union객체 자체 의 주소와 동일 합니다.
  • struct멤버는 struct객체 자체 의 메모리 주소와 정확히 동일한 메모리 주소에서 시작하여 증가하는 상대 주소를 갖 습니다. 그러나 모든 멤버의 끝에 패딩 바이트를 추가 할 수 있습니다. 얼마나? 예측할 수 없습니다. 패딩 바이트는 주로 메모리 할당 용도로 사용됩니다.
  • 산술 유형 (정수, 부동 소수점 실수 및 복소수)은 여러 가지 방법으로 표현할 수 있습니다. 구현에 따라 다릅니다.
  • 특히 정수 유형은 패딩 비트를 가질 수 있습니다 . 이것은 데스크탑 컴퓨터에서는 사실이 아닙니다. 그러나 표준은 이러한 가능성에 대한 문을 열어 두었습니다. 패딩 비트는 수학적 값을 유지하는 것이 아니라 특정 목적 (패리티, 신호, 아는 사람)을 위해 사용됩니다.
  • signed 타입은 1의 보수, 2의 보수, 단지 부호 비트의 3 가지 표현 방식을 가질 수 있습니다.
  • char종류는 1 바이트를 차지하지만, 1 바이트 (하지만 결코 적은 8 이하) 8의 서로 다른 비트 수를 가질 수 있습니다.
  • 그러나 우리는 몇 가지 세부 사항에 대해 확신 할 수 있습니다.

    ㅏ. char종류의 비트를 패딩하지 않았습니다.
    비. unsigned정수 타입은 정확하게 바이너리 형태로 표시됩니다.
    씨. unsigned char패딩 비트없이 정확히 1 바이트를 차지하며 모든 비트가 사용되기 때문에 트랩 표현이 없습니다. 또한 정수에 대한 이진 형식을 따르는 모호함이없는 값을 나타냅니다.

3. 유형 퍼닝 vs 유형 표현

이러한 모든 관찰은 유형이 다른 멤버로 유형 punning시도 하면 많은 모호성을 가질 수 있음을 보여줍니다 . 이식 가능한 코드가 아니며, 특히 프로그램에서 예측할 수없는 동작을 할 수 있습니다. 그러나 표준은 이러한 종류의 액세스를 허용합니다 .unionunsigned char

구현에서 모든 유형이 표현된다는 특정 방식에 대해 확신하더라도 다른 유형에서는 전혀 의미가없는 비트 시퀀스 ( 트랩 표현 )를 가질 수 있습니다 . 이 경우 우리는 아무것도 할 수 없습니다.

4. 안전한 경우 : unsigned char

유형 punning 을 사용하는 유일한 안전한 방법 은 with unsigned charor well unsigned char배열입니다 (배열 객체의 멤버가 엄격하게 연속적이며 크기가로 계산 될 때 패딩 바이트가 없다는 것을 알고 있기 때문입니다 sizeof()).

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

unsigned char패딩 비트없이 엄격한 이진 형식으로 표현 된다는 것을 알기 때문에 여기서 유형 punning을 사용하여 멤버의 이진 표현을 살펴볼 수 있습니다 data.
이 도구는 특정 구현에서 특정 유형의 값이 표현되는 방식을 분석하는 데 사용할 수 있습니다.

표준 사양 하에서 유형 punning 의 또 다른 안전하고 유용한 응용 프로그램을 볼 수 없습니다 .

5. CASTS에 대한 의견 ...

유형을 가지고 놀고 싶다면 자신의 변형 함수를 정의하거나 캐스트를 사용하는 것이 좋습니다 . 이 간단한 예를 기억할 수 있습니다.

  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

There are (or at least were, back in C90) two modivations for making this undefined behavior. The first was that a compiler would be allowed to generate extra code which tracked what was in the union, and generated a signal when you accessed the wrong member. In practice, I don't think any one ever did (maybe CenterLine?). The other was the optimization possibilities this opened up, and these are used. I have used compilers which would defer a write until the last possible moment, on the grounds that it might not be necessary (because the variable goes out of scope, or there is a subsequent write of a different value). Logically, one would expect that this optimization would be turned off when the union was visible, but it wasn't in the earliest versions of Microsoft C.

The issues of type punning are complex. The C committee (back in the late 1980's) more or less took the position that you should use casts (in C++, reinterpret_cast) for this, and not unions, although both techniques were widespread at the time. Since then, some compilers (g++, for example) have taken the opposite point of view, supporting the use of unions, but not the use of casts. And in practice, neither work if it is not immediately obvious that there is type-punning. This might be the motivation behind g++'s point of view. If you access a union member, it is immediately obvious that there might be type-punning. But of course, given something like:

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

called with:

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

is perfectly legal according to the strict rules of the standard, but fails with g++ (and probably many other compilers); when compiling f, the compiler assumes that pi and pd can't alias, and reorders the write to *pd and the read from *pi. (I believe that it was never the intent that this be guaranteed. But the current wording of the standard does guarantee it.)

EDIT:

Since other answers have argued that the behavior is in fact defined (largely based on quoting a non-normative note, taken out of context):

The correct answer here is that of pablo1977: the standard makes no attempt to define the behavior when type punning is involved. The probable reason for this is that there is no portable behavior that it could define. This does not prevent a specific implementation from defining it; although I don't remember any specific discussions of the issue, I'm pretty sure that the intent was that implementations define something (and most, if not all, do).

With regards to using a union for type-punning: when the C committee was developing C90 (in the late 1980's), there was a clear intent to allow debugging implementations which did additional checking (such as using fat pointers for bounds checking). From discussions at the time, it was clear that the intent was that a debugging implementation might cache information concerning the last value initialized in a union, and trap if you tried to access anything else. This is clearly stated in §6.7.2.1/16: "The value of at most one of the members can be stored in a union object at any time." Accessing a value that isn't there is undefined behavior; it can be assimilated to accessing an uninitialized variable. (There were some discussions at the time as to whether accessing a different member with the same type was legal or not. I don't know what the final resolution was, however; after around 1990, I moved on to C++.)

With regards to the quote from C89, saying the behavior is implementation-defined: finding it in section 3 (Terms, Definitions and Symbols) seems very strange. I'll have to look it up in my copy of C90 at home; the fact that it has been removed in later versions of the standards suggests that its presence was considered an error by the committee.

The use of unions which the standard supports is as a means to simulate derivation. You can define:

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;
    //  ...
};

and legally access base.type, even though the Node was initialized through inner. (The fact that §6.5.2.3/6 starts with "One special guarantee is made..." and goes on to explicitly allow this is a very strong indication that all other cases are meant to be undefined behavior. And of course, there is the statement that "Undefined behavior is otherwise indicated in this International Standard by the words ‘‘undefined behavior’’ or by the omission of any explicit definition of behavior" in §4/2; in order to argue that the behavior is not undefined, you have to show where it is defined in the standard.)

Finally, with regards to type-punning: all (or at least all that I've used) implementations do support it in some way. My impression at the time was that the intent was that pointer casting be the way an implementation supported it; in the C++ standard, there is even (non-normative) text to suggest that the results of a reinterpret_cast be "unsurprising" to someone familiar with the underlying architecture. In practice, however, most implementations support the use of union for type-punning, provided the access is through a union member. Most implementations (but not g++) also support pointer casts, provided the pointer cast is clearly visible to the compiler (for some unspecified definition of pointer cast). And the "standardization" of the underlying hardware means that things like:

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

are actually fairly portable. (It won't work on mainframes, of course.) What doesn't work are things like my first example, where the aliasing is invisible to the compiler. (I'm pretty sure that this is a defect in the standard. I seem to recall even having seen a DR concerning it.)

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

반응형