Menggunakan bilangan bulat tak bertanda di C dan C ++

23

Saya punya pertanyaan yang sangat sederhana yang membuat saya bingung untuk waktu yang lama. Saya berurusan dengan jaringan dan basis data sehingga banyak data yang saya tangani adalah penghitung 32-bit dan 64-bit (tidak ditandatangani), id identifikasi 32-bit dan 64-bit (juga tidak memiliki pemetaan tanda yang berarti). Saya praktis tidak pernah berurusan dengan masalah kata nyata yang dapat dinyatakan sebagai angka negatif.

Saya dan rekan kerja saya secara rutin menggunakan tipe yang tidak ditandatangani seperti uint32_tdan uint64_tuntuk masalah ini dan karena itu sering terjadi kami juga menggunakannya untuk indeks array dan penggunaan integer umum lainnya.

Pada saat yang sama berbagai panduan pengkodean yang saya baca (misalnya Google) mencegah penggunaan tipe integer yang tidak ditandatangani, dan sejauh yang saya tahu baik Java maupun Scala tidak memiliki tipe integer yang tidak ditandatangani.

Jadi, saya tidak tahu apa yang benar untuk dilakukan: menggunakan nilai yang ditandatangani di lingkungan kita akan sangat merepotkan, pada saat yang sama panduan pengkodean bersikeras untuk melakukan hal ini.

zzz777
sumber

Jawaban:

31

Ada dua aliran pemikiran tentang ini, dan tidak ada yang akan setuju.

Yang pertama berpendapat bahwa ada beberapa konsep yang secara inheren tidak ditandai - seperti indeks array. Tidak masuk akal untuk menggunakan nomor yang ditandatangani untuk mereka karena dapat menyebabkan kesalahan. Itu juga dapat memberlakukan batasan yang tidak perlu pada hal-hal - array yang menggunakan indeks 32-bit yang ditandatangani hanya dapat mengakses 2 miliar entri, sementara beralih ke nomor 32-bit yang tidak ditandatangani memungkinkan 4 miliar entri.

Yang kedua berpendapat bahwa dalam program apa pun yang menggunakan angka yang tidak ditandatangani, cepat atau lambat Anda akan berakhir melakukan aritmatika bertanda-tangani campuran. Ini dapat memberikan hasil yang aneh dan tidak terduga: memberikan nilai besar yang tidak ditandatangani untuk ditandatangani memberi angka negatif, dan sebaliknya memberikan angka negatif ke yang tidak ditandatangani memberikan nilai positif yang besar. Ini bisa menjadi sumber kesalahan besar.

Simon B
sumber
8
Masalah aritmatika bertanda-tangani campuran terdeteksi oleh kompiler; biarkan saja bangunan Anda bebas dari peringatan (dengan tingkat peringatan yang cukup tinggi). Selain itu, intlebih pendek untuk mengetik :)
rucamzu
7
Pengakuan: Saya dengan aliran pemikiran kedua, dan meskipun saya mengerti pertimbangan untuk jenis yang tidak ditandatangani: intlebih dari cukup untuk indeks array 99,99% dari waktu. Masalah aritmatika yang ditandatangani dan tidak ditandatangani jauh lebih umum, dan karenanya didahulukan dalam hal apa yang harus dihindari. Ya, kompiler memperingatkan Anda tentang hal ini, tetapi berapa banyak peringatan yang Anda dapatkan ketika menyusun proyek yang cukup besar? Mengabaikan peringatan itu berbahaya, dan praktik yang buruk, tetapi di dunia nyata ...
Elias Van Ootegem
11
Memberi +1 pada jawabannya. Perhatian : Blunt Opinions Di depan : 1: Respons saya terhadap aliran pemikiran kedua adalah: Saya bertaruh uang bahwa siapa pun yang mendapatkan hasil tak terduga dari tipe integral yang tidak ditandatangani dalam C akan memiliki perilaku yang tidak terdefinisi (dan bukan jenis akademis murni) di program C non-sepele mereka yang menggunakan tipe integral yang ditandatangani . Jika Anda tidak tahu C cukup baik untuk berpikir bahwa tipe unsigned adalah yang lebih baik untuk digunakan, saya sarankan menghindari C. 2: Ada satu tipe yang tepat untuk indeks array dan ukuran dalam C, dan itu size_t, kecuali ada case-case khusus alasan bagus sebaliknya.
mtraceur
5
Anda mengalami masalah tanpa campur tangan campuran. Hitung saja int unsigned minus int unsigned.
gnasher729
4
Tidak mempermasalahkan Anda, Simon, hanya dengan aliran pemikiran pertama yang berpendapat bahwa "ada beberapa konsep yang secara inheren tidak ditandai - seperti indeks susunan." khususnya: "Ada satu tipe yang tepat untuk indeks array ... di C," Omong kosong! . Kami, DSP, selalu menggunakan indeks negatif. khususnya dengan respons impuls genap atau simetri yang non-kausal. dan untuk LUT matematika. aku di sekolah kedua pemikiran, tapi saya berpikir bahwa itu berguna untuk memiliki kedua ditandatangani dan unsigned integer di C dan C ++.
robert bristow-johnson
21

Pertama-tama, pedoman pengkodean Google C ++ bukan yang sangat baik untuk diikuti: pedoman ini menghindari hal-hal seperti pengecualian, dorongan, dll. Yang merupakan bahan pokok dari C ++ modern. Kedua, hanya karena pedoman tertentu bekerja untuk perusahaan X tidak berarti itu akan cocok untuk Anda. Saya akan terus menggunakan tipe yang tidak ditandatangani, karena Anda membutuhkannya.

Aturan praktis yang layak untuk C ++ adalah: lebih suka intkecuali Anda memiliki alasan yang baik untuk menggunakan sesuatu yang lain.

bstamour
sumber
8
Bukan itu yang saya maksudkan sama sekali. Konstruktor adalah untuk menetapkan invarian, dan karena mereka bukan fungsi, mereka tidak bisa hanya return falsejika invarian itu tidak ditetapkan. Jadi, Anda dapat memisahkan hal-hal dan menggunakan fungsi init untuk objek Anda, atau Anda dapat melempar std::runtime_error, membiarkan tumpukan terjadi, dan membiarkan semua objek RAII Anda membersihkan sendiri secara otomatis dan Anda pengembang dapat menangani pengecualian di tempat yang nyaman untuk Anda melakukannya.
bstamour
5
Saya tidak melihat bagaimana jenis aplikasi membuat perbedaan. Setiap kali Anda memanggil konstruktor pada objek, Anda membuat invarian dengan parameter. Jika invarian itu tidak dapat dipenuhi, maka Anda harus memberi tanda kesalahan jika program Anda tidak dalam kondisi baik. Karena konstruktor tidak dapat mengembalikan bendera, melempar pengecualian adalah opsi alami. Tolong beri argumen yang kuat mengapa aplikasi bisnis tidak akan mendapat manfaat dari gaya pengkodean seperti itu.
bstamour
8
Saya sangat ragu bahwa setengah dari semua programmer C ++ tidak mampu menggunakan pengecualian dengan benar. Tetapi bagaimanapun juga jika Anda berpikir bahwa rekan kerja Anda tidak mampu menulis C ++ modern maka tentu saja jauhi C ++ modern.
bstamour
6
@ zzz777 Jangan gunakan pengecualian? Apakah konstruktor pribadi yang dibungkus oleh fungsi pabrik publik yang menangkap pengecualian dan melakukan apa - mengembalikan a nullptr? mengembalikan objek "default" (apa pun artinya)? Anda tidak memecahkan apa pun - Anda baru saja menyembunyikan masalah di bawah karpet, dan berharap tidak ada yang tahu.
Mael
5
@ zzz777 Jika Anda toh akan menabrak kotak, mengapa Anda peduli jika itu terjadi karena pengecualian atau signal(6)? Jika Anda menggunakan pengecualian, 50% pengembang yang tahu cara menangani mereka dapat menulis kode yang baik, dan sisanya dapat dilakukan oleh rekan-rekan mereka.
IllusiveBrian
6

Jawaban lainnya tidak memiliki contoh dunia nyata, jadi saya akan menambahkan satu. Salah satu alasan mengapa saya (secara pribadi) mencoba menghindari tipe yang tidak ditandatangani.

Pertimbangkan untuk menggunakan size_t standar sebagai indeks array:

for (size_t i = 0; i < n; ++i)
    // do something here;

Ok, sangat normal. Kemudian, pertimbangkan kami memutuskan untuk mengubah arah loop karena beberapa alasan:

for (size_t i = n - 1; i >= 0; --i)
    // do something here;

Dan sekarang tidak berfungsi. Jika kita digunakan intsebagai iterator, tidak akan ada masalah. Saya telah melihat kesalahan seperti itu dua kali dalam dua tahun terakhir. Setelah itu terjadi dalam produksi dan sulit untuk di-debug.

Alasan lain bagi saya adalah peringatan yang mengganggu, yang membuat Anda menulis sesuatu seperti ini setiap saat :

int n = 123;  // for some reason n is signed
...
for (size_t i = 0; i < size_t(n); ++i)

Ini adalah hal-hal kecil, tetapi mereka bertambah. Saya merasa kode ini lebih bersih jika hanya bilangan bulat bertanda yang digunakan di mana-mana.

Sunting: Tentu, contohnya terlihat bodoh, tapi saya melihat orang membuat kesalahan ini. Jika ada cara mudah untuk menghindarinya, mengapa tidak menggunakannya?

Ketika saya mengkompilasi bagian kode berikut dengan VS2015 atau GCC saya tidak melihat peringatan dengan pengaturan peringatan default (bahkan dengan -Dinding untuk GCC). Anda harus meminta -Wextra untuk mendapatkan peringatan tentang hal ini di GCC. Ini adalah salah satu alasan Anda harus selalu mengkompilasi dengan Wall dan Wextra (dan menggunakan analisa statis), tetapi dalam banyak proyek kehidupan nyata orang tidak melakukannya.

#include <vector>
#include <iostream>


void unsignedTest()
{
    std::vector<int> v{ 1, 2 };

    for (int i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;

    for (size_t i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;
}

int main()
{
    unsignedTest();
    return 0;
}
Aleksei Petrenko
sumber
Anda bahkan bisa lebih salah lagi dengan tipe yang ditandatangani ... Dan kode-contoh Anda sangat mati otak dan sangat keliru kompilator mana pun yang layak akan memperingatkan jika Anda meminta peringatan.
Deduplicator
1
Di masa lalu saya menggunakan kengerian for (size_t i = n - 1; i < n; --i)untuk membuatnya bekerja dengan benar.
Simon B
2
Berbicara tentang for-loop dengan size_tterbalik, ada pedoman pengkodean dalam gayafor (size_t revind = 0u; revind < n; ++revind) { size_t ind = n - 1u - revind; func(ind); }
rwong
2
@rwong Omg, ini jelek. Kenapa tidak pakai saja int? :)
Aleksei Petrenko
1
@AlexeyPetrenko - perhatikan bahwa baik standar C atau C ++ saat intini tidak cukup besar untuk menampung semua nilai valid size_t. Khususnya, intmemungkinkan angka hanya hingga 2 ^ 15-1, dan biasanya melakukannya pada sistem yang memiliki batas alokasi memori 2 ^ 16 (atau dalam kasus tertentu bahkan lebih tinggi). longmungkin taruhan yang lebih aman, meskipun masih belum dijamin berhasil. Hanya size_tdijamin untuk bekerja di semua platform dan dalam semua kasus.
Jules
4
for (size_t i = v.size() - 1; i >= 0; --i)
   std::cout << v[i] << std::endl;

Masalahnya di sini adalah bahwa Anda menulis loop dengan cara yang tidak jelas yang mengarah ke perilaku yang salah. Konstruksi loop seperti pemula yang diajarkan untuk tipe yang ditandatangani (yang OK dan benar) tetapi tidak cocok untuk nilai yang tidak ditandatangani. Tapi ini tidak bisa berfungsi sebagai argumen balasan terhadap penggunaan tipe yang tidak ditandatangani, tugas di sini adalah untuk mendapatkan loop Anda dengan benar. Dan ini dapat dengan mudah diperbaiki agar dapat bekerja dengan baik untuk tipe yang tidak ditandatangani seperti:

for (size_t i = v.size(); i-- > 0; )
    std::cout << v[i] << std::endl;

Perubahan ini hanya mengembalikan urutan perbandingan dan operasi penurunan dan menurut pendapat saya cara yang paling efektif, tidak mengganggu, bersih dan singkat untuk menangani penghitung yang tidak ditandatangani dalam loop mundur. Anda akan melakukan hal yang sama (secara intuitif) saat menggunakan loop sementara:

size_t i = v.size();
while (i > 0)
{
    --i;
    std::cout << v[i] << std::endl;
}

Tidak ada underflow yang dapat terjadi, case dari wadah kosong tertutup secara implisit, seperti pada varian terkenal untuk loop counter yang ditandatangani, dan badan loop mungkin tetap tidak berubah dalam perbandingan ke counter yang ditandatangani atau loop ke depan. Anda hanya perlu membiasakan diri pada konstruksi loop pertama yang agak aneh. Tetapi setelah Anda melihat bahwa belasan kali tidak ada yang tidak dapat dipahami lagi.

Saya akan beruntung jika kursus pemula tidak hanya menunjukkan loop yang benar untuk masuk tetapi juga untuk tipe yang tidak ditandatangani. Ini akan menghindari beberapa kesalahan yang seharusnya disalahkan oleh IMHO kepada pengembang yang tidak sengaja alih-alih menyalahkan tipe yang tidak ditandatangani.

HTH

Don Pedro
sumber
1

Bilangan bulat bertanda ada karena suatu alasan.

Pertimbangkan, misalnya, menyerahkan data sebagai byte individual, misalnya dalam paket jaringan atau buffer file. Anda kadang-kadang dapat menemukan binatang seperti integer 24-bit Mudah digeser dari tiga bilangan bulat 8-bit yang tidak ditandatangani, tidak begitu mudah dengan bilangan bulat bertanda 8-bit.

Atau pikirkan algoritma yang menggunakan tabel pencarian karakter. Jika karakter adalah bilangan bulat 8-bit unsigned, Anda bisa mengindeks tabel pencarian dengan nilai karakter. Namun, apa yang Anda lakukan jika bahasa pemrograman tidak mendukung bilangan bulat yang tidak ditandai? Anda akan memiliki indeks negatif ke array. Yah, saya kira Anda bisa menggunakan sesuatu seperti charval + 128tapi itu hanya jelek.

Banyak format file, pada kenyataannya, menggunakan bilangan bulat yang tidak ditandatangani dan jika bahasa pemrograman aplikasi tidak mendukung bilangan bulat yang tidak ditandatangani, itu bisa menjadi masalah.

Kemudian pertimbangkan nomor urut TCP. Jika Anda menulis kode pemrosesan TCP, Anda pasti ingin menggunakan bilangan bulat yang tidak ditandatangani.

Terkadang, efisiensi sangat penting sehingga Anda benar-benar membutuhkan sedikit tambahan bilangan bulat yang tidak ditandatangani. Pertimbangkan misalnya perangkat IoT yang dikirim dalam jutaan. Banyak sumber daya pemrograman dapat dibenarkan untuk dibelanjakan pada optimasi mikro.

Saya berpendapat bahwa pembenaran untuk menghindari penggunaan tipe integer yang tidak ditandatangani (aritmatika tanda campuran, perbandingan tanda campuran) dapat diatasi oleh kompiler dengan peringatan yang tepat. Peringatan semacam itu biasanya tidak diaktifkan secara default, tetapi lihat misalnya -Wextraatau secara terpisah -Wsign-compare(diaktifkan otomatis dalam bahasa C oleh -Wextra, meskipun saya pikir itu tidak diaktifkan otomatis dalam bahasa C ++) dan -Wsign-conversion.

Meskipun demikian, jika ragu, gunakan jenis yang ditandatangani. Sering kali, itu adalah pilihan yang berfungsi dengan baik. Dan aktifkan peringatan kompiler itu!

ahli hukum agama
sumber
0

Ada banyak kasus di mana bilangan bulat tidak benar-benar mewakili angka, tetapi misalnya topeng bit, id, dll. Pada dasarnya kasus di mana menambahkan 1 ke bilangan bulat tidak memiliki hasil yang berarti. Dalam kasus tersebut, gunakan yang tidak ditandatangani.

Ada banyak kasus di mana Anda melakukan aritmatika dengan bilangan bulat. Dalam kasus ini, gunakan bilangan bulat yang ditandatangani, untuk menghindari kesalahan perilaku sekitar nol. Lihat banyak contoh dengan loop, di mana menjalankan loop ke nol baik menggunakan kode yang sangat tidak intuitif atau rusak karena penggunaan nomor yang tidak ditandatangani. Ada argumen "tetapi indeks tidak pernah negatif" - tentu saja, tetapi perbedaan indeks misalnya negatif.

Dalam kasus yang sangat jarang di mana indeks melebihi 2 ^ 31 tetapi tidak 2 ^ 32, Anda tidak menggunakan bilangan bulat yang tidak ditandatangani, Anda menggunakan bilangan bulat 64 bit.

Akhirnya, jebakan yang bagus: Dalam satu lingkaran "untuk (i = 0; i <n; ++ i) a [i] ..." jika saya unsigned 32 bit, dan memori melebihi 32 bit alamat, kompilator tidak dapat mengoptimalkan akses ke [i] dengan menambah pointer, karena pada i = 2 ^ 32 - 1 saya membungkus. Bahkan ketika n tidak pernah menjadi sebesar itu. Menggunakan bilangan bulat yang ditandatangani menghindari hal ini.

gnasher729
sumber
-5

Akhirnya, saya menemukan jawaban yang sangat bagus di sini: "Secure Programming Cookbook" oleh J.Viega dan M.Messier ( http://shop.oreilly.com/product/9780596003944.do )

Masalah keamanan dengan bilangan bulat yang ditandatangani:

  1. Jika fungsi membutuhkan parameter positif, mudah untuk melupakan memeriksa kisaran yang lebih rendah.
  2. Pola bit tidak intuitif dari konversi ukuran bilangan bulat negatif.
  3. Pola bit tidak intuitif yang dihasilkan oleh operasi shift kanan bilangan bulat negatif.

Ada masalah dengan konversi yang tidak ditandatangani yang ditandatangani <-> sehingga tidak disarankan menggunakan campuran.

zzz777
sumber
1
Mengapa ini jawaban yang bagus? Apa itu resep 3.5? Apa yang dikatakan tentang integer overflow dll?
Baldrickk
Dalam pengalaman praktis saya, ini adalah buku yang sangat bagus dengan saran berharga lainnya dalam aspek yang saya coba dan cukup kuat dalam rekomendasi ini. Dibandingkan dengan bahaya overflow bilangan bulat pada array yang lebih lama dari 4G tampaknya cukup lemah. Jika saya harus berurusan dengan array sebesar itu, program saya akan memiliki banyak penyesuaian untuk menghindari hukuman kinerja.
zzz777
1
ini bukan tentang apakah buku itu bagus. Jawaban Anda tidak memberikan pembenaran untuk penggunaan penerima, dan tidak semua orang akan memiliki salinan buku untuk mencarinya. Lihatlah contoh bagaimana menulis jawaban yang baik
Baldrickk
FYI baru belajar tentang alasan lain menggunakan bilangan bulat tak bertanda: seseorang dapat dengan mudah mendeteksi overlow: youtube.com/…
zzz777