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


(En referencia a esta pregunta y respuesta .)

Antes del estándar C++17, la siguiente frase se incluyó en [basic.compuesto]/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 eliminada.

Por ejemplo, creo que que esta oración hizo que este código de ejemplo se definiera, 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 contiene la dirección a *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 el fin, así que no es un puntero a objeto y creo que no es desreferenciable.

¿Es esta interpretación de esta modificación del derecho estándar o hay otras reglas que compensan la ¿supresión de la frase citada?

Author: Barry, 2018-01-02

3 answers

¿Es esta interpretación de esta modificación del derecho estándar o existen otras reglas que compensen la supresión de esta frase citada?

Sí, esta interpretación es correcta. Un puntero pasado el final no es simplemente convertible a otro valor de puntero que apunta a esa dirección.

El nuevo [básico.compuesto]/3 dice:

Cada valor de tipo 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 pasado el final de un objeto ([expr.agregar]), o

, Esos son mutuamente excluyentes. p1+1 es un puntero pasado el final, no un puntero a un objeto. p1+1 apunta a un x[1] hipotético de una matriz de tamaño 1 en p1, no a p2. Esos dos objetos no son interconvertibles.

También tenemos la nota no normativa:

[Nota: Un puntero pasado el final de un objeto ([expr.add]) no se considera que apunte 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 viene con tratar de implementar std::vector, que es que [v.data(), v.data() + v.size()) necesita ser un rango válido y, sin embargo, vector no crea un objeto de matriz, por lo que la única aritmética de puntero definida sería yendo de cualquier objeto dado en el vector al pasado-el-final de su matriz hipotética de un tamaño. Para más recursos, ver CWG 2182, esta discusión std, y dos revisiones de un documento sobre el tema: P0593R0 y P0593R1 (sección 1.3 específicamente).

 43
Author: Barry,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-03 13:40:49

En su ejemplo, *(p1 + 1) = 10; debe 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 fue construida dinámicamente en una matriz char 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 " array of Y unsigned char" or of type "array of N std:: byte" (21.2.1), that array provides storage for the created objeto si:
(3.1) - la vida 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 un objeto de matriz más pequeño que satisfaga estas restricciones.

La 3.3 parece bastante poco clara, pero los ejemplos a continuación hacen que la intención sea más clara:

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)

Así que en el ejemplo, la matriz buffer proporciona almacenamiento tanto para *p1 como para *p2.

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

5 Por cada objeto x, hay algún 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 la misma.
(5.2) - De lo contrario, el objeto completo de x es el objeto completo del objeto (único) que contiene x.

Una vez establecido esto, 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 hacia o más allá del final de un objeto representa la dirección primer byte en memoria (4.4) ocupado por el objeto o el primer byte en memoria después del final del almacenamiento ocupado por el objeto, respectivamente. [Nota: Un puntero pasado el final de un objeto (8.7) no se considera apunta a un objeto no relacionado del tipo del objeto que podría estar ubicado en esa dirección. Un valor de puntero no es válido cuando el almacenamiento que indica alcanza el final de su duración de almacenamiento; véase 6.7. -nota final ] Para propósitos de aritmética de puntero (8.7) y comparación (8.9, 8.10), un puntero pasado el final del último elemento de una matriz x de n elementos es se considera equivalente a un puntero a un elemento hipotético x [n]. El la representación de valor de los tipos de puntero está definida por la implementación. Los punteros a tipos compatibles con el diseño deberán: tener 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 p2 y no no relacionados , pero están anidados en el mismo objeto completo, por lo que la aritmética de punteros sentido dentro del objeto que proporcionan almacenamiento: p2 - p1 está definida y es (&buffer[sizeof(int)] - buffer]) / sizeof(int) que es 1.

Así p1 + 1 es un puntero a *p2, y *(p1 + 1) = 10; ha definido el comportamiento 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 usar aritmética de puntero entre objetos creados dinámicamente en una matriz de caracteres individuales sería un cambio importante que IMHO debería ser citado allí, porque es una característica de uso común. Como no hay nada al respecto en las páginas de compatibilidad, creo que confirma que no fue 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 puede ser usado como un puntero al primer elemento de un array...

 8
Author: Serge Ballesta,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-03 10:54:33

Para ampliar las respuestas dadas aquí es un ejemplo de lo que creo que la redacción revisada está excluyendo:

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 enteramente dependientes de la implementación (y frágiles) la salida posible de este programa es:

0x7fff1e4f2a64 0x7fff1e4f2a60 4
same
10

Esa salida muestra que los dos arrays (en ese caso) pasan a ser almacenados en memoria de tal manera que' one past the end ' de A pasa a contener el valor de la dirección del primer elemento de B.

La especificación revisada garantiza que, independientemente de A+1, nunca sea un puntero válido a B. La vieja frase 'independientemente de cómo se obtiene 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.

 1
Author: Persixty,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-03 11:29:49