5. Kesalahan umum saat menggunakan array.
5.1 Jebakan: Mempercayai tautan jenis tidak aman.
OK, Anda telah diberitahu, atau telah mengetahui sendiri, bahwa global (variabel lingkup namespace yang dapat diakses di luar unit terjemahan) adalah Evil ™. Tapi apakah Anda tahu betapa sebenarnya Evil ™ mereka? Pertimbangkan program di bawah ini, yang terdiri dari dua file [main.cpp] dan [angka.cpp]:
// [main.cpp]
#include <iostream>
extern int* numbers;
int main()
{
using namespace std;
for( int i = 0; i < 42; ++i )
{
cout << (i > 0? ", " : "") << numbers[i];
}
cout << endl;
}
// [numbers.cpp]
int numbers[42] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
Pada Windows 7 ini mengkompilasi dan menghubungkan dengan baik dengan MinGW g ++ 4.4.1 dan Visual C ++ 10.0.
Karena tipe tidak cocok, program macet ketika Anda menjalankannya.
Penjelasan di dalam-formal: program ini memiliki Perilaku Tidak Terdefinisi (UB), dan alih-alih menabraknya, itu hanya dapat hang, atau mungkin tidak melakukan apa-apa, atau dapat mengirim threating email ke presiden Amerika Serikat, Rusia, India, Cina dan Swiss, dan membuat Nasal Daemon terbang keluar dari hidung Anda.
Dalam prakteknya penjelasan: dalam main.cpp
array diperlakukan sebagai pointer, ditempatkan pada alamat yang sama dengan array. Untuk 32-bit yang dapat dieksekusi ini berarti bahwa nilai pertama
int
dalam array, diperlakukan sebagai pointer. Yaitu, di main.cpp
dalam
numbers
variabel mengandung, atau muncul untuk mengandung, (int*)1
. Ini menyebabkan program mengakses memori di bagian paling bawah ruang alamat, yang secara konvensional disediakan dan menyebabkan jebakan. Hasil: Anda mengalami kerusakan.
Para penyusun sepenuhnya dalam hak mereka untuk tidak mendiagnosis kesalahan ini, karena C ++ 11 §3.5 / 10 mengatakan, tentang persyaratan jenis yang kompatibel untuk deklarasi,
[N3290 §3.5 / 10]
Pelanggaran aturan ini tentang identitas tipe tidak memerlukan diagnostik.
Paragraf yang sama merinci variasi yang diizinkan:
... deklarasi untuk objek array dapat menentukan tipe array yang berbeda dengan ada atau tidak adanya batasan array utama (8.3.4).
Variasi yang dibolehkan ini tidak termasuk mendeklarasikan nama sebagai array dalam satu unit terjemahan, dan sebagai penunjuk dalam unit terjemahan lain.
5.2 Jebakan: Melakukan optimasi prematur ( memset
& teman).
Belum ditulis
5.3 Pitfall: Menggunakan idiom C untuk mendapatkan sejumlah elemen.
Dengan pengalaman C yang dalam, wajar untuk menulis ...
#define N_ITEMS( array ) (sizeof( array )/sizeof( array[0] ))
Karena array
peluruhan untuk menunjuk ke elemen pertama di mana diperlukan, ekspresi sizeof(a)/sizeof(a[0])
juga dapat ditulis sebagai
sizeof(a)/sizeof(*a)
. Artinya sama, dan tidak peduli bagaimana ditulisnya, ini adalah ungkapan C untuk menemukan elemen-elemen bilangan array.
Perangkap utama: idiom C tidak aman untuk huruf. Misalnya, kode ...
#include <stdio.h>
#define N_ITEMS( array ) (sizeof( array )/sizeof( *array ))
void display( int const a[7] )
{
int const n = N_ITEMS( a ); // Oops.
printf( "%d elements.\n", n );
}
int main()
{
int const moohaha[] = {1, 2, 3, 4, 5, 6, 7};
printf( "%d elements, calling display...\n", N_ITEMS( moohaha ) );
display( moohaha );
}
meneruskan pointer ke N_ITEMS
, dan karena itu kemungkinan besar menghasilkan hasil yang salah. Dikompilasi sebagai executable 32-bit pada Windows 7 yang dihasilkannya ...
7 elemen, tampilan panggilan ...
1 elemen.
- Kompiler menulis ulang
int const a[7]
menjadi adil int const a[]
.
- Kompiler menulis ulang
int const a[]
untuk int const* a
.
N_ITEMS
Oleh karena itu dipanggil dengan pointer.
- Untuk 32-bit yang dapat dieksekusi
sizeof(array)
(ukuran pointer) maka 4.
sizeof(*array)
setara dengan sizeof(int)
, yang untuk executable 32-bit juga 4.
Untuk mendeteksi kesalahan ini pada saat dijalankan Anda dapat melakukan ...
#include <assert.h>
#include <typeinfo>
#define N_ITEMS( array ) ( \
assert(( \
"N_ITEMS requires an actual array as argument", \
typeid( array ) != typeid( &*array ) \
)), \
sizeof( array )/sizeof( *array ) \
)
7 elemen, memanggil tampilan ...
Pernyataan gagal: ("N_ITEMS membutuhkan array aktual sebagai argumen", typeid (a)! = Typeid (& * a)), file runtime_detect ion.cpp, line 16
Aplikasi ini telah meminta Runtime untuk menghentikannya dengan cara yang tidak biasa.
Silakan hubungi tim dukungan aplikasi untuk informasi lebih lanjut.
Deteksi kesalahan runtime lebih baik daripada tidak ada deteksi, tetapi membuang-buang sedikit waktu prosesor, dan mungkin lebih banyak waktu programmer. Lebih baik dengan deteksi pada waktu kompilasi! Dan jika Anda senang tidak mendukung array tipe lokal dengan C ++ 98, maka Anda dapat melakukannya:
#include <stddef.h>
typedef ptrdiff_t Size;
template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }
#define N_ITEMS( array ) n_items( array )
Kompilasi definisi ini diganti dengan program lengkap pertama, dengan g ++, saya dapat…
M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: Dalam fungsi 'void display (const int *)':
compile_time_detection.cpp: 14: error: tidak ada fungsi yang cocok untuk panggilan ke 'n_items (const int * &)'
M: \ hitung> _
Cara kerjanya: array dilewatkan dengan referensi untuk n_items
, dan sehingga tidak membusuk untuk pointer ke elemen pertama, dan fungsi hanya bisa mengembalikan jumlah elemen yang ditentukan oleh jenis.
Dengan C ++ 11 Anda dapat menggunakan ini juga untuk larik tipe lokal, dan ini adalah tipe aman
C ++ idiom untuk menemukan jumlah elemen array.
5.4 C ++ 11 & C ++ 14 pitfall: Menggunakan constexpr
fungsi ukuran array.
Dengan C ++ 11 dan kemudian itu wajar, tetapi karena Anda akan melihat berbahaya !, untuk menggantikan fungsi C ++ 03
typedef ptrdiff_t Size;
template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }
dengan
using Size = ptrdiff_t;
template< class Type, Size n >
constexpr auto n_items( Type (&)[n] ) -> Size { return n; }
di mana perubahan signifikan adalah penggunaan constexpr
, yang memungkinkan fungsi ini menghasilkan konstanta waktu kompilasi .
Misalnya, berbeda dengan fungsi C ++ 03, konstanta waktu kompilasi seperti itu dapat digunakan untuk mendeklarasikan array dengan ukuran yang sama dengan yang lain:
// Example 1
void foo()
{
int const x[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
constexpr Size n = n_items( x );
int y[n] = {};
// Using y here.
}
Tetapi pertimbangkan kode ini menggunakan constexpr
versi:
// Example 2
template< class Collection >
void foo( Collection const& c )
{
constexpr int n = n_items( c ); // Not in C++14!
// Use c here
}
auto main() -> int
{
int x[42];
foo( x );
}
Perangkap: pada Juli 2015 di atas mengkompilasi dengan MinGW-64 5.1.0 dengan
-pedantic-errors
, dan, pengujian dengan kompiler online di gcc.godbolt.org/ , juga dengan dentang 3.0 dan dentang 3.2, tetapi tidak dengan dentang 3.3, 3.4. 1, 3.5.0, 3.5.1, 3.6 (rc1) atau 3.7 (percobaan). Dan penting untuk platform Windows, itu tidak dikompilasi dengan Visual C ++ 2015. Alasannya adalah pernyataan C ++ 11 / C ++ 14 tentang penggunaan referensi dalam constexpr
ekspresi:
C ++ 11 C ++ 14 $ 5,19 / 2 dasbor
ke sembilan
Sebuah bersyarat ekspresi e
adalah konstan ekspresi inti kecuali evaluasi e
, mengikuti aturan dari mesin abstrak (1,9), akan mengevaluasi salah satu dari ungkapan berikut:
⋮
- sebuah id-ekspresi yang mengacu pada variabel atau data anggota dari tipe referensi kecuali referensi memiliki inisialisasi sebelumnya dan baik
- ini diinisialisasi dengan ekspresi konstan atau
- itu adalah anggota data non-statis dari suatu objek yang masa pakainya dimulai dalam evaluasi e;
Seseorang selalu dapat menulis lebih banyak verbose
// Example 3 -- limited
using Size = ptrdiff_t;
template< class Collection >
void foo( Collection const& c )
{
constexpr Size n = std::extent< decltype( c ) >::value;
// Use c here
}
... tapi ini gagal ketika Collection
bukan array mentah.
Untuk menangani koleksi yang bisa berupa non-array kita perlu kelebihan
n_items
fungsi, tetapi juga, untuk waktu kompilasi, kita perlu representasi waktu kompilasi dari ukuran array. Dan solusi klasik C ++ 03, yang berfungsi dengan baik juga di C ++ 11 dan C ++ 14, adalah membiarkan fungsi melaporkan hasilnya bukan sebagai nilai melainkan melalui tipe hasil fungsinya . Misalnya seperti ini:
// Example 4 - OK (not ideal, but portable and safe)
#include <array>
#include <stddef.h>
using Size = ptrdiff_t;
template< Size n >
struct Size_carrier
{
char sizer[n];
};
template< class Type, Size n >
auto static_n_items( Type (&)[n] )
-> Size_carrier<n>;
// No implementation, is used only at compile time.
template< class Type, size_t n > // size_t for g++
auto static_n_items( std::array<Type, n> const& )
-> Size_carrier<n>;
// No implementation, is used only at compile time.
#define STATIC_N_ITEMS( c ) \
static_cast<Size>( sizeof( static_n_items( c ).sizer ) )
template< class Collection >
void foo( Collection const& c )
{
constexpr Size n = STATIC_N_ITEMS( c );
// Use c here
(void) c;
}
auto main() -> int
{
int x[42];
std::array<int, 43> y;
foo( x );
foo( y );
}
Tentang pilihan jenis pengembalian untuk static_n_items
: kode ini tidak digunakan std::integral_constant
karena dengan std::integral_constant
hasilnya direpresentasikan secara langsung sebagai constexpr
nilai, memperkenalkan kembali masalah asli. Alih-alih Size_carrier
kelas satu dapat membiarkan fungsi langsung mengembalikan referensi ke array. Namun, tidak semua orang akrab dengan sintaksis itu.
Tentang penamaan: bagian dari solusi ini untuk masalah constexpr
-invalid-karena-referensi adalah membuat pilihan waktu kompilasi konstan.
Semoga oops-ada-ada-referensi-yang terlibat-dalam- constexpr
masalah Anda akan diperbaiki dengan C ++ 17, tetapi sampai saat itu makro seperti STATIC_N_ITEMS
portabilitas menghasilkan di atas, misalnya untuk dentang dan kompiler Visual C ++, tipe penahan keamanan.
Terkait: makro tidak menghormati cakupan, jadi untuk menghindari tabrakan nama, sebaiknya menggunakan awalan nama, mis MYLIB_STATIC_N_ITEMS
.