c++ - not - nullptr vs null
¿Un puntero con la dirección y el tipo correctos sigue siendo siempre un puntero válido desde C++ 17? (3)
¿Es esta interpretación de esta modificación del derecho estándar o hay otras reglas que compensan la eliminación de esta oración citada?
Sí, esta interpretación es correcta. Un puntero más allá del final no es simplemente convertible a otro valor de puntero que apunte a esa dirección.
El nuevo [basic.compound]/3 dice:
Cada valor del tipo de puntero es uno de los siguientes:
(3.1) un puntero a un objeto o función (se dice que el puntero apunta al objeto o función), o
(3.2) un puntero más allá del final de un objeto ([expr.add]), o
Esos son mutuamente excluyentes.
p1+1
es un puntero más allá del final, no un puntero a un objeto.
p1+1
apunta a una hipotética
x[1]
de una matriz de tamaño 1 en
p1
, no a
p2
.
Esos dos objetos no son punteros interconvertibles.
También tenemos la nota no normativa:
[Nota: No se considera que un puntero pasado el final de un objeto ([expr.add]) apunte a un objeto no relacionado del tipo de objeto que podría estar ubicado en esa dirección. [...]
lo que aclara la intención.
Como TC señala en numerosos comentarios (
especialmente este
), este es realmente un caso especial del problema que surge al tratar de implementar
std::vector
, que es que
[v.data(), v.data() + v.size())
debe ser un rango válido y, sin embargo, el
vector
no crea un objeto de matriz, por lo que la única aritmética de puntero definida iría desde cualquier objeto dado en el vector hasta el final de su hipotético tamaño único formación.
Para obtener más recursos, consulte
CWG 2182
,
esta discusión
P0593R0
y dos revisiones de un documento sobre el tema:
P0593R0
y
P0593R1
(sección 1.3 específicamente).
(En referencia a esta pregunta y respuesta ).
Antes del estándar C ++ 17, la siguiente oración se incluía en [basic.compound]/3 :
Si un objeto de tipo T se encuentra en una dirección A, se dice que un puntero de tipo cv T * cuyo valor es la dirección A apunta a ese objeto, independientemente de cómo se obtuvo el valor.
Pero desde C ++ 17, esta oración ha sido removed .
Por ejemplo, creo que esta oración definió este código de ejemplo, y que desde C ++ 17 este es un comportamiento indefinido:
alignas(int) unsigned char buffer[2*sizeof(int)];
auto p1=new(buffer) int{};
auto p2=new(p1+1) int{};
*(p1+1)=10;
Antes de C ++ 17,
p1+1
mantiene la dirección en
*p2
y tiene el tipo correcto, por lo que
*(p1+1)
es un puntero a
*p2
.
En C ++ 17,
p1+1
es un
puntero pasado-fin
, por lo que no es un
puntero para objetar
y creo que no es descartable.
¿Es esta interpretación de esta modificación del derecho estándar o hay otras reglas que compensan la eliminación de la oración citada?
En su ejemplo,
*(p1 + 1) = 10;
debería ser UB, porque es
uno más allá del final de la matriz
de tamaño 1. Pero estamos en un caso muy especial aquí, porque la matriz se construyó dinámicamente en una matriz de caracteres más grande.
La creación dinámica de objetos se describe en 4.5 El modelo de objetos C ++ [intro.object] , §3 del borrador n4659 del estándar C ++:
3 Si se crea un objeto completo (8.3.4) en el almacenamiento asociado con otro objeto e del tipo "matriz de N caracteres no firmados" o del tipo "matriz de N std :: byte" (21.2.1), esa matriz proporciona almacenamiento para el objeto creado si:
(3.1) - la vida útil de e ha comenzado y no ha terminado, y
(3.2) - el almacenamiento para el nuevo objeto se ajusta completamente dentro de e, y
(3.3): no hay ningún objeto de matriz más pequeño que satisfaga estas restricciones.
El 3.3 parece bastante poco claro, pero los ejemplos a continuación aclaran la intención:
struct A { unsigned char a[32]; }; struct B { unsigned char b[16]; }; A a; B *b = new (a.a + 8) B; // a.a provides storage for *b int *p = new (b->b + 4) int; // b->b provides storage for *p // a.a does not provide storage for *p (directly), // but *p is nested within a (see below)
Entonces, en el ejemplo, la matriz de
buffer
proporciona almacenamiento
para
*p1
y
*p2
.
Los siguientes párrafos prueban que el objeto completo para
*p1
y
*p2
es el
buffer
:
4 Un objeto a está anidado dentro de otro objeto b si:
(4.1) - a es un subobjeto de b, o
(4.2) - b proporciona almacenamiento para a, o
(4.3) - existe un objeto c donde a está anidado dentro de c, y c está anidado dentro de b.5 Para cada objeto x, hay algún objeto llamado objeto completo de x, determinado de la siguiente manera:
(5.1) - Si x es un objeto completo, entonces el objeto completo de x es él mismo.
(5.2) - De lo contrario, el objeto completo de x es el objeto completo del objeto (único) que contiene x.
Una vez que esto se establece, la otra parte relevante del borrador n4659 para C ++ 17 es [basic.coumpound] §3 (enfatice el mío):
3 ... Cada valor del tipo de puntero es uno de los siguientes:
(3.1) - un puntero a un objeto o función (se dice que el puntero apunta al objeto o función), o
(3.2) - un puntero más allá del final de un objeto (8.7), o
(3.3) - el valor de puntero nulo (7.11) para ese tipo, o
(3.4): un valor de puntero no válido.Un valor de un tipo de puntero que es un puntero hacia o más allá del final de un objeto representa la dirección del primer byte en la memoria (4.4) ocupada por el objeto o el primer byte en la memoria después del final del almacenamiento ocupado por el objeto , respectivamente. [Nota: No se considera que un puntero pasado el final de un objeto (8.7) apunta a un objeto no relacionado del tipo de objeto que podría estar ubicado en esa dirección. Un valor de puntero deja de ser válido cuando el almacenamiento que indica alcanza el final de su duración de almacenamiento; ver 6.7. —Nota final] Para propósitos de aritmética de puntero (8.7) y comparación (8.9, 8.10), un puntero más allá del final del último elemento de una matriz x de n elementos se considera equivalente a un puntero a un elemento hipotético x [ norte]. La representación del valor de los tipos de puntero está definida por la implementación. Los punteros a tipos compatibles con el diseño deben tener los mismos requisitos de representación y alineación de valores (6.11) ...
La nota
Un puntero más allá del final ...
no se aplica aquí porque los objetos señalados por
p1
y
p2
y no están
relacionados
, pero están anidados en el mismo objeto completo, por lo que la aritmética del puntero tiene sentido dentro del objeto que proporciona almacenamiento:
p2 - p1
está definido y es
(&buffer[sizeof(int)] - buffer]) / sizeof(int)
que es 1.
Entonces
p1 + 1
es
un puntero a
*p2
, y
*(p1 + 1) = 10;
tiene un comportamiento definido y establece el valor de
*p2
.
También he leído el anexo C4 sobre la compatibilidad entre C ++ 14 y los estándares actuales (C ++ 17). Eliminar la posibilidad de utilizar la aritmética de puntero entre objetos creados dinámicamente en una sola matriz de caracteres sería un cambio importante que en mi opinión debería citarse allí, porque es una característica de uso común. Como nada de esto existe en las páginas de compatibilidad, creo que confirma que no era la intención del estándar prohibirlo.
En particular, derrotaría esa construcción dinámica común de una matriz de objetos de una clase sin constructor predeterminado:
class T {
...
public T(U initialization) {
...
}
};
...
unsigned char *mem = new unsigned char[N * sizeof(T)];
T * arr = reinterpret_cast<T*>(mem); // See the array as an array of N T
for (i=0; i<N; i++) {
U u(...);
new(arr + i) T(u);
}
arr
continuación, se puede utilizar como puntero al primer elemento de una matriz ...
Para ampliar las respuestas dadas aquí es un ejemplo de lo que creo que la redacción revisada excluye:
Advertencia: comportamiento indefinido
#include <iostream>
int main() {
int A[1]{7};
int B[1]{10};
bool same{(B)==(A+1)};
std::cout<<B<< '' ''<< A <<'' ''<<sizeof(*A)<<''/n'';
std::cout<<(same?"same":"not same")<<''/n'';
std::cout<<*(A+1)<<''/n'';//!!!!!
return 0;
}
Por razones totalmente dependientes de la implementación (y frágiles), la salida posible de este programa es:
0x7fff1e4f2a64 0x7fff1e4f2a60 4
same
10
Esa salida muestra que las dos matrices (en ese caso) están almacenadas en la memoria de modo que ''uno más allá del final'' de
A
tiene el valor de la dirección del primer elemento de
B
La especificación revisada garantiza que independientemente de que
A+1
nunca sea un puntero válido a
B
La antigua frase ''independientemente de cómo se obtenga el valor'' dice que si ''A + 1'' apunta a ''B [0]'', entonces es un puntero válido a ''B [0]''.
Eso no puede ser bueno y seguramente nunca la intención.