Pregunta ¿Sigue siendo un puntero con la dirección y el tipo correctos un puntero válido desde C ++ 17?


(En referencia a esta pregunta y respuesta.)

Antes del estándar C ++ 17, se incluyó la siguiente oración en [basic.compound] / 3:

Si un objeto de tipo T está ubicado 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 remoto.

Por ejemplo, creo que esta frase hizo que este código de ejemplo esté definido, 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 tiene la dirección para *p2 y tiene el tipo correcto, por lo *(p1+1) es un puntero a *p2. En C ++ 17 p1+1 es un puntero pasado el fin, entonces no es un puntero a objeto y creo que no es dereferencable.

¿Es esta interpretación de esta modificación del derecho estándar o existen otras reglas que compensan la eliminación de la oración citada?


76
2018-01-02 14:00


origen


Respuestas:


¿Es esta interpretación de esta modificación del derecho estándar o existen 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 exclusivos. p1+1 es un puntero más allá del final, no un puntero a un objeto. p1+1 apunta a un hipotético x[1] de una matriz de tamaño 1 en p1, No a p2. Esos dos objetos no son puntero-interconvertibles.

También tenemos la nota no normativa:

[Nota: un puntero más allá del final de un objeto ([expr.add]) no se considera que apunta a un objeto no relacionado del tipo del objeto que podría estar ubicado en esa dirección. [...]

que aclara la intención.


Como T.C. señala en numerosos comentarios (notablemente este), este es realmente un caso especial del problema que surge al tratar de implementar std::vector - cual es ese [v.data(), v.data() + v.size()) debe ser un rango válido y aún 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 matriz hipotética de un solo tamaño. Para obtener más recursos, consulte CWG 2182, esta discusión estándary dos revisiones de un documento sobre el tema: P0593R0 y P0593R1 (sección 1.3 específicamente).


43
2018-01-02 14:14



En tu 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 almacenamiento asociado con otro objeto e de tipo "matriz de N"   unsigned char "o de tipo" array of N std :: byte "(21.2.1), esa matriz proporciona almacenamiento para el creado   objeto si:
  (3.1) - la vida útil de e ha comenzado y no terminado, y
  (3.2) - el almacenamiento para el nuevo objeto encaja completamente dentro de e, y
  (3.3) - no hay un objeto de matriz más pequeño que satisfaga estas restricciones.

El 3.3 parece poco claro, pero los ejemplos a continuación hacen que el intento sea más claro:

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, el buffer formación proporciona almacenamiento para ambos *p1 y *p2.

Los siguientes párrafos prueban que el objeto completo para ambos *p1 y *p2 es 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, yc está anidado dentro de b.

5 Para cada objeto x, hay un objeto llamado el 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 (enfatizar la mía):

3 ... cada   El 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 del 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 al final de un objeto o que lo representa representa la dirección del   primer byte en memoria (4.4) ocupado por el objeto o el primer byte en la memoria después del final del almacenamiento   ocupado por el objeto, respectivamente. [Nota: un puntero más allá del final de un objeto (8.7) no se considera   señalar a un no relacionado objeto del tipo del objeto que podría estar ubicado en esa dirección. Un valor de puntero   se vuelve inválido cuando el almacenamiento que denota alcanza el final de su duración de almacenamiento; ver 6.7. -Finalizar nota]   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 [n]. los   la representación del valor de los tipos de puntero está definida por la implementación. Los punteros a los tipos compatibles con el diseño deberán   tienen los mismos requisitos de representación y alineación de valores (6.11) ...

La nota Un puntero pasado el final ... no se aplica aquí porque los objetos apuntados por p1 y p2y no no relacionado, pero están anidados en el mismo objeto completo, por lo que los aritméticos de puntero tienen sentido dentro del objeto que proporciona almacenamiento: p2 - p1 se define y es (&buffer[sizeof(int)] - buffer]) / sizeof(int) eso es 1.

Asi que p1 + 1  es un puntero a *p2y *(p1 + 1) = 10; tiene un comportamiento definido y establece el valor de *p2.


También leí el anexo C4 sobre la compatibilidad entre C ++ 14 y los estándares actuales (C ++ 17). La eliminación de la posibilidad de usar aritmética de puntero entre los objetos creados dinámicamente en una única matriz de caracteres sería un cambio importante que en mi humilde opinión debería mencionarse allí, porque es una característica de uso común. Como no existe nada sobre esto en las páginas de compatibilidad, creo que confirma que no era la intención del estándar prohibirlo.

En particular, vencería la construcción dinámica común de una matriz de objetos de una clase sin un 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 puede usarse como un puntero al primer elemento de una matriz ...


8
2018-01-02 17:40



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), el posible resultado de este programa es:

0x7fff1e4f2a64 0x7fff1e4f2a60 4
same
10

Ese resultado muestra que las dos matrices (en ese caso) se almacenan en la memoria de forma tal que 'pasado el final' de A pasa a mantener el valor de la dirección del primer elemento de B.

La especificación revisada garantiza que independientemente A+1 nunca es un puntero válido para B. La frase anterior 'independientemente de cómo se obtenga el valor' dice que si 'A + 1' pasa a apuntar a 'B [0]', entonces es un puntero válido a 'B [0]'. Eso no puede ser bueno y seguramente nunca la intención.


0
2018-01-03 11:23