Por que este ponteiro é nulo
No Visual Studio, parece que o ponteiro para as variáveis de membro são inteiros assinados de 32 bits nos bastidores (mesmo no modo de 64 bits) e um ponteiro nulo é -1 nesse contexto. Então, se eu tiver uma aula como:
#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';
}
Ele compila e imprime "null". Então, estou causando algum tipo de comportamento indefinido ou o compilador deveria rejeitar este código e isso é um bug no compilador?
Editar:
Parece que posso manter o INT_MAXpadrão "2 arrays mais 2 chars" e somente nesse caso o compilador me permite adicionar quantos membros desejar e o segundo caractere é sempre considerado nulo. Veja a demonstração . Se eu mudar o padrão ligeiramente (como 1 ou 3 caracteres em vez de 2 em algum ponto), ele reclama que a classe é muito grande.
Respostas
O limite de tamanho de um objeto é definido pela implementação, conforme o Anexo B da norma [1]. Sua estrutura é de um tamanho absurdo.
Se a estrutura for:
struct Foo
{
char arr1[INT_MAX];
//char arr2[INT_MAX];
char ch1;
char ch2;
};
... o tamanho de sua estrutura em uma versão relativamente recente do MSVC de 64 bits parece estar em torno de 2147483649 bytes. Se você adicionar arr2, de repente sizeof dirá que Foo tem tamanho 1.
O padrão C ++ (Anexo B) afirma que o compilador deve documentar as limitações, o que o MSVC faz [2]. Afirma que segue o limite recomendado. O Anexo B, Seção 2.17 fornece um limite recomendado de 262144 (?) Para o tamanho de um objeto. Embora esteja claro que o MSVC pode lidar com mais do que isso, ele documenta que segue essa recomendação mínima, portanto, presumo que você deve tomar cuidado quando o tamanho do objeto for maior do que isso.
[1] http://eel.is/c++draft/implimits
[2] https://docs.microsoft.com/en-us/cpp/cpp/compiler-limits?view=vs-2019
É claramente uma colisão entre uma otimização na representação de ponteiro para membro (use apenas 4 bytes de armazenamento quando nenhuma base virtual estiver presente) e o princípio do escaninho.
Para um tipo que Xcontém Nsubobjetos do tipo char, existem N+1possíveis ponteiros para membros válidos do tipo char X::*... um para cada subobjeto e um para ponteiro nulo para membro.
Isso funciona quando há pelo menos N + 1 valores distintos na representação de ponteiro para membro, o que para uma representação de 4 bytes implica que N+1<= 2 32 e, portanto, o tamanho máximo do objeto é 2 32 - 1.
Infelizmente, o compilador em questão tornou o tamanho máximo do tipo de objeto (antes de rejeitar o programa) igual a 2 32, que é muito grande e cria um problema de escaninho - pelo menos um par de ponteiros para membros deve ser indistinguível. Não é necessário que o ponteiro-a-membro nulo seja metade desse par, mas como você observou nesta implementação, é.
A expressão &Foo::ch2é do tipo char Foo::*, que é um ponteiro para um membro da classe Foo. Por regras, um ponteiro para o membro convertido booldeve ser avaliado como falso SOMENTE se for um ponteiro nulo, ou seja, se tiver nullptratribuído a ele.
A falha aqui parece ser uma falha de implementação. ou seja, em compiladores gcc com -march = x86-64 qualquer ponteiro atribuído ao membro é avaliado como não nulo (1), a menos que tenha nullptr atribuído a ele com o seguinte código:
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';
}
Resultado:
Not null
null
-2
18446744073709551615
18446744073709551614
Provavelmente está relacionado ao fato de que o tamanho da classe está excedendo as limitações da plataforma, levando ao deslocamento do membro sendo agrupado em torno de 0 (valor interno de nullptr). O compilador não detecta porque ele se torna uma vítima de ... estouro de inteiro com valor assinado e é falha do programador causar UB dentro do compilador usando literais assinados como tamanho de array: LLONG_MAX + LLONG_MAX = -2 seria "tamanho" de dois matrizes combinadas.
Essencialmente, o tamanho dos dois primeiros membros é calculado como negativo e o deslocamento de ch1 é -2representado como não assinado 18446744073709551614. E, -2portanto, o ponteiro não é nulo. Outro compilador pode fixar o valor a 0 produzindo um nullptr, ou realmente detectar o problema existente como o clang faz.
Se o deslocamento de ch1for -2, então o deslocamento de ch2é -1? Vamos adicionar isto:
std::cout << reinterpret_cast<signed long long&&> (offsetof(foo, ch1)) << '\n';
std::cout << reinterpret_cast<signed long long&&> (offsetof(foo, ch2)) << '\n';
Resultado adicional:
-2
-1
E o deslocamento para o primeiro membro é obviamente 0 e se o ponteiro representar os deslocamentos, ele precisará de outro valor para representar nullptr. é lógico supor que esse compilador específico considera apenas -1um valor nulo, o que pode ou não ser o caso para outras implementações.
Quando eu testo o código, o VS mostra isso Foo: the class is too large.
Quando eu adicionar char arr3[INT_MAX], o Visual Studio reportará Error C2089 'Foo': 'struct' too large. Microsoft Docs explica comoThe specified structure or union exceeds the 4GB limit.