구현 정의 동작을 피하는 효율적인 unsigned-to-signed 캐스트


94

나는 그것을 취하는 함수를 정의하고 싶다. unsigned int인수로 인수에 int합동 모듈로 UINT_MAX + 1을 반환하는 .

첫 번째 시도는 다음과 같습니다.

int unsigned_to_signed(unsigned n)
{
    return static_cast<int>(n);
}

그러나 모든 언어 변호사가 알고 있듯이 INT_MAX보다 큰 값에 대해 unsigned에서 signed로 캐스팅하는 것은 구현에 따라 정의됩니다.

나는 이것을 구현하여 (a) 사양에서 요구하는 동작에만 의존합니다. (b) 최신 기계 및 최적화 컴파일러에서 no-op으로 컴파일됩니다.

기괴한 기계에 관해서는 ... unsigned int에 대해 unsigned int 합동 모듈로 UINT_MAX + 1이 없다면 예외를 던지고 싶다고합시다. 둘 이상이있는 경우 (가능한지 모르겠 음) 가장 큰 것을 원한다고 가정 해 봅시다.

좋아요, 두 번째 시도 :

int unsigned_to_signed(unsigned n)
{
    int int_n = static_cast<int>(n);

    if (n == static_cast<unsigned>(int_n))
        return int_n;

    // else do something long and complicated
}

내 겸손한 의견으로는 가능성이 낮기 때문에 일반적인 2 점 보완 시스템을 사용하지 않을 때 효율성에 대해별로 신경 쓰지 않습니다. 그리고 내 코드가 2050 년에 편재하는 부호 크기 시스템에서 병목 현상이 발생하면 누군가가이를 알아 내고 최적화 할 수있을 것입니다.

자,이 두 번째 시도는 내가 원하는 것에 매우 가깝습니다. 에 캐스팅 있지만 int구현 정의 어떤 입력에 대해,에 캐스팅 돌아unsigned 표준에 의해 은 모듈로 UINT_MAX + 1 값을 유지하도록 보장됩니다. 따라서 조건문은 내가 원하는 것을 정확히 확인하고 내가 만날 가능성이있는 시스템에서 아무것도 컴파일하지 않습니다.

그러나 ... int구현 정의 동작을 호출할지 여부를 먼저 확인하지 않고 여전히 캐스팅 중 입니다. 2050 년의 일부 가상 시스템에서는 누가 무엇을 알고 있는지 할 수 있습니다. 그래서 그것을 피하고 싶다고 가정 해 봅시다.

질문 : "세 번째 시도"는 어떤 모습이어야합니까?

요약하면 다음과 같습니다.

  • unsigned int에서 signed int로 캐스트
  • mod UINT_MAX + 1 값 유지
  • 표준 필수 동작 만 호출
  • 최적화 컴파일러를 사용하여 일반적인 2- 보완 머신에서 no-op으로 컴파일

[최신 정보]

이것이 사소한 질문이 아닌 이유를 보여주는 예를 들어 보겠습니다.

다음 속성을 사용하여 가상의 C ++ 구현을 고려하십시오.

  • sizeof(int) 4와 같음
  • sizeof(unsigned) 4와 같음
  • INT_MAX 32767과 같음
  • INT_MIN-2 32 + 32768과 같습니다.
  • UINT_MAX2와 동일 (32) - (1)
  • 산술 int은 모듈로 2 32입니다 (부터 범위 INT_MIN까지 INT_MAX).
  • std::numeric_limits<int>::is_modulo 사실이다
  • unsigned n를 int로 캐스팅 하면 0 <= n <= 32767의 값이 유지되고 그렇지 않으면 0이 생성 됩니다.

이 가상 구현에서는 intunsigned값에 합동하는 정확히 하나의 값 (mod UINT_MAX + 1)이 있습니다 . 그래서 내 질문은 잘 정의되어 있습니다.

이 가상의 C ++ 구현은 C ++ 98, C ++ 03 및 C ++ 11 사양을 완전히 준수한다고 주장합니다. 나는 그들 모두의 모든 단어를 암기하지 않았다는 것을 인정한다. 그러나 나는 관련 섹션을주의 깊게 읽었다 고 믿는다. 그래서 제가 당신의 대답을 받아들이 길 원한다면 (a)이 가상의 구현을 배제하는 사양을 인용하거나 (b) 올바르게 처리해야합니다.

실제로 정답은 표준에서 허용하는 모든 가상 구현을 처리해야합니다 . 이것이 정의상 "표준 필수 동작 만 호출"이라는 의미입니다.

덧붙여서, std::numeric_limits<int>::is_modulo여기에서는 여러 가지 이유로 완전히 쓸모가 없습니다. 우선, true서명되지 않은 캐스트가 큰 서명되지 않은 값에 대해 작동하지 않는 경우에도 가능합니다. 다른 true경우에는 산술이 전체 정수 범위를 단순히 모듈로하는 경우 보완 또는 부호 크기 시스템에서도 가능합니다. 등등. 귀하의 답변이에 따라 다르다면 is_modulo잘못된 것입니다.

[업데이트 2]

hvd의 대답 은 나에게 뭔가를 가르쳐주었습니다. 정수에 대한 나의 가상 C ++ 구현은 현대 C에서 허용 되지 않습니다 . C99 및 C11 표준은 부호있는 정수의 표현에 대해 매우 구체적입니다. 실제로, 그들은 2 개 보수, 1 개 보수 및 부호 크기 만 허용합니다 (섹션 6.2.6.2 단락 (2);).

그러나 C ++는 C가 아닙니다. 결과적으로이 사실은 제 질문의 핵심입니다.

원래 C ++ 98 표준은 훨씬 더 오래된 C89를 기반으로합니다 (섹션 3.1.2.5).

각 부호있는 정수 유형에 대해 동일한 양의 스토리지 (부호 정보 포함)를 사용하고 동일한 정렬 요구 사항을 갖는 해당 (그러나 다른) 부호없는 정수 유형 (부호없는 키워드로 지정됨)이 있습니다. 부호있는 정수 유형의 음이 아닌 값의 범위는 해당하는 부호없는 정수 유형의 하위 범위이며 각 유형에서 동일한 값의 표현은 동일합니다.

C89는 하나의 부호 비트 만 가지고 있거나 두 개의 보수 / 일-보완 / 부호 크기 만 허용하는 것에 대해 아무 말도하지 않습니다.

C ++ 98 표준은이 언어를 거의 그대로 채택했습니다 (섹션 3.9.1 단락 (3)) :

각각의 부호있는 정수 유형에 대해 해당하는 (그러나 다른) 부호없는 정수 유형 인 " unsigned char", " unsigned short int", " unsigned int"및 " unsigned long int"이 있으며, 각 유형은 동일한 양의 스토리지를 차지하고 동일한 정렬 요구 사항을 갖습니다 (3.9 ) 해당 부호있는 정수 유형으로; 즉, 각 부호있는 정수 유형은 해당하는 부호없는 정수 유형 과 동일한 객체 표현을 갖습니다 . 부호있는 정수형의 음이 아닌 값의 범위는 해당하는 부호없는 정수형의 하위 범위이며, 각 해당 부호 / 부호없는 형식의 값 표현은 동일해야합니다.

C ++ 03 표준은 C ++ 11과 동일한 언어를 사용합니다.

내가 말할 수있는 한 표준 C ++ 사양은 부호있는 정수 표현을 C 사양으로 제한하지 않습니다. 그리고 싱글 사인 비트나 그 어떤 종류의 것을 요구하는 것은 없습니다. 아닌 부호있는 정수는 해당하는 부호없는 범위의 하위 범위 여야한다는 것입니다.

그래서 다시 INT_MIN = -2 32 +32768의 INT_MAX = 32767 이 허용된다고 주장합니다 . 귀하의 답변이 그렇지 않다고 가정하면 C ++ 표준 을 인용하지 않는 한 잘못된 것입니다.


@SteveJessop : 사실, 저는 그 경우에 제가 원하는 것을 정확히 말했습니다 : "unsigned int에 부호있는 int 합동 모듈로 UINT_MAX + 1이 없다면 예외를 던지고 싶다고 가정 해 보겠습니다." 즉, 존재하는 경우 "올바른"서명 된 int를 원합니다. 그것이 존재하지 않는다면-예를 들어 패딩 비트 또는 1- 보완 표현의 경우에 발생할 수 있습니다-나는 그것을 감지하고 캐스트의 특정 호출에 대해 처리하고 싶습니다.
Nemo

죄송합니다. 어떻게 놓쳤는 지 잘 모르겠습니다.
Steve Jessop 2013

Btw, 나는 당신의 가상의 까다로운 구현 int에서 그것을 표현하기 위해 적어도 33 비트가 필요 하다고 생각 합니다. 나는 그것이 각 주일 뿐이라는 것을 알고 있으므로 그것이 비 규범 적이라고 주장 할 수 있지만, C ++ 11의 각주 49는 (표준에서 사용되는 용어의 정의이기 때문에) 사실을 의도하고 있다고 생각 하며 모순되지 않습니다. 규범 텍스트에 명시 적으로 언급 된 모든 것. 따라서 모든 음수 값은 가장 높은 비트가 설정된 비트 패턴으로 표시되어야하므로 2^32 - 3276832 비트로 압축 할 수 없습니다 . 당신의 주장이 int.
Steve Jessop 2013

그리고 hvd의 답변에 대한 편집과 관련하여 49 번 주를 잘못 해석했다고 생각합니다. 부호 크기는 금지되어 있다고 말하지만 그렇지 않습니다. 당신은 이것을 다음과 같이 읽었습니다 : "연속적인 비트로 표현 된 값은 가산 적이며, 1로 시작하고, (가장 높은 위치를 가진 비트를 제외하고는 2의 연속적인 적분 제곱으로 곱해집니다)". 필자는 "가장 높은 위치에있는 비트를 제외하고 연속 비트 (가산 성, 1로 시작, 2의 연속 적분 거듭 제곱으로 곱한 값)로 표시되는 값"이라고 읽어야한다고 생각합니다. 즉, 높은 비트가 설정되면 모든 베팅이 해제됩니다.
Steve Jessop 2013

@SteveJessop : 귀하의 해석이 정확할 수 있습니다. 그렇다면 내 가설을 배제합니다 ... 그러나 그것은 또한 정말 방대한 가능성을 도입하여이 질문에 답하기 매우 어렵게 만듭니다. 이것은 실제로 사양의 버그처럼 보입니다. (분명히 C위원회는 그렇게 생각하고 C99에서 철저히 수정했습니다. 왜 C ++ 11이 그들의 접근 방식을 채택하지 않았는지 궁금합니다.)
Nemo

답변:


70

user71404의 답변 확장 :

int f(unsigned x)
{
    if (x <= INT_MAX)
        return static_cast<int>(x);

    if (x >= INT_MIN)
        return static_cast<int>(x - INT_MIN) + INT_MIN;

    throw x; // Or whatever else you like
}

경우 x >= INT_MIN(마음에 프로모션 규칙을 유지 INT_MIN로 변환됩니다 unsigned다음) x - INT_MIN <= INT_MAX이 오버 플로우가되지 않도록.

이것이 명확하지 않은 경우 "If x >= -4u, then x + 4 <= 3." 주장을 살펴보고 INT_MAX최소한 -INT_MIN-1의 수학적 값과 동일하다는 점을 명심 하십시오.

!(x <= INT_MAX)의미 하는 가장 일반적인 시스템 x >= INT_MIN에서 옵티마이 저는 두 번째 검사를 제거하고 두 return명령문을 동일한 코드로 컴파일 할 수 있는지 확인한 다음 첫 번째 검사도 제거 할 수 있어야합니다 (내 시스템에서는 가능) . 생성 된 어셈블리 목록 :

__Z1fj:
LFB6:
    .cfi_startproc
    movl    4(%esp), %eax
    ret
    .cfi_endproc

귀하의 질문에 대한 가상 구현 :

  • INT_MAX는 32767과 같습니다.
  • INT_MIN은 -2 32 + 32768과 같습니다.

불가능하므로 특별한 고려가 필요하지 않습니다. , 또는 INT_MIN과 같습니다 . 이는 C의 정수 유형 (6.2.6.2) 표현에서 따온 것입니다. 비트는 값 비트, 1 비트는 부호 비트, 단일 트랩 표현 만 허용합니다 (패딩 비트로 인해 유효하지 않은 표현은 포함하지 않음). 즉, 그렇지 않으면 음의 0을 나타내는 것 / . C ++는 C가 허용하는 것 이상의 정수 표현을 허용하지 않습니다.-INT_MAX-INT_MAX - 1n-INT_MAX - 1

업데이트 : 마이크로 소프트의 컴파일러는 분명히 통지가되지 않습니다x > 10x >= 11같은 일을 테스트합니다. 이(이 플랫폼에서)부정으로 감지 할 수있는로x >= INT_MIN대체 된경우에만 원하는 코드를 생성합니다.x > INT_MIN - 1ux <= INT_MAX

[질문자 (Nemo)의 업데이트, 아래 토론에 대해 자세히 설명]

나는 이제이 대답이 모든 경우에 효과가 있다고 믿지만 복잡한 이유로. 나는이 솔루션에 대한 현상금을 수여 할 것 같지만 누군가가 관심을 가질 경우를 대비하여 모든 세부 사항을 캡처하고 싶습니다.

C ++ 11, 섹션 18.3.3부터 시작하겠습니다.

표 31은 헤더를 설명합니다 <climits>.

...

내용은 표준 C 라이브러리 헤더와 동일합니다 <limits.h>.

여기서 "표준 C"는 C99를 의미하며, 사양은 부호있는 정수의 표현을 엄격하게 제한합니다. 그것들은 부호없는 정수와 같지만 1 비트는 "부호"전용이고 0 개 이상의 비트는 "패딩"전용입니다. 패딩 비트는 정수 값에 영향을주지 않으며 부호 비트는 2 보수, 1 보수 또는 부호 크기로만 기여합니다.

C ++ 11 <climits>은 C99 의 매크로를 상속하므로 INT_MIN은 -INT_MAX 또는 -INT_MAX-1이며 hvd의 코드는 작동하도록 보장됩니다. (패딩으로 인해 INT_MAX는 UINT_MAX / 2보다 훨씬 작을 수 있습니다 ...하지만 signed-> unsigned 캐스트가 작동하는 방식 덕분에이 답변은 잘 처리됩니다.)

C ++ 03 / C ++ 98은 더 까다 롭습니다. <climits>"Standard C"에서 상속하기 위해 동일한 단어를 사용 하지만 이제 "Standard C"는 C89 / C90을 의미합니다.

이 모든 것-C ++ 98, C ++ 03, C89 / C90-내 질문에 내가 제공하는 문구가 있지만 다음도 포함합니다 (C ++ 03 섹션 3.9.1 단락 7).

정수형 표현은 순수 이진법을 사용하여 값을 정의해야합니다. (44) [ :이 국제 표준은 정수형에 대해 2의 보수, 1의 보수 및 부호있는 크기 표현을 허용합니다.]

각주 (44)는 "순수 이진 숫자 시스템"을 정의합니다.

이진수 0과 1을 사용하는 정수에 대한 위치 표현으로, 연속 비트로 표시되는 값은 가산 적이며 1로 시작하며 2의 연속 적분 거듭 제곱으로 곱해집니다. 단, 가장 높은 위치의 비트는 예외입니다.

이 표현에 대해 흥미로운 점은 "순수 이진 수법"의 정의가 부호 / 크기 표현을 허용하지 않기 때문에 자체적으로 모순된다는 것입니다! 높은 비트가 값 -2 n-1 (2의 보수) 또는-(2 n-1 -1) (1의 보수) 를 가질 수 있습니다 . 그러나 부호 / 크기를 초래하는 높은 비트 값은 없습니다.

어쨌든 내 "가상 구현"은이 정의에서 "순수 바이너리"로 간주되지 않으므로 배제됩니다.

그러나 높은 비트가 특별하다는 사실은 작은 양의 값, 큰 양의 값, 작은 음의 값 또는 큰 음의 값과 같은 모든 값에 기여한다고 상상할 수 있음을 의미합니다. (부호 비트가-(2 n-1 -1)에 기여할 수 있다면 -(2 n-1 -2)가 아닌 이유는 무엇입니까?)

따라서 "부호"비트에 이상한 값을 할당하는 부호있는 정수 표현을 상상해 봅시다.

부호 비트에 대한 작은 양수 값은 int(아마도) 에 대한 양의 범위가 unsigned되며 hvd의 코드는 잘 처리합니다.

부호 비트에 대한 큰 양의 값 int은 최대 값이보다 큰 결과를 가져 unsigned오며 이는 금지됩니다.

부호 비트에 대한 큰 음수 값은 int연속되지 않은 범위의 값 을 나타내며 사양 규칙의 다른 문구가이를 나타냅니다.

마지막으로, 작은 음수에 기여하는 부호 비트는 어떻습니까? "부호 비트"의 1이 int 값에 -37을 기여할 수 있습니까? 그렇다면 INT_MAX는 (예를 들어) 2 31-1 이고 INT_MIN은 -37입니까?

이것은 어떤 숫자가 두 개의 표현을 갖는 결과를 낳을 것입니다 ... 그러나 1- 보완은 두 개의 표현을 0으로 제공하며 이는 "예제"에 따라 허용됩니다. 사양에서 0이 두 가지 표현을 가질 수있는 유일한 정수 라고 말하는 곳은 없습니다. 그래서 저는이 새로운 가설이 사양에 의해 허용되었다고 생각합니다.

사실, -1부터 아래까지의 음수 값 -INT_MAX-1은 "부호 비트"에 대한 값으로 허용되는 것처럼 보이지만 더 작은 것은 없습니다 (범위가 연속적이지 않음). 즉, -1 INT_MIN에서 무엇이든 될 수 있습니다 -INT_MAX-1.

자, 그거 알아? 구현 정의 동작을 피하기 위해 hvd의 코드에서 두 번째 캐스트의 x - (unsigned)INT_MIN경우 INT_MAX. 우리 INT_MIN는 최소한 -INT_MAX-1. 당연히 x최대 UINT_MAX. 음수를 unsigned로 캐스팅하는 것은를 추가하는 것과 같습니다 UINT_MAX+1. 종합 해보세요.

x - (unsigned)INT_MIN <= INT_MAX

경우에만

UINT_MAX - (INT_MIN + UINT_MAX + 1) <= INT_MAX
-INT_MIN-1 <= INT_MAX
-INT_MIN <= INT_MAX+1
INT_MIN >= -INT_MAX-1

마지막이 우리가 방금 보여준 것이므로이 비뚤어진 경우에도 코드는 실제로 작동합니다.

그것은 모든 가능성을 소진시켜이 극도로 학문적 인 연습을 끝냅니다.

결론 : C ++ 98 / C ++ 03에서 상속 된 C89 / C90의 부호있는 정수에 대해 심각하게 지정되지 않은 동작이 있습니다. C99에서 수정되었으며 C ++ 11 <limits.h>은 C99에서 통합하여 간접적으로 수정을 상속합니다 . 그러나 C ++ 11조차도 모순되는 "순수 이진 표현"이라는 표현을 유지합니다 ...


질문이 업데이트되었습니다. 나는 다른 사람들을 낙담시키기 위해 (당분간)이 답변에 반대표를 던집니다. 대답이 흥미 롭기 때문에 나중에 반대표를 해제하겠습니다. (C에는 맞지만 C ++에는 잘못된 것 같습니다.)
Nemo

@Nemo C 표준은이 경우 C ++에 적용됩니다. 최소한의 값 <limits.h>은 C 표준에서와 동일한 의미를 갖는 것으로 C ++ 표준에 정의되어 있으므로 INT_MININT_MAX에 대한 C의 모든 요구 사항 은 C ++에서 상속됩니다. C ++ 03이 C90을 참조하고 C90이 허용 된 정수 표현에 대해 모호하지만 C99 변경 (적어도 <limits.h>C ++ 11을 통해 상 속됨 , 바라건대 더 간단한 방법으로 제한됨)은 다음과 같이 제한됩니다. 이 세 가지는 기존 관행을 체계화 한 것입니다. 다른 구현은 존재하지 않았습니다.

나는 INT_MINetc. 의 의미가 C로부터 물려받은 것에 동의합니다 . 그러나 그것이 가치 가 있다는 것을 의미하지는 않습니다 . (실제로 모든 구현이 다르기 때문에 어떻게 할 수 있습니까?) INT_MIN1 이내 의 추론 은 -INT_MAXC ++ 사양에 나타나지 않는 단어 에 따라 다릅니다. 따라서 C ++는 매크로의 의미 론적 의미를 상속하지만 사양은 추론을 지원하는 단어를 제공 (또는 상속)하지 않습니다. 이것은 C ++ 사양에 대한 감독으로 인해 완전히 준수하는 효율적인 서명되지 않은 캐스트를 방지하는 것으로 보입니다.
Nemo 2011

@Nemo C ++가 다른 표현을 허용한다고 (아마도 올바르게) 주장한다면, 그런 구현에서 나는 그것이 type의 최소 표현 가능한 값일 필요 INT_MIN 는 없다고 주장합니다 int. 왜냐하면 C에 관한 한, 유형이 그렇지 않다면 의 요구 사항과 일치하면 intC 표준은 어떤 식 으로든 해당 구현을 다룰 수 없으며 C ++ 표준은 "C 표준이 말하는 것"이외의 정의를 제공하지 않습니다. 더 간단한 설명이 있는지 확인해 보겠습니다.

7
이것은 멋지다. 당시에이 질문을 어떻게 놓쳤는 지 모르겠습니다.
궤도에서 가벼운 경주

17

이 코드는 사양에서 요구하는 동작에만 의존하므로 요구 사항 (a)를 쉽게 충족 할 수 있습니다.

int unsigned_to_signed(unsigned n)
{
  int result = INT_MAX;

  if (n > INT_MAX && n < INT_MIN)
    throw runtime_error("no signed int for this number");

  for (unsigned i = INT_MAX; i != n; --i)
    --result;

  return result;
}

요구 사항 (b)는 그렇게 쉽지 않습니다. 이것은 gcc 4.6.3 (-Os, -O2, -O3) 및 clang 3.0 (-Os, -O, -O2, -O3)을 사용하여 no-op으로 컴파일됩니다. Intel 12.1.0은이를 최적화하지 않습니다. 그리고 Visual C에 대한 정보가 없습니다.


1
좋아요, 이건 굉장합니다. 나는 현상금을 80:20으로 나눌 수 있으면 좋겠다. 컴파일러의 추론은 다음과 같다. 루프가 종료되지 않으면 result오버플로; 정수 오버플로가 정의되지 않았습니다. 따라서 루프가 종료됩니다. 따라서 i == n종료시; 그러므로 result같습니다 n. 나는 여전히 hvd의 대답을 선호해야하지만 (덜 똑똑한 컴파일러의 비 병리학 적 행동에 대해) 더 많은 찬성 투표가 필요합니다.
Nemo

1
부호없는 것은 모듈로로 정의됩니다. n일부 unsigned 값이고 i결국 모든 unsigned 값에 도달해야 하기 때문에 루프도 종료되도록 보장됩니다 .
idupree

7

원래 답변은 unsigned=> 만 문제를 해결했습니다 int. "일부 부호없는 유형"의 일반적인 문제를 해당 서명 유형으로 해결하려면 어떻게해야합니까? 또한 원래 답변은 표준의 섹션을 인용하고 일부 코너 사례를 분석하는 데 탁월했지만 그것이 작동하는 이유를 이해하는 데 실제로 도움이되지 않았 으므로이 답변은 강력한 개념적 기반을 제공하려고 노력할 것입니다. 이 답변은 "이유"를 설명하고 최신 C ++ 기능을 사용하여 코드를 단순화하는 데 도움이됩니다.

C ++ 20 답변

문제는 P0907로 극적으로 단순화 되었습니다. 부호있는 정수는 2의 보완 이며 C ++ 20 표준에 투표 된 최종 문구 P1236 입니다. 이제 대답은 가능한 한 간단합니다.

template<std::unsigned_integral T>
constexpr auto cast_to_signed_integer(T const value) {
    return static_cast<std::make_signed_t<T>>(value);
}

그게 다야. A static_cast(또는 C 스타일 캐스트)는 마침내이 질문에 필요한 작업을 수행하도록 보장되며 많은 프로그래머가 항상 그랬다고 생각한 작업을 수행합니다.

C ++ 17 답변

C ++ 17에서는 상황이 훨씬 더 복잡합니다. 세 가지 가능한 정수 표현 (2의 보수, 1의 보수, 부호 크기)을 처리해야합니다. 가능한 값의 범위를 확인했기 때문에 2의 보수 여야한다는 것을 알고있는 경우에도 부호있는 정수 범위를 벗어난 값을 부호있는 정수로 변환하면 구현 정의 결과가 제공됩니다. 우리는 다른 답변에서 본 것과 같은 트릭을 사용해야합니다.

먼저, 일반적으로 문제를 해결하는 방법에 대한 코드는 다음과 같습니다.

template<typename T, typename = std::enable_if_t<std::is_unsigned_v<T>>>
constexpr auto cast_to_signed_integer(T const value) {
    using result = std::make_signed_t<T>;
    using result_limits = std::numeric_limits<result>;
    if constexpr (result_limits::min() + 1 != -result_limits::max()) {
        if (value == static_cast<T>(result_limits::max()) + 1) {
            throw std::runtime_error("Cannot convert the maximum possible unsigned to a signed value on this system");
        }
    }
    if (value <= result_limits::max()) {
        return static_cast<result>(value);
    } else {
        using promoted_unsigned = std::conditional_t<sizeof(T) <= sizeof(unsigned), unsigned, T>;
        using promoted_signed = std::make_signed_t<promoted_unsigned>;
        constexpr auto shift_by_window = [](auto x) {
            // static_cast to avoid conversion warning
            return x - static_cast<decltype(x)>(result_limits::max()) - 1;
        };
        return static_cast<result>(
            shift_by_window( // shift values from common range to negative range
                static_cast<promoted_signed>(
                    shift_by_window( // shift large values into common range
                        static_cast<promoted_unsigned>(value) // cast to avoid promotion to int
                    )
                )
            )
        );
    }
}

여기에는 허용 된 답변보다 몇 가지 더 많은 캐스트가 있으며, 이는 컴파일러에서 서명 된 / 서명되지 않은 불일치 경고가 없는지 확인하고 정수 승격 규칙을 적절하게 처리하기위한 것입니다.

먼저 2의 보수가 아닌 시스템에 대한 특별한 경우가 있습니다 (따라서 매핑 할 항목이 없기 때문에 가능한 최대 값을 특별히 처리해야합니다). 그 후 실제 알고리즘에 도달합니다.

두 번째 최상위 조건은 간단합니다. 값이 최대 값보다 작거나 같음을 알고 있으므로 결과 유형에 적합합니다. 세 번째 조건은 주석이 있어도 조금 더 복잡하므로 몇 가지 예는 각 문장이 필요한 이유를 이해하는 데 도움이 될 것입니다.

개념적 기초 : 수직선

첫째,이 window개념 은 무엇 입니까? 다음 번호 선을 고려하십시오.

   |   signed   |
<.........................>
          |  unsigned  |

2의 보수 정수의 경우 두 유형 중 하나에 도달 할 수있는 수직선의 하위 집합을 동일한 크기의 세 범주로 나눌 수 있습니다.

- => signed only
= => both
+ => unsigned only

<..-------=======+++++++..>

이것은 표현을 고려하여 쉽게 증명할 수 있습니다. 부호없는 정수의 시작 0과 모든 비트의 사용 가치가 부호 비트를 제외하고 정확하게 모든 비트에 대해 동일 2. 부호있는 정수의 거듭 제곱의 값을 증가 -(2^position)대신 2^position. 이는 모든 n - 1비트에 대해 동일한 값을 나타냄 을 의미 합니다. 그런 다음 부호없는 정수는 하나 이상의 일반 비트를 가지며, 이는 총 값 수를 두 배로 늘립니다 (즉, 해당 비트가 설정되지 않은 값만큼 많은 값이 설정 됨). 해당 비트가 설정된 모든 값이 음수라는 점을 제외하면 부호있는 정수에 대해서도 동일한 논리가 적용됩니다.

다른 두 개의 합법적 인 정수 표현 인 1의 보수와 부호 크기는 1을 제외하고는 2의 보수 정수와 동일한 값을 갖습니다 : 가장 음의 값입니다. C ++에서 정의 제외한 정수 유형에 대한 모든 것 reinterpret_cast(및 C ++ 20 std::bit_cast) 표현할 수있는 값의 범위의 관점에서가 아니라 비트 표현의 용어이다. 이것은 우리가 트랩 표현을 만들려고 시도하지 않는 한 분석이이 세 가지 표현 각각에 대해 유지된다는 것을 의미합니다. 이 누락 된 값에 매핑되는 부호없는 값은 다소 불행한 것입니다. 부호없는 값의 중간에있는 값입니다. 다행스럽게도 첫 번째 조건은 그러한 표현이 존재하는지 (컴파일 시간에) 확인한 다음 특별히 런타임 확인으로 처리합니다.

첫 번째 조건은 =섹션 에있는 경우를 처리합니다. 즉, 하나의 값이 변경없이 다른 값으로 표시 될 수있는 겹치는 영역에 있습니다. shift_by_window코드 의 함수는 이러한 각 세그먼트의 크기만큼 모든 값을 아래로 이동합니다 (산술 오버플로 문제를 피하기 위해 최대 값을 뺀 다음 1을 뺍니다). 우리가 다시 고유 한 매핑을 갖도록 해당 지역 (지역에 있음) 밖에있는 경우 .+ ), 우리는 한 창 크기만큼 아래로 점프해야합니다. 이것은 우리를 겹치는 범위에있게하는데, 이는 값에 변화가 없기 때문에 우리가 부호없는 것을 부호있는 것으로 안전하게 변환 할 수 있음을 의미합니다. 그러나 두 개의 부호없는 값을 각 부호있는 값에 매핑했기 때문에 아직 완료되지 않았습니다. 따라서 다음 창으로 이동해야합니다 (-

이제 UINT_MAX + 1질문에서 요청한대로 결과 일치하는 mod를 제공 합니까? UINT_MAX + 1는와 동일합니다 2^n. 여기서은 n값 표현의 비트 수입니다. 창 크기에 사용하는 값은 다음과 같습니다 2^(n - 1)(값 시퀀스의 최종 인덱스는 크기보다 하나 작음). 우리는 우리가 빼기 의미, 두 배 값을 빼기 2 * 2^(n - 1)동일하다 2^n. 덧셈과 뺄셈 x은 산술 mod x에서 작동하지 않으므로 원래 값 mod에 영향을주지 않았습니다 2^n.

정수 승격을 올바르게 처리

이 일반적인 기능이 아니기 때문에 단지 intunsigned, 우리는 또한 통합 프로모션 규칙 우려 자신을해야합니다. 두 가지 가능성이 흥미로운 경우가있다 : 하나에서이 short보다 작 int과 하나가 short같은 크기입니다 int.

예 : short보다 작음int

경우 short보다 작은 int(현대 플랫폼에서 공통) 우리는 또한 알고 unsigned short에 들어갈 수있는 int수단이에 어떤 작업이 실제로 일어날 것이다, 이는 int우리가 명시 적으로이를 방지하기 위해 승진 형식으로 캐스팅, 그래서. 우리의 최종 진술은 매우 추상적이며 실제 가치로 대체하면 이해하기 쉬워집니다. 첫 번째 흥미로운 경우, 일반성을 잃지 않고 16 비트 short와 17 비트를 고려해 보겠습니다 int(새로운 규칙에서 여전히 허용되며,이 두 정수 유형 중 적어도 하나에 패딩 비트가 있음을 의미합니다). ) :

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return static_cast<int16_t>(
    shift_by_window(
        static_cast<int17_t>(
            shift_by_window(
                static_cast<uint17_t>(value)
            )
        )
    )
);

가능한 가장 큰 16 비트 부호없는 값 해결

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return int16_t(
    shift_by_window(
        int17_t(
            shift_by_window(
                uint17_t(65535)
            )
        )
    )
);

단순화

return int16_t(
    int17_t(
        uint17_t(65535) - uint17_t(32767) - 1
    ) -
    int17_t(32767) -
    1
);

단순화

return int16_t(
    int17_t(uint17_t(32767)) -
    int17_t(32767) -
    1
);

단순화

return int16_t(
    int17_t(32767) -
    int17_t(32767) -
    1
);

단순화

return int16_t(-1);

우리는 가능한 한 가장 큰 서명되지 않은 것을 넣고 -1성공했습니다!

예 : short같은 크기int

short크기가 같은 경우 int(최신 플랫폼에서는 일반적이지 않음) 통합 프로모션 규칙이 약간 다릅니다. 이 경우로 short승격 int하고로 unsigned short승격합니다 unsigned. 다행히도 각 결과를 계산을 수행하려는 유형에 명시 적으로 캐스팅하므로 문제가있는 프로모션이 발생하지 않습니다. 일반성을 잃지 않고 16 비트 short와 16 비트를 고려해 보겠습니다 int.

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return static_cast<int16_t>(
    shift_by_window(
        static_cast<int16_t>(
            shift_by_window(
                static_cast<uint16_t>(value)
            )
        )
    )
);

가능한 가장 큰 16 비트 부호없는 값 해결

auto x = int16_t(
    uint16_t(65535) - uint16_t(32767) - 1
);
return int16_t(
    x - int16_t(32767) - 1
);

단순화

return int16_t(
    int16_t(32767) - int16_t(32767) - 1
);

단순화

return int16_t(-1);

우리는 가능한 한 가장 큰 서명되지 않은 것을 넣고 -1성공했습니다!

난 그냥 무엇을 걱정하는 경우 intunsigned원래의 질문처럼, 경고에 대한 상관 없어?

constexpr int cast_to_signed_integer(unsigned const value) {
    using result_limits = std::numeric_limits<int>;
    if constexpr (result_limits::min() + 1 != -result_limits::max()) {
        if (value == static_cast<unsigned>(result_limits::max()) + 1) {
            throw std::runtime_error("Cannot convert the maximum possible unsigned to a signed value on this system");
        }
    }
    if (value <= result_limits::max()) {
        return static_cast<int>(value);
    } else {
        constexpr int window = result_limits::min();
        return static_cast<int>(value + window) + window;
    }
}

라이브보기

https://godbolt.org/z/74hY81

여기서 clang, gcc 및 icc는 castcast_to_signed_integer_basicat -O2및에 대한 코드를 -O3생성하지 않으며 MSVC는에서 코드를 생성하지 /O2않으므로 솔루션이 최적입니다.


3

수행 할 작업을 컴파일러에 명시 적으로 지정할 수 있습니다.

int unsigned_to_signed(unsigned n) {
  if (n > INT_MAX) {
    if (n <= UINT_MAX + INT_MIN) {
      throw "no result";
    }
    return static_cast<int>(n + INT_MIN) - (UINT_MAX + INT_MIN + 1);
  } else {
    return static_cast<int>(n);
  }
}

와 컴파일 gcc 4.7.2에 대한 x86_64-linux( g++ -O -S test.cpp)에

_Z18unsigned_to_signedj:
    movl    %edi, %eax
    ret

UINT_MAX은 유형의 표현 이며 그 유형 unsigned int의 전체를 만듭니다 static_cast<int>(n + INT_MIN) - (UINT_MAX + INT_MIN + 1). 그래도 수정이 가능해야하며, 그래도 여전히 똑같이 컴파일 될 것으로 예상합니다.

2

x우리의 입력 이라면 ...

만약 x > INT_MAX, 우리는 다음 k과 같은 상수를 찾고자 합니다.0 < x - k*INT_MAX<을 INT_MAX.

이것은 쉽습니다- unsigned int k = x / INT_MAX;. 그런 다음unsigned int x2 = x - k*INT_MAX;

이제 캐스팅 할 수 있습니다. x2int 안전하게 할 수 있습니다. 허락하다int x3 = static_cast<int>(x2);

이제 다음과 같은 것을 빼고 싶습니다 UINT_MAX - k * INT_MAX + 1.x3 , ifk > 0 .

이제 2 초 보완 시스템에서 x > INT_MAX,이 작업 은 다음 과 같이 작동합니다.

unsigned int k = x / INT_MAX;
x -= k*INT_MAX;
int r = int(x);
r += k*INT_MAX;
r -= UINT_MAX+1;

참고 UINT_MAX+1 C ++에서 제로입니다 int로 변환이 무 조작했다, 보장, 우리는 공제k*INT_MAX 다음 "같은 값"에 다시 추가. 따라서 허용 가능한 최적화 프로그램은 모든 조작을 지울 수 있어야합니다!

그것은 문제를 남깁니다 x > INT_MAX. 음, 우리는 2 개의 브랜치를 만듭니다.x > INT_MAX . 없는 것은 컴파일러가 noop로 최적화하는 strait cast를 수행합니다. ...가있는 사람은 옵티마이 저가 완료된 후 noop를 수행합니다. 스마트 옵티마이 저는 두 분기를 동일한 것으로 인식하고 분기를 삭제합니다.

문제 :이에 UINT_MAX비해 실제로 크면 INT_MAX위의 방법이 작동하지 않을 수 있습니다. 나는 그것을 가정하고있다k*INT_MAX <= UINT_MAX+1 암묵적으로 있습니다.

우리는 아마도 다음과 같은 열거 형으로 이것을 공격 할 수 있습니다 :

enum { divisor = UINT_MAX/INT_MAX, remainder = UINT_MAX-divisor*INT_MAX };

내가 믿는 2의 보수 시스템에서 2와 1로 작동하며 (수학이 작동하도록 보장됩니까? 까다로운 일입니다 ...) 2가 아닌 보수 시스템에서 쉽게 최적화되는 이들을 기반으로 논리를 수행합니다.

이것은 또한 예외 사례를 엽니 다. UINT_MAX가 (INT_MIN-INT_MAX)보다 훨씬 큰 경우에만 가능하므로 어떻게 든 정확히 그 질문을 묻는 if 블록에 예외 코드를 넣을 수 있으며 기존 시스템에서 속도가 느려지지 않습니다.

이를 올바르게 처리하기 위해 컴파일 타임 상수를 구성하는 방법을 정확히 모르겠습니다.


UINT_MAXINT_MAX사양은 모든 양의 부호있는 int를 부호없는 int로 표현할 수 있도록 보장하기 때문에에 비해 작을 수 없습니다 . 그러나 UINT_MAX+1모든 시스템에서 0입니다. 부호없는 산술은 항상 모듈로 UINT_MAX+1입니다. 아직 ... 여기에 가능한 접근 방식의 커널이있을 수 있습니다
니모

@Nemo이 스레드를 따라 가기 만하면 내 잠재적 인 질문을 용서해주십시오. UINT_MAX+1'03 -spec에서 확립 된 " 모든 시스템에서 " 이 (가) 0입니까? "라고 말합니까? 그렇다면 특정 하위 섹션을 살펴 봐야합니까? 감사합니다.
WhozCraig

@WhozCraig : 섹션 3.9.1 단락 4 : "부호없는 것으로 선언 된 부호없는 정수는 산술 모듈로 2 ^ n의 법칙을 준수해야합니다. 여기서 n은 특정 정수 크기의 값 표현에있는 비트 수입니다."라는 각주와 함께 "이것은 결과적으로 부호없는 정수 유형으로 표시 할 수없는 결과가 결과 부호없는 정수 유형으로 표시 될 수있는 가장 큰 값보다 하나 더 큰 숫자의 모듈로로 축소되기 때문에 부호없는 산술이 오버 플로우되지 않음을 의미합니다." 기본적으로 unsigned는 원하는 / 기대하는 방식으로 작동하도록 지정됩니다.
니모

@Nemo 감사합니다. 대단히 감사합니다.
WhozCraig 2010 년

1

std::numeric_limits<int>::is_modulo컴파일 시간 상수입니다. 템플릿 전문화에 사용할 수 있습니다. 적어도 컴파일러가 인라인과 함께 작동하면 문제가 해결되었습니다.

#include <limits>
#include <stdexcept>
#include <string>

#ifdef TESTING_SF
    bool const testing_sf = true;
#else
    bool const testing_sf = false;
#endif

// C++ "extensions"
namespace cppx {
    using std::runtime_error;
    using std::string;

    inline bool hopefully( bool const c ) { return c; }
    inline bool throw_x( string const& s ) { throw runtime_error( s ); }

}  // namespace cppx

// C++ "portability perversions"
namespace cppp {
    using cppx::hopefully;
    using cppx::throw_x;
    using std::numeric_limits;

    namespace detail {
        template< bool isTwosComplement >
        int signed_from( unsigned const n )
        {
            if( n <= unsigned( numeric_limits<int>::max() ) )
            {
                return static_cast<int>( n );
            }

            unsigned const u_max = unsigned( -1 );
            unsigned const u_half = u_max/2 + 1;

            if( n == u_half )
            {
                throw_x( "signed_from: unsupported value (negative max)" );
            }

            int const i_quarter = static_cast<int>( u_half/2 );
            int const int_n1 = static_cast<int>( n - u_half );
            int const int_n2 = int_n1 - i_quarter;
            int const int_n3 = int_n2 - i_quarter;

            hopefully( n == static_cast<unsigned>( int_n3 ) )
                || throw_x( "signed_from: range error" );

            return int_n3;
        }

        template<>
        inline int signed_from<true>( unsigned const n )
        {
            return static_cast<int>( n );
        }
    }    // namespace detail

    inline int signed_from( unsigned const n )
    {
        bool const is_modulo = numeric_limits< int >::is_modulo;
        return detail::signed_from< is_modulo && !testing_sf >( n );
    }
}    // namespace cppp

#include <iostream>
using namespace std;
int main()
{
    int const x = cppp::signed_from( -42u );
    wcout << x << endl;
}


편집 : 모듈식이 아닌 컴퓨터에서 가능한 트랩을 피하기 위해 코드를 수정했습니다 (유니시스 Clearpath의 구식으로 구성된 버전 만 존재하는 것으로 알려져 있음). 단순화를 위해 이것은 -2 n -1 값을 지원하지 않음으로써 수행됩니다. 여기서 n 은 해당 int머신 (즉, Clearpath에서)에서 값 비트 의 수입니다 . 실제로이 값은 기계에서 지원되지 않습니다 (예 : 부호 및 크기 또는 1의 보수 표현).


1

int 유형이 2 바이트 이상이라고 생각하므로 INT_MIN 및 INT_MAX는 다른 플랫폼에서 변경 될 수 있습니다.

기본 유형

≤climits≥ 헤더


기본적으로 "-mint8"로 구성된 6809 용 컴파일러를 사용하는 것이 저주를 받았습니다. 여기서 int는 8 비트입니다.-((이것은 Vectrex의 개발 환경입니다) long은 2 바이트, long long은 4 바이트이며 짧은 것이 무엇인지 모르겠습니다 ...
Graham Toal

1

내 돈은 memcpy를 사용하고 있습니다. 괜찮은 컴파일러는이를 최적화하는 방법을 알고 있습니다.

#include <stdio.h>
#include <memory.h>
#include <limits.h>

static inline int unsigned_to_signed(unsigned n)
{
    int result;
    memcpy( &result, &n, sizeof(result));
    return result;
}

int main(int argc, const char * argv[])
{
    unsigned int x = UINT_MAX - 1;
    int xx = unsigned_to_signed(x);
    return xx;
}

나를 위해 (Xcode 8.3.2, Apple LLVM 8.1, -O3) 다음을 생성합니다.

_main:                                  ## @main
Lfunc_begin0:
    .loc    1 21 0                  ## /Users/Someone/main.c:21:0
    .cfi_startproc
## BB#0:
    pushq    %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    ##DEBUG_VALUE: main:argc <- %EDI
    ##DEBUG_VALUE: main:argv <- %RSI
Ltmp3:
    ##DEBUG_VALUE: main:x <- 2147483646
    ##DEBUG_VALUE: main:xx <- 2147483646
    .loc    1 24 5 prologue_end     ## /Users/Someone/main.c:24:5
    movl    $-2, %eax
    popq    %rbp
    retq
Ltmp4:
Lfunc_end0:
    .cfi_endproc

1
부호의 이진 표현이됩니다 이것은, 질문에 대답하지 않습니다 하지 서명 된 표현을 일치하도록 표준이 보장하는.
TLW
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.