Development Tip

signed int가 아닌 unsigned를 사용하면 버그가 발생할 가능성이 더 큽니까?

yourdevel 2020. 10. 16. 08:11
반응형

signed int가 아닌 unsigned를 사용하면 버그가 발생할 가능성이 더 큽니까? 왜?


에서 구글 C ++ 스타일 가이드 "부호없는 정수"의 주제에, 그것은하는 것이 좋습니다

역사적 사고로 인해 C ++ 표준은 컨테이너의 크기를 나타 내기 위해 부호없는 정수도 사용합니다. 표준기구의 많은 구성원은 이것이 실수라고 생각하지만이 시점에서 수정하는 것은 사실상 불가능합니다. 부호없는 산술은 단순한 정수의 동작을 모델링하지 않고 대신 모듈 식 산술 (오버플로 / 언더 플로에서 래핑)을 모델링하기 위해 표준에 의해 정의된다는 사실은 컴파일러에서 중요한 버그 클래스를 진단 할 수 없음을 의미합니다.

모듈 식 산술의 문제점은 무엇입니까? unsigned int의 예상되는 동작이 아닙니까?

이 가이드에서는 어떤 종류의 버그 (중요한 클래스)를 언급합니까? 범람하는 버그?

부호없는 유형을 사용하여 변수가 음수가 아니라고 단언하지 마십시오.

내가 unsigned int 대신 signed int를 사용하는 것을 생각할 수있는 한 가지 이유는 그것이 오버플로 (음수로)되면 감지하기가 더 쉽다는 것입니다.


답변 중 일부는 여기에 서명 부호없는 값 사이의 놀라운 프로모션 규칙을 언급하지만,이 더에 관한 문제처럼 보인다 혼합 서명과 서명되지 않은 값을, 왜 반드시 설명하지 않습니다 서명 선호되는 부호 시나리오를 혼합, 외부.

내 경험상, 혼합 된 비교와 승진 규칙 외에 부호없는 값이 큰 버그 자석 인 두 가지 주요 이유가 있습니다.

부호없는 값은 프로그래밍에서 가장 일반적인 값인 0에서 불연속성을 갖습니다.

부호없는 정수와 부호있는 정수는 모두 최소값과 최대 값에서 불연속성 을 가지며, 여기에서 줄 바꿈 (부호 없음)하거나 정의되지 않은 동작 (부호 있음)을 유발합니다. 들어 unsigned이러한 점에있다 제로UINT_MAX. 들어 int가에있다 INT_MIN하고 INT_MAX. 전형적인 값 INT_MININT_MAX4 바이트로 시스템 int값은 다음 -2^312^31-1, 이러한 시스템에서 UINT_MAX일반적이다 2^32-1.

unsigned적용되지 않는 주요 버그 유발 문제 는 0에서 불연속성int 이 있다는 것 입니다. 물론 0은 1,2,3과 같은 다른 작은 값과 함께 프로그램에서 매우 일반적인 값입니다. 다양한 구조에서 작은 값, 특히 1을 더하고 빼는 것이 일반적이며, 값에서 어떤 것을 빼고 0이된다면 엄청난 양의 값과 거의 확실한 버그를 얻게됩니다.unsigned

마지막 0.5를 제외하고 벡터의 모든 값을 인덱스로 반복하는 코드를 고려하십시오 .

for (size_t i = 0; i < v.size() - 1; i++) { // do something }

이것은 언젠가 빈 벡터를 전달할 때까지 잘 작동합니다. 0 회 반복을 수행하는 대신 v.size() - 1 == a giant number1 을 얻고 40 억 반복을 수행하고 거의 버퍼 오버 플로우 취약점을 갖게됩니다.

다음과 같이 작성해야합니다.

for (size_t i = 0; i + 1 < v.size(); i++) { // do something }

따라서이 경우 "고정"될 수 있지만의 서명되지 않은 특성에 대해 신중하게 생각해야만합니다 size_t. 때로는 상수 대신 적용하려는 변수 오프셋이 있기 때문에 위의 수정을 적용 할 수없는 경우도 있습니다. 이는 양수 또는 음수 일 수 있습니다. 따라서 비교해야하는 "측"은 부호에 따라 달라집니다. -이제 코드가 정말 지저분 해집니다.

0까지 반복하는 코드에도 비슷한 문제가 있습니다. 같은 것은 while (index-- > 0)잘 작동하지만 분명히 동등한 while (--index >= 0)것은 서명되지 않은 값으로 종료되지 않습니다. 컴파일러는 오른쪽이 리터럴 0 일 때 경고 할 수 있지만 런타임에 결정된 값이면 확실히 그렇지 않습니다.

대위법

어떤 사람들은 부호있는 값에도 두 개의 불연속성이 있다고 주장 할 수 있는데, 왜 unsigned를 선택합니까? 차이점은 두 불연속 모두 0에서 매우 (최대) 멀리 떨어져 있다는 것입니다. 나는 이것을 "오버플로"라는 별도의 문제라고 생각합니다. 부호있는 값과 부호없는 값은 모두 매우 큰 값에서 오버플로 될 수 있습니다. 많은 경우 값의 가능한 범위에 대한 제약으로 인해 오버플로가 불가능하며 많은 64 비트 값의 오버플로가 물리적으로 불가능할 수 있습니다. 가능하더라도 오버플로 관련 버그의 가능성은 "0에서"버그에 비해 종종 미미 하며 부호없는 값에 대해서도 오버플로가 발생합니다 . 따라서 unsigned는 두 세계의 최악을 결합합니다. 잠재적으로 매우 큰 크기 값으로 오버플로되고 0에서 불연속됩니다. 서명은 전자 만 있습니다.

많은 사람들이 서명하지 않은 상태에서 "당신은 조금 잃는다"고 주장 할 것입니다. 이것은 종종 사실이지만 항상 그런 것은 아닙니다 (부호없는 값 사이의 차이를 나타내야하는 경우 어쨌든 해당 비트를 잃게됩니다. 너무 많은 32 비트 항목이 어쨌든 2GiB로 제한되거나 이상한 회색 영역이 표시됩니다. 파일은 4GiB 일 수 있지만 두 번째 2GiB에서는 특정 API를 사용할 수 없습니다.

unsigned가 당신을 조금 사주는 경우에도 : 그것은 당신을 많이 사주지 않습니다. 만약 당신이 20 억 개 이상의 "사물"을 지원해야한다면, 아마 곧 40 억 개 이상을 지원해야 할 것입니다.

논리적으로 부호없는 값은 부호있는 값의 하위 집합입니다.

수학적으로 부호없는 값 (음이 아닌 정수)은 부호있는 정수의 하위 집합입니다 (단지 _integers라고 함). 2 . 그러나 부호있는은 빼기와 같이 부호없는에서만 연산에서 자연스럽게 튀어 나옵니다 . 부호없는 값은 빼기로 닫히지 않는다고 말할 수 있습니다 . 부호있는 값의 경우도 마찬가지입니다.

파일에있는 두 개의 서명되지 않은 인덱스 사이의 "델타"를 찾고 싶습니까? 뺄셈을 올바른 순서로하는 것이 좋습니다. 그렇지 않으면 잘못된 답을 얻을 수 있습니다. 물론 올바른 순서를 결정하기 위해 종종 런타임 검사가 필요합니다! 부호없는 값을 숫자로 처리 할 때 (논리적으로) 부호있는 값이 계속 표시되는 경우가 많으므로 부호있는 값으로 시작하는 것이 좋습니다.

대위법

위의 각주 (2)에서 언급했듯이 C ++의 부호있는 값은 실제로 동일한 크기의 부호없는 값의 하위 집합이 아니므로 부호없는 값은 부호있는 값과 동일한 수의 결과를 나타낼 수 있습니다.

사실이지만 범위가 덜 유용합니다. 뺄셈과 0 ~ 2N 범위의 부호없는 숫자와 -N ~ N 범위의 부호있는 숫자를 고려합니다. 임의의 뺄셈은 _ 두 경우 모두 -2N ~ 2N 범위의 결과를 가져 오며 두 유형의 정수 모두 다음을 나타낼 수 있습니다. 그것의 절반. -N에서 N까지의 0을 중심으로하는 영역은 일반적으로 0에서 2N 범위보다 훨씬 더 유용합니다 (실제 코드에서 더 많은 실제 결과를 포함 함). 균일 (log, zipfian, normal 등) 이외의 일반적인 분포를 고려하고 해당 분포에서 무작위로 선택된 값을 빼는 것을 고려하십시오. [0, 2N]보다 더 많은 값이 [-N, N]으로 끝나는 방식 (실제로 결과 분포) 항상 0에 중심).

64 비트는 부호있는 값을 숫자로 사용하는 많은 이유에서 문을 닫습니다.

나는 인수가 위에서 이미 32 비트 값에 대한 강력한라고 생각하지만, 모두 서명하고 서로 다른 임계 값에 부호없는 영향을 미치는 오버 플로우의 경우는 않는다 "20 억은"많은 초과 할 수있는 숫자이기 때문에, 32 비트 값을 발생 추상 및 물리적 수량 (수십억 달러, 수십억 나노초, 수십억 개의 요소가있는 어레이). 따라서 누군가가 unsigned 값에 대해 양의 범위를 두 배로 늘려서 충분히 확신한다면 오버플로가 중요하고 unsigned를 약간 선호하는 케이스를 만들 수 있습니다.

특수 도메인 외부에서 64 비트 값은이 문제를 크게 제거합니다. 10 개 이상 - 64 비트 서명 된 값 9,223,372,036,854,775,807의 상부 범위가 quintillion를 . 그것은 많은 나노초 (약 292 년 가치)와 많은 돈입니다. 또한 오랜 시간 동안 일관된 주소 공간에 RAM이있는 컴퓨터보다 더 큰 어레이입니다. 그렇다면 아마도 9 천경이면 모두에게 충분할까요 (현재로서는)?

부호없는 값을 사용하는 경우

스타일 가이드는 부호없는 숫자의 사용을 금지하거나 반드시 권장하지 않습니다. 결론은 다음과 같습니다.

부호없는 유형을 사용하여 변수가 음수가 아니라고 단언하지 마십시오.

실제로 부호없는 변수에 대한 좋은 용도가 있습니다.

  • N- 비트 수량을 정수가 아니라 단순히 "비트 모음"으로 처리하려는 경우. 예를 들어, 비트 마스크 나 비트 맵, N 부울 값 등으로. 이 사용은 종종 변수의 정확한 크기를 알고 싶어하기 때문에 uint32_t같은 고정 너비 유형과 함께 사용됩니다 uint64_t. 특정 변수가이 치료를받을 권리가 있다는 힌트는 단지와 함께에서 작동한다는 것입니다 비트 와 같은 통신 사업자 ~, |, &, ^, >>등, 그리고 같은 산술 연산과 +, -, *, /

    비트 연산자의 동작이 잘 정의되고 표준화되어 있기 때문에 Unsigned가 이상적입니다. 부호있는 값에는 이동시 정의되지 않은 동작과 지정되지 않은 동작, 지정되지 않은 표현과 같은 몇 가지 문제가 있습니다.

  • 실제로 모듈 식 산술을 원할 때. 때로는 실제로 2 ^ N 모듈 식 산술을 원합니다. 이러한 경우 "오버플로"는 버그가 아니라 기능입니다. 부호없는 값은 모듈 식 산술을 사용하도록 정의되었으므로 여기에서 원하는 것을 제공합니다. 부호있는 값은 지정되지 않은 표현이 있고 오버플로가 정의되지 않았기 때문에 (쉽고 효율적으로) 전혀 사용할 수 없습니다.

0.5 내가 이것을 쓴 후에 나는 이것이 내가 보지 못했던 Jarod의 예거의 동일하다는 것을 깨달았습니다. 그리고 좋은 이유가 있습니다.

1 우리는 size_t여기서 이야기하고 있으므로 일반적으로 32 비트 시스템에서는 2 ^ 32-1, 64 비트 시스템에서는 2 ^ 64-1입니다.

2 C ++에서는 부호없는 값이 해당 부호있는 유형보다 상단에 더 많은 값을 포함하기 때문에 정확히 그렇지는 않지만 부호없는 값을 조작하면 (논리적으로) 부호있는 값이 생성 될 수 있다는 기본적인 문제가 있지만 해당 문제는 없습니다. 부호있는 값 포함 (부호있는 값에는 이미 부호없는 값이 포함되어 있으므로)


언급 한 바와 같이, 혼합 unsigned및 것은 signed(잘 정의 된 경우에도) 예상치 못한 동작이 발생할 수 있습니다.

마지막 5 개를 제외한 벡터의 모든 요소를 ​​반복한다고 가정하면 다음과 같이 잘못 작성할 수 있습니다.

for (int i = 0; i < v.size() - 5; ++i) { foo(v[i]); } // Incorrect
// for (int i = 0; i + 5 < v.size(); ++i) { foo(v[i]); } // Correct

가정 v.size() < 5으로, 다음, v.size()이다 unsigned, s.size() - 5매우 많은 수의 것, 그래서 i < v.size() - 5true의 가치를 더 예상 범위 i. 그리고 UB는 빠르게 발생합니다 (한 번 바운드 액세스 외 i >= v.size())

v.size()부호있는 값을 반환 하면 s.size() - 5음수가되고 위의 경우 조건은 즉시 거짓이됩니다.

반면에 인덱스는 사이에 [0; v.size()[있어야 unsigned하므로 의미가 있습니다. Signed는 또한 음수 부호의 오른쪽 시프트에 대한 오버플로 또는 구현 정의 동작이있는 UB와 같은 자체 문제가 있지만 반복에 대한 버그의 원인은 적습니다.


가장 큰 오류의 예 중 하나는 부호있는 값과 부호없는 값을 혼합하는 경우입니다.

#include <iostream>
int main()  {
    auto qualifier = -1 < 1u ? "makes" : "does not make";
    std::cout << "The world " << qualifier << " sense" << std::endl;
}

출력 :

세상이 말이 안돼

Unless you have a trivial application, it's inevitable you'll end up with either dangerous mixes between signed and unsigned values (resulting in runtime errors) or if you crank up warnings and make them compile-time errors, you end up with a lot of static_casts in your code. That's why it's best to strictly use signed integers for types for math or logical comparison. Only use unsigned for bitmasks and types representing bits.

Modeling a type to be unsigned based on the expected domain of the values of your numbers is a Bad Idea. Most numbers are closer to 0 than they are to 2 billion, so with unsigned types, a lot of your values are closer to the edge of the valid range. To make things worse, the final value may be in a known positive range, but while evaluating expressions, intermediate values may underflow and if they are used in intermediate form may be VERY wrong values. Finally, even if your values are expected to always be positive, that doesn't mean that they won't interact with other variables that can be negative, and so you end up with a forced situation of mixing signed and unsigned types, which is the worst place to be.


Why is using an unsigned int more likely to cause bugs than using a signed int?

Using an unsigned type is not more likely to cause bugs than using a signed type with certain classes of tasks.

Use the right tool for the job.

What is wrong with modular arithmetic? Isn't that the expected behaviour of an unsigned int?
Why is using an unsigned int more likely to cause bugs than using a signed int?

If the task if well-matched: nothing wrong. No, not more likely.

Security, encryption, and authentication algorithm count on unsigned modular math.

Compression/decompression algorithms too as well as various graphic formats benefit and are less buggy with unsigned math.

Any time bit-wise operators and shifts are used, the unsigned operations do not get messed up with the sign-extension issues of signed math.


Signed integer math has an intuitive look and feel readily understood by all including learners to coding. C/C++ was not targeted originally nor now should be an intro-language. For rapid coding that employs safety nets concerning overflow, other languages are better suited. For lean fast code, C assumes that coders knows what they are doing (they are experienced).

A pitfall of signed math today is the ubiquitous 32-bit int that with so many problems is well wide enough for the common tasks without range checking. This leads to complacency that overflow is not coded against. Instead, for (int i=0; i < n; i++) int len = strlen(s); is viewed as OK because n is assumed < INT_MAX and strings will never be too long, rather than being full ranged protected in the first case or using size_t, unsigned or even long long in the 2nd.

C/C++ developed in an era that included 16-bit as well as 32-bit int and the extra bit an unsigned 16-bit size_t affords was significant. Attention was needed in regard to overflow issues be it int or unsigned.

With 32-bit (or wider) applications of Google on non-16 bit int/unsigned platforms, affords the lack of attention to +/- overflow of int given its ample range. This makes sense for such applications to encourage int over unsigned. Yet int math is not well protected.

The narrow 16-bit int/unsigned concerns apply today with select embedded applications.

Google's guidelines apply well for code they write today. It is not a definitive guideline for the larger wide scope range of C/C++ code.


One reason that I can think of using signed int over unsigned int, is that if it does overflow (to negative), it is easier to detect.

In C/C++, signed int math overflow is undefined behavior and so not certainly easier to detect than defined behavior of unsigned math.


As @Chris Uzdavinis well commented, mixing signed and unsigned is best avoided by all (especially beginners) and otherwise coded carefully when needed.


I have some experience with Google's style guide, AKA the Hitchhiker's Guide to Insane Directives from Bad Programmers Who Got into the Company a Long Long Time Ago. This particular guideline is just one example of the dozens of nutty rules in that book.

Errors only occur with unsigned types if you try to do arithmetic with them (see Chris Uzdavinis example above), in other words if you use them as numbers. Unsigned types are not intended to be used to store numeric quantities, they are intended to store counts such as the size of containers, which can never be negative, and they can and should be used for that purpose.

The idea of using arithmetical types (like signed integers) to store container sizes is idiotic. Would you use a double to store the size of a list, too? That there are people at Google storing container sizes using arithmetical types and requiring others to do the same thing says something about the company. One thing I notice about such dictates is that the dumber they are, the more they need to be strict do-it-or-you-are-fired rules because otherwise people with common sense would ignore the rule.


Using unsigned types to represent non-negative values...

  • is more likely to cause bugs involving type promotion, when using signed and unsigned values, as other answer demonstrate and discuss in depth, but
  • is less likely to cause bugs involving choice of types with domains capable of representing undersirable/disallowed values. In some places you'll assume the value is in the domain, and may get unexpected and potentially hazardous behavior when other value sneak in somehow.

The Google Coding Guidelines puts emphasis on the first kind of consideration. Other guideline sets, such as the C++ Core Guidelines, put more emphasis on the second point. For example, consider Core Guideline I.12:

I.12: Declare a pointer that must not be null as not_null

Reason

To help avoid dereferencing nullptr errors. To improve performance by avoiding redundant checks for nullptr.

Example

int length(const char* p);            // it is not clear whether length(nullptr) is valid
length(nullptr);                      // OK?
int length(not_null<const char*> p);  // better: we can assume that p cannot be nullptr
int length(const char* p);            // we must assume that p can be nullptr

By stating the intent in source, implementers and tools can provide better diagnostics, such as finding some classes of errors through static analysis, and perform optimizations, such as removing branches and null tests.

Of course, you could argue for a non_negative wrapper for integers, which avoids both categories of errors, but that would have its own issues...

참고URL : https://stackoverflow.com/questions/51677855/is-using-an-unsigned-rather-than-signed-int-more-likely-to-cause-bugs-why

반응형