programing

좌측 피연산자 값이 음수인 경우 좌측 시프트 작업이 정의되지 않은 동작을 호출하는 이유는 무엇입니까?

javaba 2022. 7. 28. 23:24
반응형

좌측 피연산자 값이 음수인 경우 좌측 시프트 작업이 정의되지 않은 동작을 호출하는 이유는 무엇입니까?

C에서 왼쪽 피연산자의 값이 음수이면 왼쪽 피연산자가 정의되지 않은 동작을 호출합니다.

ISO C99 (6.5.7/4)의 관련 견적

E1 < E2의 결과는 E1의 좌측 시프트 E2 비트 위치입니다.빈 비트는 0으로 구분됩니다.E1이 부호 없는 타입일 경우 결과 값은 E1 × 2로E2 결과 타입에서 나타낼 수 있는 최대값보다 1개 더 감소합니다.E1이 부호 있는 유형과 음이 아닌 값을 가지며, E1 × 2가E2 결과 유형으로 표현될 수 있는 경우, 그것은 결과 값이다. 그렇지 않은 경우, 동작은 정의되지 않는다.

그러나 C++에서는 동작이 잘 정의되어 있습니다.

ISO C++-03 (5.8/2)

E1 < E2의 값은 E1(비트 패턴으로 해석됨) 좌측 시프트E2 비트 위치입니다빈 비트는 0으로 채워집니다.E1이 부호 없는 타입이면 결과값은 E1에 거듭제곱 E2를 곱한 값, E1이 부호 없는 긴 타입이면 축소 모듈 ULONG_MAX+1이 된다.(주의: ULONG_MAX 및 UINT_MAX의 상수는 헤더에 정의되어 있습니다.)

즉,

int a = -1, b=2, c;
c= a << b ;

는 C에서 Undefined Behavior를 호출하지만 동작은 C++에서 잘 정의되어 있습니다.

무엇이 ISO C++ 위원회로 하여금 C의 행동과 반대로 잘 정의된 행동을 고려하게 했는가?

, 은 「」입니다.implementation defined왼쪽 피연산자가 음수일 때 오른쪽 비트 시프트 연산의 경우, 맞죠?

궁금한 점은 왼쪽 시프트 조작이 C에서 정의되지 않은 동작을 호출하는 이유와 오른쪽 시프트 오퍼레이터가 구현 정의된 동작만을 호출하는 이유는 무엇입니까?

추신: "표준에 정의되지 않은 행동"과 같은 답변은 하지 마십시오. :P

복사한 문단은 부호 없는 글씨체입니다.동작은 C++에서 정의되어 있지 않습니다.마지막 C++0x 드래프트부터:

E1 << E2 의 값은 E1 의 좌측 시프트 E2 비트 위치입니다.빈 비트는 0 으로 채워집니다.E1이 부호 없는 타입일 경우 결과 값은 E1 × 2로E2 결과 타입에서 나타낼 수 있는 최대값보다 1개 더 감소합니다.그렇지 않으면 E1에 부호 있는 타입과 음이 아닌 값이 있고 E1E2×2가 결과 타입으로 표현 가능한 경우, 그것이 결과값입니다.그렇지 않으면 동작은 정의되지 않습니다.

편집: C++98 용지를 확인했습니다.서명된 활자는 전혀 언급되지 않았습니다.그래서 아직 정의되지 않은 행동입니다.

우회전 네거티브는 구현이 정의되어 있습니다.왜일까요?제 의견으로는:왼쪽 문제에서 잘라낸 부분이 없기 때문에 구현 정의하기가 쉽습니다.왼쪽으로 이동할 때는 오른쪽에서 이동한 것뿐만 아니라 다른 비트에 대해서도 설명해야 합니다(예: 2의 보형 표현). 이는 다른 이야기입니다.

C 비트 왼쪽 이동 작업에서 왼쪽 피연산자의 값이 음수일 때 정의되지 않은 동작을 호출합니다. [...] 그러나 C++에서는 동작이 잘 정의됩니다. [...왜...]

쉬운 답은: 기준에 따르면 그렇게 되어 있기 때문입니다.

더 긴 대답은 다음과 같습니다.이것은 아마도 C와 C++ 둘 다 2의 보수가 아닌 음수에 대한 다른 표현을 허용한다는 사실과 관련이 있을 것이다.앞으로 일어날 일에 대한 보증이 적기 때문에 불분명한 머신이나 오래된 머신 등 다른 하드웨어에서 언어를 사용할 수 있습니다.

어떤 이유에서인지 C++ 표준화 위원회는 비트 표현이 어떻게 변화하는지 약간의 보증을 추가하고 싶어했습니다.그러나 음수는 여전히 1의 보완 또는 부호+크기를 통해 표시될 수 있기 때문에 결과 값 가능성은 여전히 다양합니다.

16비트 ints를 가정하면

 -1 = 1111111111111111  // 2's complement
 -1 = 1111111111111110  // 1's complement
 -1 = 1000000000000001  // sign+magnitude

왼쪽으로 3만큼 이동하면

 -8 = 1111111111111000  // 2's complement
-15 = 1111111111110000  // 1's complement
  8 = 0000000000001000  // sign+magnitude

무엇이 ISO C++ 위원회로 하여금 C의 행동과 반대로 잘 정의된 행동을 고려하게 했는가?

이 보증은 <<>를 사용하고 있는 것을 알고 있을 때(즉, 사용의 머신에 2의 보수가 사용되고 있는 것이 확실할 때)에 적절히 사용할 수 있도록 하고 있다고 생각합니다.

한편 왼쪽 피연산자가 음의 경우 동작은 비트별 오른쪽 시프트 동작에 대해 정의됩니다.

기준을 확인해 봐야겠어요.하지만 당신 말이 맞을 수도 있어요.2의 보완 기계에서 기호 확장자가 없는 오른쪽 이동은 특별히 유용하지 않습니다.따라서 현재 상태는 빈 비트를 0으로 채우도록 요구하는 것보다 확실히 좋습니다.이는 부호 확장을 실행하는 머신에 여유가 있기 때문입니다.

C89에서는 부호 있는 정수형과 부호 없는 정수형에 패딩 비트를 사용하지 않는 2개의 보완 플랫폼에서 왼쪽 음수 변환의 동작이 명확하게 정의되었습니다.부호 있는 타입과 부호 없는 타입의 값 비트는 공통적으로 같은 장소에 존재하며, 부호 없는 타입의 부호 비트는 부호 없는 타입의 상위 값 비트와 같은 장소에 존재해야 합니다.따라서 부호 없는 타입의 부호 비트는 다른 모든 것의 왼쪽에 있어야 합니다.

C89의 필수 동작은 패딩이 없는 두 개의 보완 플랫폼에 유용하고 합리적이었으며, 적어도 이들을 곱셈으로 처리하면 오버플로가 발생하지 않는 경우였다.이 동작은 다른 플랫폼이나 서명된 정수 오버플로를 확실하게 트랩하려는 구현에서는 최적이 아닐 수 있습니다.C99의 저자들은 아마도 C89의 의무적인 동작이 이상적이지 않을 경우 구현의 유연성을 허용하고 싶었을 것입니다.그러나 그 근거에 따르면 품질 구현이 다른 강제적인 이유가 없는 경우 이전 방식으로 계속 수행되어서는 안 된다는 의도는 없습니다.

불행하게도 2의 보완연산을 사용하지 않는 C99의 실장은 없었지만, C11의 저자들은 공통 케이스(비오버플로) 동작의 정의를 거부했습니다.IIRC는 그렇게 하면 "최적화"를 방해할 것이라고 주장했습니다.왼쪽 피연산자가 음수일 때 왼쪽 피연산자가 정의되지 않은 동작을 호출하도록 하면 컴파일러는 왼쪽 피연산자가 음수가 아닌 경우에만 시프트에 도달할 수 있다고 가정할 수 있습니다.

이러한 최적화가 실제로 얼마나 자주 유용한지는 의문이지만, 그러한 유용성의 희귀성은 실제로 행동을 정의하지 않은 채로 두는 데 유리하게 작용합니다.2개의 보완 구현이 일반적인 방식으로 동작하지 않는 유일한 상황이 최적화가 실제로 도움이 되는 경우, 그리고 그러한 상황이 실제로 존재하지 않는 경우, 구현은 명령이 있든 없든 일반적인 방식으로 동작하므로 동작을 명령할 필요가 없습니다.

실제 질문에 대한 답변은 다음과 같습니다.서명된 유형의 연산에 대해서는 수학적 연산의 결과가 대상 유형에 맞지 않는 경우(언더 또는 오버플로)에는 정의되지 않은 동작이 정의되지 않았습니다.부호 있는 정수형은 이렇게 설계되어 있습니다.

왼쪽 교대 연산의 경우 값이 양수 또는 0인 경우 연산자를 2의 거듭제곱으로 정의하는 것이 타당하므로 결과가 오버플로하지 않는 한 모든 것이 정상입니다.

값이 음수일 경우 곱셈에 대한 해석은 2의 거듭제곱으로 동일하게 할 수 있지만 비트 시프트의 관점에서만 생각하면 놀라운 일이 될 수 있습니다.분명히 표준 위원회는 그러한 모호함을 피하고 싶어했다.

결론:

  • 실제 비트 패턴 작업을 수행하려면 부호 없는 유형을 사용합니다.
  • 값(서명 여부에 관계없이)에 2의 거듭제곱을 하려면 다음과 같이 하십시오.

    i * (1u << k)

컴파일러는 어떤 경우에도 이를 적절한 어셈블러로 변환합니다.

이동 결과는 숫자 표현에 따라 달라집니다.이동은 숫자가 2의 보완으로 표현될 때만 곱셈과 같이 동작합니다.하지만 이 문제는 음수만의 문제가 아니다.8을 초과하는 4비트 부호화 숫자(오프셋 이진수라고도 함)를 고려합니다.숫자 1은 1+8 또는 1001로 표시됩니다. 이것을 비트로 이동시키면 -6을 나타내는 0010이 됩니다.마찬가지로 -1은 -1+8 0111로 나타나며, 왼쪽 이동하면 +6을 나타내는 1110이 됩니다.비트 단위의 동작은 명확하게 정의되어 있지만, 수치적인 동작은 표현 방식에 따라 크게 좌우됩니다.

이러한 종류의 대부분은 하나의 명령으로 실제로 지원할 수 있는 일반적인 CPU와 추가 명령이 필요한 경우에도 컴파일러 라이터가 보증할 수 있을 만큼 유용한 CPU 간의 균형입니다.일반적으로 비트 시프트 연산자를 사용하는 프로그래머는 이러한 명령으로 CPU의 단일 명령에 매핑될 것으로 예상하기 때문에 CPU가 동작을 강요하거나 동작이 예기치 않게 느려지는 것이 아니라 "엣지" 조건을 다양하게 처리하는 경우에는 정의되지 않거나 구현 동작이 발생합니다.간단한 사용 사례에서도 추가 사전/사후 또는 취급 지침이 제공될 수 있습니다.일부 CPU가 트랩/예외/인터럽트(C++ try/catch 타입의 예외와 구별됨) 또는 일반적으로 무용지물/불가결한 결과를 생성하는 경우 정의되지 않은 동작이 필요했을 수 있습니다.또한 표준위원회가 검토한 CPU 세트가 적어도 몇 가지 정의된 동작을 제공하는 경우에는 동작이 비활성화될 수 있습니다.레멘테이션이 정의되어 있습니다.

궁금한 점은 왼쪽 시프트 조작이 C에서 정의되지 않은 동작을 호출하는 이유와 오른쪽 시프트 오퍼레이터가 구현 정의된 동작만을 호출하는 이유는 무엇입니까?

LLVM 관계자들은 명령이 다양한 플랫폼에서 구현되는 방식 때문에 시프트 오퍼레이터가 제약을 받는다고 추측합니다.정의되지 않은 동작 #1/3에 대해 모든 C 프로그래머가 알아야 할 것:

...여러 CPU의 기본적인 시프트 조작에 의해, 예를 들면 X86은 32비트 시프트의 양을 5비트로 잘라냅니다(따라서 32비트 시프트의 양은 0비트의 시프트와 같음), PowerPC는 32비트 시프트의 양을 6비트로 잘라냅니다(따라서 32비트 시프트의 경우는 0이 됩니다).이러한 하드웨어 차이로 인해 동작은 C에 의해 완전히 정의되지 않습니다.

논의는 레지스터 크기보다 더 큰 금액을 이동시키는 것에 관한 것이었음을 네이트하십시오.하지만 제가 찾은 것 중 가장 가까운 것은 권위자의 변화 제약에 대한 설명입니다.

번째 이유는 2의 칭찬 기계에서 신호가 바뀔 수 있기 때문이라고 생각합니다.하지만 어디서도 읽어본 적이 없다(@selibitze(그리고 우연히 그의 의견에 동의함)).

C++03 의 동작은, C++11 및 C99 의 동작과 같기 때문에, 왼쪽 시프트의 룰을 넘으면 됩니다.

이 기준서의 섹션 5p5는 다음과 같이 기술하고 있다.

식을 평가하는 동안 결과가 수학적으로 정의되지 않았거나 해당 유형의 대표 가능한 값 범위에 포함되지 않은 경우 동작은 정의되지 않습니다.

정의되지 않은 동작으로서 C99 및 C++11에서 특별히 호출되는 왼쪽 시프트 표현식은 대표 가능한 값의 범위를 벗어난 결과로 평가되는 표현식과 동일합니다.

실제로 모듈러 산술을 사용한 부호 없는 타입에 대한 문장은 대표 가능한 범위를 벗어난 값을 생성하지 않기 위해 특별히 존재하며, 이는 자동으로 정의되지 않은 동작입니다.

언급URL : https://stackoverflow.com/questions/3784996/why-does-left-shift-operation-invoke-undefined-behaviour-when-the-left-side-oper

반응형