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

84

(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 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 se ha eliminado .

Por ejemplo, creo 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+1contiene la dirección ay *p2tiene el tipo correcto, por lo que *(p1+1)es un puntero a *p2. En C ++ 17 p1+1es un puntero más allá del final , por lo que no es un puntero al objeto y creo que no es desreferenciable.

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

Oliv
fuente
Nota: hay reglas nuevas / actualizadas sobre la procedencia del puntero en [basic.stc.dynamic.safety] y [util.dynamic.safety]
MM
@MM Eso solo importa en implementaciones con estricta seguridad de puntero, que es un conjunto vacío (dentro del error experimental).
TC
4
La declaración citada nunca ha sido realmente cierta en la práctica. Dado int a, b = 0;, no puede hacer *(&a + 1) = 1;incluso si marcó &a + 1 == &b. Si puede obtener un puntero válido a un objeto simplemente adivinando su dirección, incluso almacenar variables locales en registros se vuelve problemático.
TC
@TC 1) ¿Qué compilador pone una var en reg después de haber tomado su dirección? 2) ¿Cómo adivinas una dirección correctamente sin medirla?
Curioso
@curiousguy Exactamente es por eso que simplemente lanzar un número obtenido por otros medios (por ejemplo, adivinando) a la dirección donde se encuentra un objeto es problemático: asigna un alias a ese objeto, pero el compilador no lo sabe. Si, por el contrario, toma la dirección del objeto, es como dice: el compilador recibe una advertencia y se sincroniza en consecuencia.
Peter - Reincorpora a Monica

Respuestas:

45

¿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 más allá del final no es simplemente convertible en otro valor de puntero que apunte a esa dirección.

El nuevo [basic.compound] / 3 dice:

Cada valor de 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+1es un puntero más allá del final, no un puntero a un objeto. p1+1apunta a un hipotético x[1]de una matriz de tamaño 1 en p1, no a p2. Estos dos objetos no son interconvertibles por puntero.

También tenemos la nota no normativa:

[Nota: No se considera que un puntero más allá del 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. [...]

que aclara la intención.


Como TC señala en numerosos comentarios (en particular este ), este es realmente un caso especial del problema que surge al intentar implementar std::vector, que es que [v.data(), v.data() + v.size())debe ser un rango válido y, sin vectorembargo, no crea un objeto de matriz, por lo que el solo la aritmética de puntero definida iría desde cualquier objeto dado en el vector hasta más allá del final de su matriz hipotética de un tamaño. Para obtener más recursos, consulte CWG 2182 , esta discusión estándar y dos revisiones de un documento sobre el tema: P0593R0 y P0593R1 (sección 1.3 específicamente).

Barry
fuente
3
Este ejemplo es básicamente un caso especial del conocido " vectorproblema de implementabilidad". +1.
TC
2
@Oliv El caso general ha existido desde C ++ 03. La causa principal es que la aritmética del puntero no funciona como se esperaba porque no tiene un objeto de matriz.
TC
1
@TC Creí que el único problema provenía de la restricción en la aritmética de punteros. ¿No es la eliminación de esta oración un nuevo problema? ¿El ejemplo de código también es UB en pre-C ++ 17?
Oliv
1
@Oliv Si la aritmética del puntero es fija, entonces p1+1ya no produciría un puntero más allá del final y toda la discusión sobre los punteros más allá del final es discutible. Su caso especial particular de dos elementos puede no ser UB pre-17, pero tampoco es muy interesante.
TC
5
@TC ¿Puede indicarme algún lugar donde pueda leer sobre este "problema de implementación de vectores"?
SirGuy
8

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 de 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 de tipo "matriz de N caracteres sin signo" o de 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 del nuevo objeto encaja completamente dentro de e, y
(3.3) - no hay un objeto de matriz más pequeño que satisfaga estos limitaciones.

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 buffermatriz proporciona almacenamiento para ambos *p1y *p2.

Los siguientes párrafos prueban que el objeto completo para ambos *p1y *p2es 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 un 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 de 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) 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 que apuntan a una relaciónobjeto del tipo de objeto que podría estar ubicado en esa dirección. Un valor de puntero se vuelve inválido cuando el almacenamiento que denota llega al final de su duración de almacenamiento; ver 6.7. —Nota final] A los efectos de la aritmética de punteros (8.7) y la 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 alineación y representación de valores (6.11) ...

La nota Un puntero más allá del final ... no se aplica aquí porque los objetos apuntados por p1y p2no sin relación , pero están anidados en el mismo objeto completo, por lo que la aritmética de puntero tiene sentido dentro del objeto que proporciona almacenamiento: p2 - p1está definido y es (&buffer[sizeof(int)] - buffer]) / sizeof(int)eso es 1.

Por lo que p1 + 1 es un puntero a *p2, *(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 usar aritmética de puntero entre objetos creados dinámicamente en una sola matriz de caracteres sería un cambio importante que en mi humilde opinión debería citarse allí, porque es una característica de uso común. Como no existe nada al respecto 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 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 luego se puede usar como un puntero al primer elemento de una matriz ...

Serge Ballesta
fuente
Ajá, entonces el mundo no se ha vuelto loco. +1
StoryTeller - Unslander Monica
@StoryTeller: Yo también espero. Además ni una palabra al respecto en la sección de compatibilidad. Pero parece que la opinión contraria tiene más reputación aquí ...
Serge Ballesta
2
Está tomando una sola palabra, "no relacionada", en una nota no normativa, y le está dando un significado que no puede soportar, en contradicción con las reglas normativas en [expr.add] que gobiernan la aritmética de punteros. No hay nada en el Anexo C porque la aritmética de punteros de casos generales nunca ha funcionado en ningún estándar. No hay nada que romper.
TC
3
@TC: Google es muy poco útil para localizar información sobre este "problema de implementación de vectores", ¿podrías ayudar?
Matthieu M.
6
@MatthieuM. Consulte el problema principal 2182 , este hilo de discusión estándar , P0593R0 y P0593R1 (en particular, la sección 1.3) . El problema básico es que vectorno crea (y no puede) un objeto de matriz, pero tiene una interfaz que permite al usuario obtener un puntero que admita la aritmética de punteros (que solo se define para punteros en objetos de matriz).
TC
1

Para ampliar las respuestas que se dan aquí, hay un ejemplo de lo que creo que excluye la redacción revisada:

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

0x7fff1e4f2a64 0x7fff1e4f2a60 4
same
10

Esa salida muestra que las dos matrices (en ese caso) se almacenan en la memoria de manera que 'uno más allá del final' de Atiene el valor de la dirección del primer elemento de B.

La especificación revisada asegura que independientemente A+1nunca sea un puntero válido 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.

Persixty
fuente
¿Esto también proscribe efectivamente el uso de una matriz vacía al final de una estructura de modo que una clase derivada o un asignador personalizado nuevo pueda especificar una matriz de tamaño personalizado? Quizás el nuevo problema sea con el "independientemente de cómo": ¿hay algunas formas que son válidas y otras que son peligrosas?
Gem Taylor
@Persixty Entonces, el valor de un objeto puntero está determinado por los bytes de los objetos y nada más. Entonces, dos objetos con el mismo estado apuntan al mismo objeto. Si uno es válido, el otro también lo es. Entonces, en arquitecturas comunes, donde un valor de puntero se representa como un número, dos punteros con valores iguales apuntan a los mismos objetos y uno de los extremos a los mismos otros objetos.
Curioso
@Persixty Además, tipo trivial significa que puede enumerar los posibles valores de un tipo. Esencialmente, cualquier compilador moderno en cualquier modo de optimización (incluso -O0en algunos compiladores) no considera a los punteros como tipos triviales. Los compiladores no se toman en serio los requisitos del std, ni tampoco las personas que escriben el std, que sueñan con un lenguaje diferente y hacen todo tipo de inventos que contradicen directamente principios básicos. Obviamente, los usuarios se confunden y, a veces, se les trata mal cuando se quejan de errores del compilador.
Curioso
La nota no normativa de la pregunta quiere que pensemos en "uno más allá del fin" como si no apuntara a nada. Ambos sabemos que, en la práctica, es posible que esté apuntando a algo y, en la práctica, es posible desreferenciarlo. Pero eso (según el estándar) no es un programa válido. Podemos imaginar una implementación que sepa que un puntero se obtuvo mediante aritmética hasta el final y genera una excepción si se desreferencia. Si bien sé de una plataforma que hace eso. Creo que el estándar no quiere excluirlo.
Persixty
@curiousguy Además, no estoy seguro de lo que quiere decir al enumerar los valores posibles. Esa no es una característica requerida de un tipo trivial como lo define C ++.
Persixty