Việc sử dụng int chưa ký thay vì đã ký có nhiều khả năng gây ra lỗi hơn không? Tại sao?


81

Trong Hướng dẫn kiểu Google C ++ , về chủ đề "Số nguyên không dấu", đề xuất rằng

Do sự cố lịch sử, tiêu chuẩn C ++ cũng sử dụng các số nguyên không dấu để thể hiện kích thước của vùng chứa - nhiều thành viên của cơ quan tiêu chuẩn tin rằng đây là một sai lầm, nhưng không thể sửa chữa tại thời điểm này. Thực tế là số học không dấu không mô hình hóa hành vi của một số nguyên đơn giản, mà thay vào đó được xác định theo tiêu chuẩn để mô hình hóa số học mô-đun (bao quanh trên tràn / dòng dưới), có nghĩa là trình biên dịch không thể chẩn đoán một lớp lỗi đáng kể.

Điều gì sai với số học mô-đun? Đó không phải là hành vi mong đợi của một int unsigned?

Hướng dẫn đề cập đến những loại lỗi nào (một lớp đáng kể)? Tràn lỗi?

Không sử dụng kiểu không dấu chỉ để khẳng định rằng một biến không âm.

Một lý do mà tôi có thể nghĩ đến khi sử dụng int có dấu thay vì int không dấu, đó là nếu nó có tràn (thành âm), thì sẽ dễ dàng phát hiện hơn.


4
Cố gắng làm unsigned int x = 0; --x;và xem những gì xsẽ trở thành. Nếu không có kiểm tra giới hạn, kích thước có thể đột nhiên nhận được một số giá trị không mong muốn dễ dẫn đến UB.
Một số lập trình viên dude

33
Ít nhất tràn không dấu có một hành vi được xác định rõ và tạo ra kết quả mong đợi.
user7860670

35
Trên một lưu ý không liên quan (với câu hỏi của bạn nhưng không liên quan đến các hướng dẫn định kiểu của Google), nếu bạn tìm kiếm một chút, bạn sẽ tìm thấy một số lời chỉ trích (đôi khi đúng) về các hướng dẫn định kiểu của Google. Đừng coi chúng là phúc âm.
Một số lập trình viên dude

18
Mặt khác, inttràn và chảy dưới là UB. Bạn ít có khả năng gặp phải tình huống intcố gắng thể hiện một giá trị không thể hơn là tình huống giảm unsigned intxuống dưới 0 nhưng loại người sẽ ngạc nhiên bởi hành vi của unsigned intsố học là loại người cũng có thể viết mã có thể gây inttràn UB liên quan như sử dụng a < a + 1để kiểm tra tràn.
François Andrieux

12
Nếu tràn số nguyên không dấu, nó được xác định rõ. Nếu tràn số nguyên có dấu, đó là hành vi không xác định. Tôi thích hành vi được xác định rõ ràng hơn, nhưng nếu mã của bạn không thể xử lý các giá trị bị tràn, bạn sẽ bị mất cả hai. Sự khác biệt là: đối với đã ký, bạn đã bị mất vì hoạt động tràn, đối với chưa đăng nhập trong mã sau. Điểm duy nhất tôi đồng ý là nếu bạn cần các giá trị âm, thì kiểu số nguyên không dấu là lựa chọn sai - rõ ràng.
quá trung thực cho trang web này

Câu trả lời:


70

Một số các câu trả lời ở đây đề cập đến các quy tắc xúc tiến đáng ngạc nhiên giữa các giá trị ký kết và unsigned, nhưng điều đó có vẻ giống như một vấn đề liên quan đến pha trộn các giá trị ký kết và unsigned, và không nhất thiết giải thích tại sao có chữ ký biến sẽ được ưa thích hơn unsigned bên ngoài trộn kịch bản.

Theo kinh nghiệm của tôi, ngoài các quy tắc so sánh và thăng hạng hỗn hợp, có hai lý do chính khiến các giá trị không có dấu là nam châm lỗi như sau.

Các giá trị không dấu có sự gián đoạn bằng 0, giá trị phổ biến nhất trong lập trình

Cả số nguyên không dấu và có dấu đều có sự gián đoạn ở các giá trị nhỏ nhất và lớn nhất của chúng, nơi chúng quấn quanh (không dấu) hoặc gây ra hành vi không xác định (có dấu). Đối với unsignednhững điểm này là 0UINT_MAX. Vì inthọ đang ở INT_MININT_MAX. Các giá trị điển hình của INT_MININT_MAXtrên hệ thống có intgiá trị 4 byte là -2^312^31-1và trên hệ thống như vậy UINT_MAXthường là2^32-1 .

Vấn đề chính gây ra lỗi unsignedkhông áp dụng cho intnó là nó có sự gián đoạn ở mức 0 . Tất nhiên, số không là một giá trị rất phổ biến trong các chương trình, cùng với các giá trị nhỏ khác như 1,2,3. Việc cộng và trừ các giá trị nhỏ, đặc biệt là 1, trong các cấu trúc khác nhau, và nếu bạn trừ bất kỳ unsignedgiá trị nào khỏi một giá trị và nó xảy ra bằng 0, bạn chỉ nhận được một giá trị dương lớn và một lỗi gần như nhất định.

Hãy xem xét mã lặp lại trên tất cả các giá trị trong một vectơ theo chỉ mục ngoại trừ 0,5 cuối cùng :

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

Điều này hoạt động tốt cho đến một ngày bạn vượt qua một vector trống. Thay vì thực hiện 0 lần lặp, bạn nhận được v.size() - 1 == a giant number1 và bạn sẽ thực hiện 4 tỷ lần lặp và gần như có lỗ hổng tràn bộ đệm.

Bạn cần viết nó như thế này:

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

Vì vậy, nó có thể được "sửa chữa" trong trường hợp này, nhưng chỉ bằng cách suy nghĩ cẩn thận về bản chất không dấu của size_t. Đôi khi bạn không thể áp dụng cách khắc phục ở trên bởi vì thay vì một hằng số, bạn có một số bù biến số mà bạn muốn áp dụng, có thể là dương hoặc âm: vì vậy, "bên" nào của phép so sánh mà bạn cần đặt nó phụ thuộc vào độ dấu - bây giờ mã thực sự trở nên lộn xộn.

Có một vấn đề tương tự với mã cố gắng lặp xuống và bao gồm cả số không. Một cái gì đó như while (index-- > 0)hoạt động tốt, nhưng tương đương rõ ràng while (--index >= 0)sẽ không bao giờ kết thúc đối với một giá trị không dấu. Trình biên dịch của bạn có thể cảnh báo bạn khi phía bên tay phải là chữ không, nhưng chắc chắn không nếu nó là một giá trị xác định tại thời gian chạy.

Đối điểm

Một số người có thể tranh luận rằng các giá trị có dấu cũng có hai điểm không liên tục, vậy tại sao lại chọn không có dấu? Sự khác biệt là cả hai điểm gián đoạn đều rất (tối đa) cách xa 0. Tôi thực sự coi đây là một vấn đề riêng biệt của "tràn", cả giá trị có dấu và không dấu có thể tràn ở các giá trị rất lớn. Trong nhiều trường hợp, việc làm tràn là không thể xảy ra do các ràng buộc về phạm vi giá trị có thể có và việc tràn nhiều giá trị 64 bit có thể là không thể thực hiện được). Ngay cả khi có thể, khả năng xảy ra lỗi liên quan đến tràn thường rất nhỏ so với lỗi "ở mức không" và lỗi tràn cũng xảy ra đối với các giá trị chưa được đánh dấu . Vì vậy, không dấu kết hợp điều tồi tệ nhất của cả hai thế giới: có khả năng tràn các giá trị cường độ rất lớn và sự gián đoạn ở mức 0. Đã ký chỉ có trước đây.

Nhiều người sẽ tranh luận "bạn thua một chút" với trái dấu. Điều này thường đúng - nhưng không phải lúc nào cũng đúng (nếu bạn cần biểu thị sự khác biệt giữa các giá trị không có dấu, dù sao thì bạn cũng sẽ mất bit đó: rất nhiều thứ 32 bit được giới hạn ở 2 GiB, hoặc bạn sẽ có một vùng màu xám kỳ lạ khi nói một tệp có thể là 4 GiB, nhưng bạn không thể sử dụng một số API nhất định trên nửa sau 2 GiB).

Ngay cả trong những trường hợp không ký tên cũng mua cho bạn một chút: nó không mua cho bạn nhiều: nếu bạn phải hỗ trợ hơn 2 tỷ "thứ", có lẽ bạn sẽ sớm phải hỗ trợ hơn 4 tỷ.

Về mặt logic, các giá trị không dấu là một tập hợp con của các giá trị có dấu

Về mặt toán học, các giá trị không dấu (số nguyên không âm) là một tập hợp con của các số nguyên có dấu (được gọi là _integers). 2 . Tuy nhiên, có chữ ký giá trị tự nhiên bật ra khỏi các hoạt động hoàn toàn vào unsigned giá trị, chẳng hạn như phép trừ. Chúng tôi có thể nói rằng các giá trị chưa được đánh dấu không bị đóng dưới phép trừ. Điều này cũng không đúng với các giá trị đã ký.

Bạn muốn tìm "delta" giữa hai chỉ mục không dấu vào một tệp? Tốt hơn hết bạn nên thực hiện phép trừ theo đúng thứ tự, nếu không bạn sẽ nhận được câu trả lời sai. Tất nhiên, bạn thường cần kiểm tra thời gian chạy để xác định đúng thứ tự! Khi xử lý các giá trị không dấu dưới dạng số, bạn sẽ thường thấy rằng các giá trị có dấu (một cách hợp lý) luôn xuất hiện, vì vậy bạn cũng có thể bắt đầu với có dấu.

Đối điểm

Như đã đề cập trong chú thích (2) ở trên, các giá trị có dấu trong C ++ không thực sự là một tập hợp con của các giá trị không dấu có cùng kích thước, vì vậy các giá trị không dấu có thể đại diện cho cùng một số kết quả mà các giá trị có dấu có thể.

Đúng, nhưng phạm vi ít hữu ích hơn. Hãy xem xét phép trừ và các số không có dấu có phạm vi từ 0 đến 2N và các số có dấu với phạm vi từ -N đến N. Các phép trừ tùy ý dẫn đến kết quả trong phạm vi -2N đến 2N trong các trường hợp _ thứ và một trong hai loại số nguyên chỉ có thể biểu diễn một nửa của nó. Nó chỉ ra rằng vùng tập trung xung quanh 0 của -N đến N thường hữu ích hơn (chứa nhiều kết quả thực tế hơn trong mã thế giới thực) so với phạm vi từ 0 đến 2N. Hãy xem xét bất kỳ phân phối điển hình nào khác với phân phối đồng nhất (log, zipfian, bình thường, bất kỳ) và xem xét việc trừ các giá trị được chọn ngẫu nhiên khỏi phân phối đó: nhiều giá trị kết thúc bằng [-N, N] hơn [0, 2N] (thực sự, kết quả là phân phối luôn luôn có tâm ở vị trí không).

64-bit đóng cửa vì nhiều lý do để sử dụng các giá trị có dấu làm số

Tôi nghĩ rằng các đối số ở trên đã hấp dẫn đối với các giá trị 32 bit, nhưng các trường hợp tràn, ảnh hưởng đến cả có dấu và không dấu ở các ngưỡng khác nhau, làm xảy ra cho các giá trị 32-bit, vì "2 tỷ" là một con số đó có thể vượt qua bởi nhiều các đại lượng trừu tượng và vật lý (hàng tỷ đô la, hàng tỷ nano giây, mảng với hàng tỷ phần tử). Vì vậy, nếu ai đó đủ thuyết phục bằng cách tăng gấp đôi phạm vi tích cực cho các giá trị không dấu, họ có thể biến trường hợp tràn thành vấn đề và nó hơi ủng hộ không dấu.

Bên ngoài các miền chuyên biệt, giá trị 64-bit phần lớn loại bỏ mối quan tâm này. Các giá trị 64 bit đã ký có phạm vi cao hơn là 9.223.372.036.854.775.807 - hơn chín tạ . Đó là rất nhiều nano giây (giá trị khoảng 292 năm) và rất nhiều tiền. Nó cũng là một mảng lớn hơn bất kỳ máy tính nào có khả năng có RAM trong một không gian địa chỉ nhất quán trong một thời gian dài. Vì vậy, có lẽ 9 tạ tỷ là đủ cho tất cả mọi người (hiện tại)?

Khi nào sử dụng các giá trị không dấu

Lưu ý rằng hướng dẫn kiểu không cấm hoặc thậm chí không khuyến khích sử dụng các số không có dấu. Nó kết thúc bằng:

Không sử dụng kiểu không dấu chỉ để khẳng định rằng một biến không âm.

Thật vậy, có những cách sử dụng tốt cho các biến không dấu:

  • Khi bạn muốn coi một số lượng N-bit không phải là một số nguyên, mà chỉ đơn giản là một "túi bit". Ví dụ: dưới dạng bitmask hoặc bitmap, hoặc N giá trị boolean hoặc bất cứ thứ gì. Việc sử dụng này thường đi đôi với các loại chiều rộng cố định như uint32_tuint64_tvì bạn thường muốn biết kích thước chính xác của biến. Một gợi ý rằng một biến đặc biệt xứng đáng điều trị này là bạn chỉ hoạt động trên nó với với Bitwise nhà khai thác như ~, |, &, ^, >>và như vậy, chứ không phải với các phép tính số học như +, -, *, /, vv

    Không có dấu là lý tưởng ở đây vì hành vi của các toán tử bitwise được xác định rõ và chuẩn hóa. Các giá trị đã ký có một số vấn đề, chẳng hạn như hành vi không xác định và không xác định khi dịch chuyển và biểu diễn không xác định.

  • Khi bạn thực sự muốn số học mô-đun. Đôi khi bạn thực sự muốn số học mô-đun 2 ^ N. Trong những trường hợp này, "tràn" là một tính năng, không phải lỗi. Các giá trị không dấu cung cấp cho bạn những gì bạn muốn ở đây vì chúng được xác định để sử dụng số học mô-đun. Các giá trị đã ký không thể được sử dụng (dễ dàng, hiệu quả) vì chúng có biểu diễn không xác định và tràn là không xác định.


0,5 Sau khi viết cái này, tôi nhận ra đây gần giống với ví dụ của Jarod mà tôi chưa từng thấy - và vì lý do chính đáng, đó là một ví dụ tốt!

1 Chúng ta đang nói đến size_tở đây nên thường là 2 ^ 32-1 trên hệ thống 32 bit hoặc 2 ^ 64-1 trên hệ thống 64 bit.

2 Trong C ++, đây không phải là trường hợp chính xác vì các giá trị không dấu chứa nhiều giá trị ở đầu trên hơn so với kiểu có dấu tương ứng, nhưng vấn đề cơ bản tồn tại là thao tác các giá trị không dấu có thể dẫn đến (một cách hợp lý) các giá trị có dấu, nhưng không có vấn đề tương ứng với các giá trị có dấu (vì các giá trị có dấu đã bao gồm các giá trị chưa dấu).


10
Tôi đồng ý với mọi thứ bạn đã đăng, nhưng "64 bit phải là đủ cho tất cả mọi người" chắc chắn có vẻ như quá gần với "640k phải đủ cho tất cả mọi người".
Andrew Henle

6
@Andrew - yup, tôi đã lựa chọn từ ngữ của mình một cách cẩn thận :).
BeeOnRope

4
"64-bit đóng cửa trên các giá trị không được đánh dấu" -> Không đồng ý. Một số tác vụ lập trình số nguyên đơn giản không phải là trường hợp đếm và không cần giá trị âm nhưng cần độ rộng lũy ​​thừa của 2: Mật khẩu, mã hóa, đồ họa bit, lợi ích với phép toán không dấu. Nhiều ý tưởng ở đây chỉ ra lý do tại sao mã có thể sử dụng toán học có dấu khi có thể, nhưng lại rất thiếu sót khi biến kiểu không dấu trở nên vô dụng và đóng cánh cửa đối với chúng.
chux - Reinstate Monica

2
@Deduplicator - vâng, tôi đã bỏ qua vì nó có vẻ giống như một sự ràng buộc. Về mặt của mod-2 ^ N không dấu, ít nhất bạn phải có một hành vi được xác định và không có "tối ưu hóa" bất ngờ nào sẽ xuất hiện. Về phía UB, bất kỳ sự cố tràn nào trong quá trình tính toán đối với không dấu hoặc có dấu có thể là lỗi trong phần lớn các trường hợp (ngoài một số ít người mong đợi mod arithmetic) và trình biên dịch cung cấp các tùy chọn như -ftrapvvậy có thể bắt tất cả các tràn có dấu, nhưng không phải tất cả các tràn chưa ký. Tác động đến hiệu suất không quá tệ, vì vậy có thể hợp lý để biên dịch -ftrapvtrong một số trường hợp.
BeeOnRope

2
@BeeOnRope That's about the age of the universe measured in nanoseconds.Tôi nghi ngờ điều đó. Vũ trụ về 13.7*10^9 yearsgià là 4.32*10^17 shoặc 4.32*10^26 ns. Để biểu diễn 4.32*10^26dưới dạng int, bạn cần ít nhất 90 bits. 9,223,372,036,854,775,807 nssẽ chỉ về 292.5 years.
Osiris

36

Như đã nêu, trộn unsignedsignedcó thể dẫn đến hành vi không mong muốn (ngay cả khi được xác định rõ).

Giả sử bạn muốn lặp lại trên tất cả các phần tử của vectơ ngoại trừ năm phần tử cuối cùng, bạn có thể viết sai:

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

Giả sử v.size() < 5, sau đó, như v.size()unsigned, s.size() - 5sẽ là một số lượng rất lớn, và do đó i < v.size() - 5sẽ truecho một phạm vi dự kiến hơn về giá trị của i. Và UB sau đó diễn ra nhanh chóng (ra khỏi quyền truy cập bị ràng buộc một lần i >= v.size())

Nếu v.size()sẽ trả về giá trị có dấu, thì s.size() - 5sẽ là giá trị âm, và trong trường hợp trên, điều kiện sẽ là sai ngay lập tức.

Mặt khác, chỉ mục nên ở giữa [0; v.size()[như vậy unsignedcó ý nghĩa. Đã ký cũng có một vấn đề riêng là UB với hành vi tràn hoặc do triển khai xác định để chuyển sang phải một số có dấu âm, nhưng nguồn lỗi ít thường xuyên hơn đối với việc lặp lại.


2
Mặc dù bản thân tôi sử dụng số có chữ ký bất cứ khi nào tôi có thể, tôi không nghĩ rằng ví dụ này đủ mạnh. Ai sử dụng số không dấu lâu năm chắc hẳn đều biết câu thành ngữ này: thay vì i<size()-X, nên viết i+X<size(). Chắc chắn, đó là một điều cần nhớ, nhưng nó không quá khó để làm quen, theo ý kiến ​​của tôi.
geza

8
Những gì bạn đang nói về cơ bản là người ta phải biết ngôn ngữ và các quy tắc ép buộc giữa các loại. Tôi không thấy điều này thay đổi như thế nào cho dù một người sử dụng có dấu hay không dấu như câu hỏi yêu cầu. Không phải tôi khuyên bạn nên sử dụng có dấu nếu không cần giá trị âm. Tôi đồng ý với @geza, chỉ sử dụng có chữ ký khi cần thiết. Điều này làm cho hướng dẫn google có vấn đề tốt nhất . Imo đó là lời khuyên tồi.
quá trung thực đối với trang web này.

2
@toohonestforthissite Vấn đề là các quy tắc phức tạp, im lặng và là nguyên nhân chính gây ra lỗi. Việc sử dụng các loại được ký độc quyền cho số học sẽ giúp bạn giải quyết vấn đề. BTW sử dụng các kiểu không có dấu với mục đích thực thi các giá trị tích cực là một trong những hành vi lạm dụng tồi tệ nhất đối với chúng.
qua đường vào

2
Rất may, các trình biên dịch và IDE hiện đại đưa ra cảnh báo khi trộn các số có dấu và không dấu trong một biểu thức.
Alexey B.

5
@PasserBy: Nếu bạn gọi chúng là arcane, bạn cũng phải thêm các khuyến mãi số nguyên và UB cho tràn các loại có dấu là arcane. Và toán tử sizeof rất phổ biến dù sao cũng trả về một ký tự không có dấu, vì vậy bạn phải biết về chúng. Nói rằng: nếu bạn không muốn học chi tiết ngôn ngữ, chỉ cần không sử dụng C hoặc C ++! Xem xét google quảng cáo go, có lẽ đó chính xác là mục tiêu của họ. Những ngày của "Do not be evil" qua lâu rồi ...
quá trung thực cho trang web này

20

Một trong những ví dụ khó hiểu nhất về lỗi là khi bạn MIX các giá trị có dấu và chưa dấu:

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

Đầu ra:

Thế giới không có ý nghĩa

Trừ khi bạn có một ứng dụng tầm thường, bạn sẽ không thể tránh khỏi sự kết hợp nguy hiểm giữa các giá trị đã ký và chưa được ký (dẫn đến lỗi thời gian chạy) hoặc nếu bạn tạo ra các cảnh báo và tạo ra chúng lỗi thời gian biên dịch, bạn sẽ gặp phải rất nhiều static_cast trong mã của bạn. Đó là lý do tại sao tốt nhất bạn nên sử dụng nghiêm ngặt các số nguyên có dấu cho các kiểu để so sánh toán học hoặc logic. Chỉ sử dụng không dấu cho các mặt nạ bit và các loại biểu diễn bit.

Lập mô hình một loại không có dấu dựa trên miền mong đợi của các giá trị trong số của bạn là một Ý tưởng Xấu. Hầu hết các số đều gần 0 hơn là 2 tỷ, do đó, với các loại không có dấu, nhiều giá trị của bạn gần với mép của phạm vi hợp lệ hơn. Để làm cho mọi thứ tồi tệ hơn, giá trị cuối cùng có thể nằm trong một phạm vi dương đã biết, nhưng trong khi đánh giá các biểu thức, các giá trị trung gian có thể bị thiếu và nếu chúng được sử dụng ở dạng trung gian có thể là giá trị RẤT sai. Cuối cùng, ngay cả khi các giá trị của bạn được mong đợi là luôn dương, điều đó không có nghĩa là chúng sẽ không tương tác với các biến khác có thể là âm, và do đó, bạn sẽ gặp phải tình huống buộc phải trộn các loại có dấu và không có dấu, đó là nơi tồi tệ nhất để được.


8
Lập mô hình một loại không có dấu dựa trên miền dự kiến ​​của các giá trị trong số của bạn là một Ý tưởng Xấu * nếu bạn không coi các chuyển đổi ngầm định là cảnh báo và quá lười sử dụng các phôi loại thích hợp. * Lập mô hình các loại của bạn dựa trên giá trị dự kiến ​​của chúng giá trị là hoàn toàn hợp lý, chỉ là không có trong C / C ++ với các kiểu tích hợp sẵn.
villasv

1
@ user7586189 Không thể khởi tạo dữ liệu không hợp lệ là một phương pháp hay, vì vậy việc có các biến chỉ dương cho kích thước là hoàn toàn hợp lý. Nhưng bạn không thể tinh chỉnh các kiểu tích hợp trong C / C ++ để không cho phép theo mặc định các kiểu xấu như kiểu trong câu trả lời này và hiệu lực sẽ thuộc về trách nhiệm của người khác. Nếu bạn đang sử dụng một ngôn ngữ có phôi chặt chẽ hơn (ngay cả giữa các tệp được tích hợp sẵn), thì lập mô hình miền mong đợi là một ý tưởng khá hay.
biệt thự và

1
Lưu ý, tôi đã đề cập đến việc tạo cảnh báo và đặt chúng thành lỗi, nhưng không phải ai cũng làm như vậy. Tôi vẫn không đồng ý @villasv với tuyên bố của bạn về giá trị mô hình. Bằng cách chọn không dấu, bạn CŨNG ngầm mô hình hóa mọi giá trị khác mà nó có thể tiếp xúc mà không có nhiều dự đoán về điều đó sẽ như thế nào. Và gần như chắc chắn nhận sai.
Chris Uzdavinis

1
Lập mô hình với miền trong tâm trí là một điều tốt. Sử dụng không dấu để lập mô hình miền là KHÔNG. (Đã ký vs unsigned nên được lựa chọn dựa trên các loại sử dụng , không dao động của giá trị , trừ khi nó không thể làm khác.)
Chris Uzdavinis

2
Khi cơ sở mã của bạn có hỗn hợp các giá trị có dấu và chưa dấu, khi bạn đưa ra các cảnh báo và quảng cáo chúng thành lỗi, mã sẽ kết thúc với static_casts để làm cho chuyển đổi rõ ràng (vì vẫn cần phải thực hiện phép toán.) Ngay cả khi đúng, nó dễ xảy ra lỗi, khó làm việc hơn và khó đọc hơn.
Chris Uzdavinis

11

Tại sao việc sử dụng một int chưa ký lại có nhiều khả năng gây ra lỗi hơn sử dụng một int có dấu?

Sử dụng một unsigned loại là không nhiều khả năng nguyên nhân lỗi vì sử dụng một loại với các lớp học nhất định các nhiệm vụ.

Sử dụng các công cụ thích hợp cho công việc.

Điều gì sai với số học mô-đun? Đó không phải là hành vi mong đợi của một int unsigned?
Tại sao việc sử dụng một int chưa ký lại có nhiều khả năng gây ra lỗi hơn sử dụng một int có dấu?

Nếu nhiệm vụ nếu được kết hợp tốt: không có gì sai. Không, không nhiều khả năng.

Thuật toán bảo mật, mã hóa và xác thực dựa trên phép toán mô-đun không dấu.

Các thuật toán nén / giải nén cũng như các định dạng đồ họa khác nhau đều có lợi và ít lỗi hơn với phép toán không dấu .

Bất cứ lúc nào các nhà khai thác chút khôn ngoan và thay đổi được sử dụng, unsigned hoạt động không được điều sai lầm với các vấn đề đăng nhập phần mở rộng của toán học.


Phép toán số nguyên có dấu có giao diện trực quan và cảm thấy dễ hiểu đối với tất cả mọi người, kể cả người học viết mã. C / C ++ ban đầu không được nhắm mục tiêu và bây giờ không nên là một ngôn ngữ giới thiệu. Để mã hóa nhanh sử dụng lưới an toàn liên quan đến tràn, các ngôn ngữ khác phù hợp hơn. Đối với mã Lean nhanh, C giả định rằng các lập trình viên biết họ đang làm gì (họ có kinh nghiệm).

Một cái bẫy của toán học hôm nay là phổ biến 32-bit intmà với rất nhiều vấn đề là cũng đủ rộng cho các tác vụ thông thường mà không cần kiểm tra phạm vi. Điều này dẫn đến sự tự mãn mà tràn không được mã hóa chống lại. Thay vào đó, for (int i=0; i < n; i++) int len = strlen(s);được xem là OK vì nđược giả định là < INT_MAXvà các chuỗi sẽ không bao giờ quá dài, thay vì được bảo vệ phạm vi đầy đủ trong trường hợp đầu tiên hoặc sử dụng size_t, unsignedhoặc thậm chí long longtrong trường hợp thứ hai.

C / C ++ được phát triển trong thời đại bao gồm 16-bit cũng như 32-bit intvà bổ sung thêm bit 16-bit không dấu size_tlà rất quan trọng. Cần chú ý đến các vấn đề tràn có thể là nó inthoặc unsigned.

Với các ứng dụng 32-bit (hoặc rộng hơn) của Google trên các int/unsignednền tảng không phải 16 bit , khiến cho việc thiếu sự chú ý đến +/- tràn intdo phạm vi rộng của nó. Điều này có ý nghĩa đối với các ứng dụng như vậy để khuyến khích inthơn unsigned. Tuy nhiên, inttoán học không được bảo vệ tốt.

Ngày int/unsignednay, mối quan tâm hẹp về 16 bit được áp dụng với một số ứng dụng nhúng.

Nguyên tắc của Google áp dụng tốt cho mã họ viết ngày hôm nay. Nó không phải là một hướng dẫn dứt khoát cho phạm vi rộng lớn hơn của mã C / C ++.


Một lý do mà tôi có thể nghĩ đến khi sử dụng int có dấu thay vì int không dấu, đó là nếu nó có tràn (thành âm), thì sẽ dễ dàng phát hiện hơn.

Trong C / C ++, tràn toán học int có dấu là hành vi không xác định và do đó chắc chắn không dễ phát hiện hơn hành vi đã xác định của toán không dấu .


Như @ Chris Uzdavinis cũng nhận xét, trộn unsigned tốt nhất là tránh tất cả (đặc biệt là người mới bắt đầu) và nếu không được mã hóa cẩn thận khi cần thiết.


2
Bạn hiểu rõ rằng an intcũng không mô hình hóa hành vi của một số nguyên "thực tế". Hành vi không xác định trên tràn không phải là cách một nhà toán học nghĩ về số nguyên: họ không có khả năng "tràn" với một số nguyên trừu tượng. Nhưng đây là những đơn vị lưu trữ máy móc, không phải là những con số của một gã toán học.
tchrist

1
@tchrist: Hành vi không dấu khi tràn là cách một nhà toán học sẽ nghĩ về một vòng đại số trừu tượng của các số nguyên đồng dư mod (type_MAX + 1).
supercat

Nếu bạn đang sử dụng gcc, signed intthì rất dễ phát hiện tràn (với -ftrapv), trong khi "tràn" không dấu thì khó phát hiện.
anatolyg

5

Tôi có một số kinh nghiệm với hướng dẫn phong cách của Google, AKA the Hitchhiker's Guide to Insane Directions from Bad Lập trình viên đã gia nhập công ty trong một thời gian dài. Hướng dẫn cụ thể này chỉ là một ví dụ trong hàng chục quy tắc hấp dẫn trong cuốn sách đó.

Lỗi chỉ xảy ra với các loại không dấu nếu bạn cố gắng thực hiện số học với chúng (xem ví dụ của Chris Uzdavinis ở trên), nói cách khác nếu bạn sử dụng chúng dưới dạng số. Loại không có dấu không nhằm mục đích sử dụng để lưu trữ số lượng, chúng nhằm lưu trữ số lượng chẳng hạn như kích thước của thùng chứa, không bao giờ có thể là số âm, và chúng có thể và nên được sử dụng cho mục đích đó.

Ý tưởng sử dụng các kiểu số học (như số nguyên có dấu) để lưu trữ kích thước thùng chứa là ngu ngốc. Bạn cũng sẽ sử dụng double để lưu trữ kích thước của một danh sách chứ? Việc có những người tại Google lưu trữ kích thước vùng chứa bằng các kiểu số học và yêu cầu những người khác làm điều tương tự nói lên điều gì đó về công ty. Một điều tôi nhận thấy về những mệnh lệnh như vậy là họ càng ngáo đá, họ càng cần phải tuân thủ các quy tắc nghiêm ngặt để làm điều đó hoặc bạn bị sa thải bởi vì nếu không những người có ý thức thông thường sẽ bỏ qua quy tắc.


Mặc dù tôi hiểu được sự trôi chảy của bạn, nhưng các câu lệnh tổng hợp được thực hiện hầu như sẽ loại bỏ các phép toán bit nếu unsignedcác kiểu chỉ có thể chứa số đếm và không được sử dụng trong số học. Vì vậy, phần "Chỉ thị mất trí từ các lập trình viên tồi" có ý nghĩa hơn.
David C. Rankin

@ DavidC.Rankin Xin đừng coi đó là một tuyên bố "chăn". Rõ ràng là có nhiều cách sử dụng hợp pháp cho các số nguyên không dấu (như lưu trữ các giá trị bitwise).
Tyler Durden

Vâng, vâng - tôi đã không, đó là lý do tại sao tôi nói "I get your drift."
David C. Rankin

1
Số đếm thường được so sánh với những thứ đã thực hiện số học trên chúng, chẳng hạn như chỉ số. Cách C xử lý các phép so sánh liên quan đến số có dấu và không dấu có thể dẫn đến nhiều câu hỏi kỳ quặc. Ngoại trừ trong các trường hợp giá trị cao nhất của số đếm sẽ nằm trong loại không có dấu nhưng không phải là loại có dấu tương ứng (phổ biến trong những ngày màint 16 bit, nhưng ngày nay ít hơn nhiều) thì tốt hơn nên có các số đếm hoạt động như số.
supercat

1
"Lỗi chỉ xảy ra với các loại không dấu nếu bạn cố gắng làm số học với chúng" - Điều này luôn xảy ra. "Ý tưởng sử dụng các kiểu số học (như số nguyên có dấu) để lưu trữ kích thước vùng chứa là ngu ngốc" - Không phải vậy và ủy ban C ++ hiện coi việc sử dụng size_t là một sai lầm lịch sử. Nguyên nhân? Chuyển đổi ngầm định.
Átila Neves

1

Sử dụng các loại không dấu để biểu thị các giá trị không âm ...

  • nhiều khả năng gây ra lỗi liên quan đến quảng bá kiểu, khi sử dụng các giá trị có dấu và không dấu, vì câu trả lời khác chứng minh và thảo luận sâu hơn, nhưng
  • ít có khả năng để gây ra lỗi liên quan đến sự lựa chọn các loại với lĩnh vực có khả năng đại diện cho undersirable / giá trị không được phép. Ở một số nơi, bạn sẽ cho rằng giá trị nằm trong miền và có thể nhận được hành vi không mong muốn và tiềm ẩn nguy hiểm khi giá trị khác xâm nhập bằng cách nào đó.

Nguyên tắc mã hóa của Google nhấn mạnh vào loại cân nhắc đầu tiên. Các bộ hướng dẫn khác, chẳng hạn như Nguyên tắc cốt lõi của C ++ , nhấn mạnh nhiều hơn vào điểm thứ hai. Ví dụ, hãy xem xét Nguyên tắc cốt lõi I.12 :

I.12: Khai báo một con trỏ không được rỗng như not_null

Lý do

Để giúp tránh lỗi nullptr hội nghị. Để cải thiện hiệu suất bằng cách tránh kiểm tra dư thừa cho nullptr.

Thí dụ

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

Bằng cách nêu rõ mục đích trong nguồn, người triển khai và công cụ có thể cung cấp chẩn đoán tốt hơn, chẳng hạn như tìm một số lớp lỗi thông qua phân tích tĩnh và thực hiện tối ưu hóa, chẳng hạn như loại bỏ các nhánh và thử nghiệm rỗng.

Tất nhiên, bạn có thể tranh luận về một non_negativetrình bao bọc cho số nguyên, điều này tránh được cả hai loại lỗi, nhưng điều đó sẽ có vấn đề riêng ...


0

Tuyên bố của google về việc sử dụng không dấu làm loại kích thước cho vùng chứa . Ngược lại, câu hỏi có vẻ chung chung hơn. Hãy ghi nhớ điều đó trong khi bạn đọc tiếp.

Vì hầu hết các câu trả lời cho đến nay đều phản ứng với tuyên bố của google, ít hơn đối với câu hỏi lớn hơn, tôi sẽ bắt đầu câu trả lời của mình về kích thước vùng chứa âm và sau đó cố gắng thuyết phục bất kỳ ai (vô vọng, tôi biết ...) rằng không dấu là tốt.

Kích thước vùng chứa đã ký

Giả sử ai đó đã mã hóa một lỗi, dẫn đến chỉ mục vùng chứa âm. Kết quả là hành vi không xác định hoặc một ngoại lệ / vi phạm quyền truy cập. Điều đó có thực sự tốt hơn việc nhận được hành vi không xác định hoặc vi phạm ngoại lệ / truy cập khi loại chỉ mục chưa được đánh dấu? Tôi nghĩ không có.

Bây giờ, có một lớp người thích nói về toán học và những gì là "tự nhiên" trong bối cảnh này. Làm thế nào một loại tích phân với số âm có thể tự nhiên để mô tả một cái gì đó, vốn dĩ là> = 0? Sử dụng mảng có kích thước âm nhiều? IMHO, đặc biệt là những người có khuynh hướng toán học sẽ thấy sự không phù hợp về ngữ nghĩa này (loại kích thước / chỉ số nói rằng có thể có âm, trong khi khó tưởng tượng nổi một mảng có kích thước âm).

Vì vậy, câu hỏi duy nhất, còn lại về vấn đề này là nếu - như đã nêu trong nhận xét của google - một trình biên dịch thực sự có thể hỗ trợ tích cực trong việc tìm ra các lỗi như vậy. Và thậm chí còn tốt hơn giải pháp thay thế, đó sẽ là các số nguyên không dấu được bảo vệ theo quy trình (hợp ngữ x86-64 và có lẽ các kiến ​​trúc khác có phương tiện để đạt được điều đó, chỉ C / C ++ không sử dụng các phương tiện đó). Cách duy nhất tôi có thể hiểu là nếu trình biên dịch tự động thêm kiểm tra thời gian chạy ( if (index < 0) throwOrWhatever) hoặc trong trường hợp các hành động thời gian biên dịch tạo ra nhiều cảnh báo / lỗi tích cực có khả năng xảy ra sai "Chỉ số cho quyền truy cập mảng này có thể là số âm." Tôi có nghi ngờ của tôi, điều này sẽ hữu ích.

Ngoài ra, những người thực sự viết kiểm tra thời gian chạy cho các chỉ số mảng / vùng chứa của họ, việc xử lý các số nguyên có dấu sẽ nhiều hơn . Thay vì viết if (index < container.size()) { ... }bây giờ bạn phải viết:if (index >= 0 && index < container.size()) { ... } . Có vẻ như lao động cưỡng bức đối với tôi và không giống như một sự cải thiện ...

Những ngôn ngữ không có loại không dấu sẽ rất ...

Vâng, đây là một cú đâm vào java. Bây giờ, tôi đến từ nền tảng lập trình nhúng và chúng tôi đã làm việc rất nhiều với các bus trường, nơi các phép toán nhị phân (và, hoặc, xor, ...) và thành phần bit khôn ngoan của các giá trị thực sự là bánh mì và bơ. Đối với một trong những sản phẩm của chúng tôi, chúng tôi - hay đúng hơn là một khách hàng - muốn có một cổng java ... và tôi ngồi đối diện với một người may mắn rất có năng lực đã làm cổng (tôi đã từ chối ...). Anh ấy cố gắng giữ bình tĩnh ... và chịu đựng trong im lặng ... nhưng nỗi đau ở đó, anh ấy không thể ngừng nguyền rủa sau vài ngày liên tục xử lý các giá trị tích phân có dấu, những giá trị này NÊN không có dấu ... Ngay cả khi viết các bài kiểm tra đơn vị cho những tình huống đó thật khó chịu và cá nhân tôi, tôi nghĩ java sẽ tốt hơn nếu họ bỏ qua các số nguyên có dấu và chỉ cung cấp không dấu ... ít nhất sau đó, bạn không phải quan tâm đến tiện ích mở rộng ký hiệu, v.v.

Đó là 5 xu của tôi về vấn đề này.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.