Mengapa penunjuk ini null

Oct 01 2020

Dalam Visual Studio, sepertinya penunjuk ke variabel anggota adalah bilangan bulat bertanda 32 bit di belakang layar (bahkan dalam mode 64 bit), dan penunjuk null adalah -1 dalam konteks itu. Jadi kalau saya punya kelas seperti:

#include <iostream>
#include <cstdint>

struct Foo
{
    char arr1[INT_MAX];
    char arr2[INT_MAX];
    char ch1;
    char ch2;
};


int main()
{
    auto p = &Foo::ch2;
    std::cout << (p?"Not null":"null") << '\n';
}

Ini mengkompilasi, dan mencetak "null". Jadi, apakah saya menyebabkan semacam perilaku tidak terdefinisi, atau apakah kompilator seharusnya menolak kode ini dan ini merupakan bug di kompilator?

Edit:

Tampaknya saya dapat mempertahankan pola "2 INT_MAXlarik ditambah 2 karakter" dan hanya dalam hal ini kompilator mengizinkan saya untuk menambahkan anggota sebanyak yang saya inginkan dan karakter kedua selalu dianggap nol. Lihat demo . Jika saya mengubah polanya sedikit (seperti 1 atau 3 karakter daripada 2 di beberapa titik) mengeluh bahwa kelasnya terlalu besar.

Jawaban

8 computerquip Oct 08 2020 at 08:52

Batas ukuran suatu objek ditentukan oleh implementasi, sesuai dengan Lampiran B dari standar [1]. Ukuran struct Anda tidak masuk akal.

Jika struct adalah:

struct Foo
{
    char arr1[INT_MAX];
    //char arr2[INT_MAX];
    char ch1;
    char ch2;
};

... ukuran struct Anda dalam versi yang relatif baru dari MSVC 64-bit tampaknya sekitar 2147483649 byte. Jika Anda kemudian menambahkan arr2, tiba-tiba sizeof akan memberitahu Anda bahwa Foo berukuran 1.

Standar C ++ (Lampiran B) menyatakan bahwa kompilator harus mendokumentasikan batasan, yang dilakukan oleh MSVC [2]. Ini menyatakan bahwa itu mengikuti batas yang disarankan. Lampiran B, Bagian 2.17 memberikan batas yang direkomendasikan sebesar 262144 (?) Untuk ukuran suatu objek. Meskipun jelas bahwa MSVC dapat menangani lebih dari itu, ia mendokumentasikan bahwa itu mengikuti rekomendasi minimum sehingga saya berasumsi Anda harus berhati-hati ketika ukuran objek Anda lebih dari itu.

[1] http://eel.is/c++draft/implimits

[2] https://docs.microsoft.com/en-us/cpp/cpp/compiler-limits?view=vs-2019

3 BenVoigt Oct 08 2020 at 23:20

Ini jelas merupakan tabrakan antara pengoptimalan pada representasi pointer-ke-anggota (gunakan hanya penyimpanan 4 byte ketika tidak ada basis virtual) dan prinsip pigeonhole.

Untuk tipe yang Xberisi Nsubobjek tipe char, ada N+1kemungkinan tipe pointer-to-members yang valid char X::*... satu untuk setiap subobjek, dan satu untuk null-pointer-to-member.

Ini berfungsi ketika ada setidaknya N + 1 nilai berbeda dalam representasi pointer-ke-anggota, yang untuk representasi 4-byte menyiratkan bahwa N+1<= 2 32 dan oleh karena itu ukuran objek maksimum adalah 2 32-1 .

Sayangnya kompilator tersebut membuat ukuran tipe objek maksimum (sebelum menolak program) sama dengan 2 32 yang terlalu besar dan menciptakan masalah pigeonhole - setidaknya satu pasang pointer-ke-anggota harus tidak dapat dibedakan. Tidak perlu bahwa null pointer-to-member menjadi setengah dari pasangan ini, tetapi seperti yang Anda amati dalam implementasi ini.

1 Swift-FridayPie Oct 08 2020 at 15:44

Ekspresi &Foo::ch2adalah tipe char Foo::*, yang merupakan penunjuk ke anggota kelas Foo. Berdasarkan aturan, pointer ke anggota yang diubah menjadi boolharus dievaluasi sebagai salah HANYA jika itu adalah pointer nol, yaitu telah nullptrditugaskan padanya.

Kesalahan di sini tampaknya merupakan kesalahan implementasi. yaitu pada kompiler gcc dengan -march = x86-64, penunjuk apa pun yang ditugaskan ke anggota mengevaluasi ke non-null (1) kecuali jika nullptr ditugaskan padanya dengan kode berikut:

struct foo
{
    char arr1[LLONG_MAX];
    char arr2[LLONG_MAX];
    char ch1;
    char ch2;
};

int main()
{
    char  foo::* p1 = &foo::ch1;
    char  foo::* p2 = &foo::ch2;
    std::cout << (p1?"Not null ":"null ") << '\n';
    std::cout << (p2?"Not null ":"null ") << '\n';
    
    std::cout << LLONG_MAX + LLONG_MAX << '\n';
    std::cout << ULLONG_MAX << '\n';
    std::cout << offsetof(foo, ch1) << '\n';
}

Keluaran:

Not null 
null 
-2
18446744073709551615
18446744073709551614

Kemungkinan besar ini terkait dengan fakta bahwa ukuran kelas melebihi batasan platform, yang menyebabkan offset anggota berada di sekitar 0 (nilai internal nullptr). Compiler tidak mendeteksinya karena menjadi korban dari ... integer overflow dengan nilai yang ditandatangani dan kesalahan pemrogram menyebabkan UB dalam kompiler dengan menggunakan literal bertanda tangan sebagai ukuran array: LLONG_MAX + LLONG_MAX = -2 akan menjadi "ukuran" dari dua array digabungkan.

Pada dasarnya ukuran dua anggota pertama dihitung sebagai negatif dan offset ch1 -2direpresentasikan sebagai unsigned 18446744073709551614. Dan -2oleh karena itu pointer tidak null. Kompiler lain mungkin menjepit nilai ke 0 menghasilkan nullptr, atau sebenarnya mendeteksi masalah yang ada seperti halnya clang.

Jika offset ch1adalah -2, maka offset ch2adalah -1? Mari tambahkan ini:

std::cout << reinterpret_cast<signed long long&&> (offsetof(foo, ch1)) << '\n';
std::cout << reinterpret_cast<signed long long&&> (offsetof(foo, ch2)) << '\n';

Output tambahan:

-2
-1

Dan offset untuk anggota pertama jelas 0 dan jika pointer mewakili offset, maka diperlukan nilai lain untuk mewakili nullptr. logis untuk mengasumsikan bahwa kompilator khusus ini menganggap hanya -1sebagai nilai null, yang mungkin atau mungkin tidak kasus untuk implementasi lain.

BarrnetChou Oct 01 2020 at 09:29

Ketika saya menguji kodenya, VS menunjukkan itu Foo: the class is too large.

Ketika saya menambahkan char arr3[INT_MAX], Visual Studio akan melaporkan Error C2089 'Foo': 'struct' too large. Microsoft Docs menjelaskannya sebagaiThe specified structure or union exceeds the 4GB limit.