Perché questo puntatore è nullo
In Visual Studio, sembra che il puntatore alle variabili membro siano interi con segno a 32 bit dietro le quinte (anche in modalità 64 bit) e un puntatore null è -1 in quel contesto. Quindi se ho una classe come:
#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';
}
Compila e stampa "null". Quindi, sto causando un qualche tipo di comportamento indefinito, o il compilatore avrebbe dovuto rifiutare questo codice e questo è un bug nel compilatore?
Modificare:
Sembra che posso mantenere il INT_MAXpattern "2 array più 2 caratteri" e solo in questo caso il compilatore mi permette di aggiungere tutti i membri che desidero e il secondo carattere è sempre considerato nullo. Guarda la demo . Se ho cambiato leggermente il pattern (come 1 o 3 caratteri invece di 2 ad un certo punto) si lamenta che la classe è troppo grande.
Risposte
Il limite di dimensione di un oggetto è definito dall'implementazione, in base all'Allegato B della norma [1]. La tua struttura è di dimensioni assurde.
Se la struttura è:
struct Foo
{
char arr1[INT_MAX];
//char arr2[INT_MAX];
char ch1;
char ch2;
};
... la dimensione della tua struttura in una versione relativamente recente di MSVC a 64 bit sembra essere di circa 2147483649 byte. Se poi aggiungi arr2, improvvisamente sizeof ti dirà che Foo è di taglia 1.
Lo standard C ++ (allegato B) afferma che il compilatore deve documentare le limitazioni, cosa che MSVC fa [2]. Afferma che segue il limite consigliato. L'allegato B, sezione 2.17 fornisce un limite consigliato di 262144 (?) Per le dimensioni di un oggetto. Sebbene sia chiaro che MSVC può gestire più di questo, documenta che segue quella raccomandazione minima, quindi presumo che dovresti fare attenzione quando la dimensione del tuo oggetto è superiore a quella.
[1] http://eel.is/c++draft/implimits
[2] https://docs.microsoft.com/en-us/cpp/cpp/compiler-limits?view=vs-2019
È chiaramente una collisione tra un'ottimizzazione sulla rappresentazione pointer-to-member (usa solo 4 byte di memoria quando non sono presenti basi virtuali) e il principio della casella.
Per un tipo Xcontenente Nsuboggetti di tipo char, ci sono N+1possibili puntatori validi a membri di tipo char X::*... uno per ogni oggetto secondario e uno per puntatore null a membro.
Questo funziona quando ci sono almeno N + 1 valori distinti nella rappresentazione del puntatore a membro, che per una rappresentazione a 4 byte implica che N+1<= 2 32 e quindi la dimensione massima dell'oggetto è 2 32 - 1.
Sfortunatamente il compilatore in questione ha reso la dimensione massima del tipo di oggetto (prima di rifiutare il programma) uguale a 2 32 che è troppo grande e crea un problema di casella - almeno una coppia di puntatori a membri deve essere indistinguibile. Non è necessario che il puntatore a membro nullo sia la metà di questa coppia, ma come hai osservato in questa implementazione lo è.
L'espressione &Foo::ch2è di tipo char Foo::*, che è un puntatore a un membro della classe Foo. Secondo le regole, un puntatore al membro convertito in booldovrebbe essere valutato come falso SOLO se è un puntatore nullo, cioè se gli è stato nullptrassegnato.
L'errore qui sembra essere un difetto di implementazione. cioè sui compilatori gcc con -march = x86-64 qualsiasi puntatore assegnato al membro restituisce non-null (1) a meno che non gli sia stato assegnato nullptr con il seguente codice:
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';
}
Produzione:
Not null
null
-2
18446744073709551615
18446744073709551614
Probabilmente è correlato al fatto che la dimensione della classe supera i limiti della piattaforma, portando a un offset del membro attorno a 0 (valore interno di nullptr). Il compilatore non lo rileva perché diventa vittima di ... integer overflow con valore con segno ed è colpa del programmatore causare UB all'interno del compilatore utilizzando letterali con segno come dimensione dell'array: LLONG_MAX + LLONG_MAX = -2 sarebbe "size" di due array combinati.
Essenzialmente la dimensione dei primi due membri è calcolata come negativa e l'offset di ch1 è -2rappresentato come non firmato 18446744073709551614. E -2quindi il puntatore non è nullo. Un altro compilatore può bloccare il valore a 0 producendo un nullptr o rilevare effettivamente un problema esistente come fa clang.
Se offset di ch1è -2, allora offset di ch2è -1? Aggiungiamo questo:
std::cout << reinterpret_cast<signed long long&&> (offsetof(foo, ch1)) << '\n';
std::cout << reinterpret_cast<signed long long&&> (offsetof(foo, ch2)) << '\n';
Uscita aggiuntiva:
-2
-1
E l'offset per il primo membro è ovviamente 0 e se il puntatore rappresenta gli offset, allora ha bisogno di un altro valore per rappresentare nullptr. è logico presumere che questo particolare compilatore consideri solo -1un valore nullo, che può essere o meno il caso per altre implementazioni.
Quando provo il codice, VS lo mostra Foo: the class is too large.
Quando aggiungo char arr3[INT_MAX], Visual Studio riporterà Error C2089 'Foo': 'struct' too large. Microsoft Docs lo spiega comeThe specified structure or union exceeds the 4GB limit.