Apakah menambah pointer ke array dinamis berukuran 0 tidak terdefinisi?

34

AFAIK, meskipun kita tidak dapat membuat array memori statis berukuran 0, tetapi kita dapat melakukannya dengan yang dinamis:

int a[0]{}; // Compile-time error
int* p = new int[0]; // Is well-defined

Seperti yang sudah saya baca, pbertindak seperti elemen satu-melewati-akhir. Saya dapat mencetak alamat yang pmenunjuk.

if(p)
    cout << p << endl;
  • Meskipun saya yakin kita tidak bisa melakukan dereferensi pointer itu (elemen terakhir-terakhir) karena kita tidak bisa dengan iterator (elemen terakhir-terakhir), tetapi apa yang saya tidak yakin adalah apakah menambah pointer itu p? Apakah perilaku tidak terdefinisi (UB) seperti dengan iterator?

    p++; // UB?
Itachi Uchiwa
sumber
4
UB "... Setiap situasi lain (yaitu, upaya untuk menghasilkan pointer yang tidak menunjuk pada elemen dari array yang sama atau melewati masa lalu) meminta perilaku yang tidak terdefinisi ...." dari: en.cppreference.com / w / cpp / bahasa / operator_arithmetic
Richard Critten
3
Nah, ini mirip std::vectordengan item dengan 0 di dalamnya. begin()sudah sama dengan end()sehingga Anda tidak bisa menambah iterator yang menunjuk di awal.
Phil1970
1
@PeterMortensen Saya pikir suntingan Anda mengubah arti dari kalimat terakhir ("Apa yang saya yakin -> Saya tidak yakin mengapa"), bisakah Anda periksa ulang?
Fabio mengatakan Reinstate Monica
@PeterMortensen: Paragraf terakhir yang Anda edit telah menjadi sedikit kurang dapat dibaca.
Itachi Uchiwa

Jawaban:

32

Pointer ke elemen array diperbolehkan untuk menunjuk ke elemen yang valid, atau yang melewati akhir. Jika Anda menambah pointer dengan cara yang berjalan lebih dari satu melewati akhir, perilaku tidak terdefinisi.

Untuk array ukuran 0 Anda, psudah menunjuk satu melewati akhir, jadi penambahan itu tidak diperbolehkan.

Lihat C ++ 17 8.7 / 4 tentang +operator ( ++memiliki batasan yang sama):

f ekspresi Pmenunjuk ke elemen x[i]objek array xdengan n elemen, ekspresi P + Jdan J + P(di mana Jmemiliki nilai j) menunjuk ke elemen (mungkin-hipotetis) x[i+j]jika 0≤i + j≤n; jika tidak, perilaku tidak terdefinisi.

interjay
sumber
2
Jadi satu-satunya kasus x[i]adalah sama seperti x[i + j]ketika keduanya idan jmemiliki nilai 0?
Rami Yen
8
@RamiYen x[i]adalah elemen yang sama seperti x[i+j]jika j==0.
interjay
1
Ugh, aku benci "twilight zone" dari semantik C ++ ... +1.
einpoklum
4
@ einpoklum-reinstateMonica: Tidak ada zona senja benar-benar. Hanya saja C ++ konsisten bahkan untuk N = 0 case. Untuk larik elemen N, ada N + 1 nilai penunjuk yang valid karena Anda bisa menunjuk di belakang larik. Itu berarti Anda bisa mulai dari awal array dan menambah pointer N kali hingga akhir.
MSalters
1
@ MaximEgorushkin Jawaban saya adalah tentang apa yang bahasa saat ini memungkinkan. Diskusi tentang Anda yang ingin dibolehkannya adalah di luar topik.
interjay
2

Saya kira Anda sudah memiliki jawabannya; Jika Anda melihat sedikit lebih dalam: Anda telah mengatakan bahwa menambahkan iterator off-the-end adalah UB dengan demikian: Jawaban ini ada di dalam apa itu iterator?

Iterator hanyalah sebuah objek yang memiliki pointer dan menambahkan bahwa iterator benar-benar menambah pointer yang dimilikinya. Jadi, dalam banyak aspek, iterator ditangani dengan menggunakan pointer.

int arr [] = {0,1,2,3,4,5,6,7,8,9};

int * p = arr; // p menunjuk ke elemen pertama di arr

++ p; // p poin untuk arr [1]

Sama seperti kita dapat menggunakan iterator untuk melintasi elemen dalam vektor, kita dapat menggunakan pointer untuk melintasi elemen dalam array. Tentu saja, untuk melakukannya, kita perlu mendapatkan pointer ke elemen pertama dan terakhir melewati elemen terakhir. Seperti yang baru saja kita lihat, kita dapat memperoleh pointer ke elemen pertama dengan menggunakan array itu sendiri atau dengan mengambil alamat-elemen pertama. Kita dapat memperoleh pointer off-the-end dengan menggunakan properti khusus array lainnya. Kita dapat mengambil alamat elemen yang tidak ada satu melewati elemen terakhir dari sebuah array:

int * e = & arr [10]; // pointer baru saja melewati elemen terakhir di arr

Di sini kami menggunakan operator subskrip untuk mengindeks elemen yang tidak ada; arr memiliki sepuluh elemen, jadi elemen terakhir dalam arr adalah pada posisi indeks 9. Satu-satunya hal yang dapat kita lakukan dengan elemen ini adalah mengambil alamatnya, yang kita lakukan untuk menginisialisasi e. Seperti iterator off-the-end (§ 3.4.1, hlm. 106), pointer off-the-end tidak menunjuk ke suatu elemen. Sebagai akibatnya, kami tidak boleh melakukan dereferensi atau menambah penunjuk off-the-end.

Ini dari C ++ primer 5 edisi oleh Lipmann.

Jadi UB tidak melakukannya.

Raindrop7
sumber
-4

Dalam arti yang paling ketat, ini bukan Perilaku Tidak Terdefinisi, tetapi implementasi-didefinisikan. Jadi, meskipun tidak disarankan jika Anda berencana untuk mendukung arsitektur non-mainstream, Anda mungkin dapat melakukannya.

Kutipan standar yang diberikan oleh interjay adalah yang baik, menunjukkan UB, tetapi itu hanya hit terbaik kedua menurut saya, karena berurusan dengan pointer-pointer aritmatika (lucu, satu secara eksplisit UB, sedangkan yang lain tidak). Ada paragraf yang membahas operasi dalam pertanyaan secara langsung:

[expr.post.incr] / [expr.pre.incr]
Operand harus [...] atau sebuah penunjuk ke tipe objek yang didefinisikan secara lengkap.

Oh, tunggu sebentar, tipe objek yang didefinisikan sepenuhnya? Itu saja? Maksudku, sungguh, ketik ? Jadi, Anda tidak perlu objek sama sekali?
Dibutuhkan sedikit bacaan untuk benar-benar menemukan petunjuk bahwa sesuatu di sana mungkin tidak begitu jelas. Karena sejauh ini, sepertinya Anda benar-benar diizinkan melakukannya, tidak ada batasan.

[basic.compound] 3membuat pernyataan tentang apa jenis penunjuk yang mungkin dimiliki, dan karena tidak satu pun dari tiga penunjuk lainnya, hasil operasi Anda akan jelas berada di bawah 3.4: penunjuk tidak valid .
Namun itu tidak mengatakan bahwa Anda tidak diizinkan memiliki pointer yang tidak valid. Sebaliknya, ia mencantumkan beberapa kondisi normal yang sangat umum (misalnya, akhir durasi penyimpanan) di mana pointer secara teratur menjadi tidak valid. Jadi itu tampaknya hal yang diijinkan terjadi. Dan memang:

[basic.stc] 4
Tidak langsung melalui nilai penunjuk yang tidak valid dan meneruskan nilai penunjuk yang tidak valid ke fungsi deallokasi memiliki perilaku yang tidak terdefinisi. Penggunaan lain dari nilai pointer yang tidak valid memiliki perilaku yang ditentukan implementasi.

Kami melakukan "yang lain" di sana, jadi itu bukan Perilaku Tidak Terdefinisi, tetapi ditentukan oleh implementasi, dengan demikian umumnya diperbolehkan (kecuali jika implementasi secara eksplisit mengatakan sesuatu yang berbeda).

Sayangnya, itu bukan akhir dari cerita. Meskipun hasil bersih tidak berubah lagi dari sini, itu semakin membingungkan, semakin lama Anda mencari "pointer":

[basic.compound]
Nilai valid dari tipe pointer objek mewakili alamat byte dalam memori atau null pointer. Jika objek tipe T terletak di alamat A [...] dikatakan menunjuk ke objek itu, terlepas dari bagaimana nilai itu diperoleh .
[Catatan: Misalnya, alamat yang melewati ujung array akan dianggap menunjuk ke objek yang tidak terkait dari jenis elemen array yang mungkin terletak di alamat itu. [...]].

Baca sebagai: Oke, siapa peduli! Selama pointer menunjuk suatu tempat di memori , saya baik-baik saja?

[basic.stc.dynamic.safety] Nilai penunjuk adalah penunjuk yang diturunkan dengan aman [blah blah]

Baca sebagai: OK, diturunkan dengan aman, apa pun. Itu tidak menjelaskan apa ini, juga tidak mengatakan saya benar-benar membutuhkannya. Yang diturunkan dengan aman. Rupanya saya masih bisa memiliki pointer yang tidak aman dengan baik. Saya menduga bahwa melakukan dereferensi pada mereka mungkin bukan ide yang bagus, tetapi sangat memungkinkan untuk memilikinya. Itu tidak mengatakan sebaliknya.

Suatu implementasi mungkin memiliki keselamatan pointer yang santai, dalam hal ini validitas nilai pointer tidak tergantung pada apakah itu adalah nilai pointer yang diturunkan dengan aman.

Oh, jadi mungkin tidak masalah, hanya apa yang saya pikirkan. Tapi tunggu ... "mungkin tidak"? Itu artinya, mungkin juga . Bagaimana aku tahu?

Sebagai alternatif, suatu implementasi mungkin memiliki keamanan pointer yang ketat, dalam hal ini nilai pointer yang bukan nilai pointer yang diturunkan dengan aman adalah nilai pointer yang tidak valid kecuali objek lengkap yang dirujuk memiliki durasi penyimpanan dinamis dan sebelumnya telah dinyatakan dapat dijangkau

Tunggu, jadi mungkin saja saya perlu memanggil declare_reachable()setiap pointer? Bagaimana aku tahu?

Sekarang, Anda dapat mengonversi ke intptr_t, yang terdefinisi dengan baik, memberikan representasi integer dari pointer yang diturunkan dengan aman. Untuk yang, tentu saja, sebagai bilangan bulat, itu sah dan terdefinisi dengan baik untuk menambahkannya sesuka Anda.
Dan ya, Anda dapat mengonversi intptr_tkembali ke sebuah pointer, yang juga terdefinisi dengan baik. Hanya saja, tidak menjadi nilai asli, itu tidak lagi dijamin bahwa Anda memiliki pointer yang diturunkan dengan aman (jelas). Namun, secara keseluruhan, untuk surat standar, sementara sedang didefinisikan implementasi, ini adalah hal yang 100% sah untuk dilakukan:

[expr.reinterpret.cast] 5
Nilai tipe integral atau tipe enumerasi dapat secara eksplisit dikonversi ke pointer. Pointer dikonversi menjadi integer dengan ukuran yang cukup [...] dan kembali ke tipe pointer yang sama [...] nilai asli; pemetaan antara pointer dan integer jika tidak ditentukan implementasi.

Tangkapan

Pointer hanyalah bilangan bulat biasa, hanya Anda yang menggunakannya sebagai pointer. Oh andai saja itu benar!
Sayangnya, ada arsitektur di mana itu tidak benar sama sekali, dan hanya menghasilkan pointer yang tidak valid (tidak men-dereferensinya, hanya dengan memasukkannya ke register pointer) akan menyebabkan jebakan.

Jadi itulah dasar "implementasi didefinisikan". Itu, dan fakta bahwa incrementing pointer setiap kali Anda inginkan, tolong bisa saja menyebabkan overflow, yang standar tidak ingin berurusan dengan. Akhir ruang alamat aplikasi mungkin tidak sesuai dengan lokasi overflow, dan Anda bahkan tidak tahu apakah ada yang namanya overflow untuk pointer pada arsitektur tertentu. Semua dalam semua itu berantakan mimpi buruk tidak dalam kaitannya dengan manfaat yang mungkin.

Berhubungan dengan kondisi objek satu-lampau di sisi lain, mudah: Implementasinya harus memastikan tidak ada objek yang pernah dialokasikan sehingga byte terakhir di ruang alamat ditempati. Jadi itu didefinisikan dengan baik karena berguna dan mudah untuk dijamin.

Damon
sumber
1
Logika Anda cacat. "Jadi kamu sama sekali tidak butuh benda?" salah mengartikan Standar dengan berfokus pada satu aturan. Aturan itu adalah tentang waktu kompilasi, apakah program Anda terbentuk dengan baik. Ada aturan lain tentang run time. Hanya pada saat dijalankan Anda dapat benar-benar berbicara tentang keberadaan objek di alamat tertentu. program Anda harus memenuhi semua aturan; aturan waktu kompilasi pada waktu kompilasi dan aturan run-time pada saat run time.
MSalters
5
Anda memiliki kelemahan logika yang sama dengan "OK, siapa yang peduli! Selama pointer menunjuk ke suatu tempat di memori, saya baik-baik saja?". Tidak. Anda harus mengikuti semua aturan. Bahasa yang sulit tentang "akhir dari satu array menjadi awal dari array lain" hanya memberikan izin implementasi untuk mengalokasikan memori secara bersamaan; tidak perlu menjaga ruang kosong di antara alokasi. Itu berarti kode Anda mungkin memiliki nilai A yang sama sebagai akhir dari satu objek array dan awal yang lain.
MSalters
1
"Jebakan" bukanlah sesuatu yang dapat dijelaskan dengan perilaku "implementasi yang ditentukan". Perhatikan bahwa interjay telah menemukan batasan pada +operator (dari mana ++mengalir) yang berarti bahwa menunjuk setelah "satu-setelah-akhir" tidak didefinisikan.
Martin Bonner mendukung Monica
1
@PeterCordes: Harap baca basic.stc, paragraf 4 . Ia mengatakan "Indirection [...] perilaku tidak terdefinisi. Penggunaan lain dari nilai pointer tidak valid memiliki perilaku implementasi-didefinisikan " . Saya tidak membingungkan orang dengan menggunakan istilah itu untuk makna lain. Ini adalah kata-kata yang tepat. Itu bukan perilaku yang tidak terdefinisi.
Damon
2
Hampir tidak mungkin Anda telah menemukan celah untuk kenaikan pasca tetapi Anda tidak mengutip bagian lengkap tentang apa yang dilakukan pasca kenaikan. Saya tidak akan melihat itu sendiri sekarang. Setuju bahwa jika ada, itu tidak disengaja. Lagi pula, sama baiknya jika ISO C ++ mendefinisikan lebih banyak hal untuk model memori datar, @ MaximEgorushkin, ada alasan lain (seperti pointer wrap-around) untuk tidak mengizinkan hal-hal yang sewenang-wenang. Lihat komentar pada Haruskah perbandingan pointer ditandatangani atau tidak ditandatangani dalam 64-bit x86?
Peter Cordes