tutorialspoint pointer objects array and c++ pointers c++11 language-lawyer pointer-arithmetic

objects - pointers c++



Aritmética del puntero a través de los límites del subobjeto (1)

Actualizado: Esta respuesta al principio omitió algo de información y por lo tanto condujo a conclusiones erróneas.

En sus ejemplos, initial objetos initial y de rest son claramente distintos (matriz), por lo que comparar punteros a initial (o sus elementos) con punteros para rest (o sus elementos) es

  • UB, si usas la diferencia de los punteros. (§5.7,6)
  • no especificado, si usa operadores relacionales (§5.9.2)
  • bien definido para == (Por lo tanto, el segundo recorte es bueno, ver más abajo)

Primer fragmento:

Crear la diferencia en el primer fragmento es un comportamiento indefinido para la cita que proporcionó ( §5.7.6 ):

A menos que ambos punteros apuntan a elementos del mismo objeto de matriz, o uno más allá del último elemento del objeto de matriz, el comportamiento no está definido.

Para aclarar las partes UB del primer código de ejemplo:

//first example int main() { Derived<float, 10> d; assert(&d.rest[9] - &d.initial == 10); //!!! UB !!! assert(&d.end - &d.begin == sizeof(float) * 10); //!!! UB !!! (*) return 0; }

La línea marcada con (*) es interesante: d.begin y d.end no son elementos de la misma matriz y, por lo tanto, la operación da como resultado UB. Esto a pesar del hecho de que puede reinterpret_cast<char*>(&d) y tener ambas direcciones en la matriz resultante. Pero dado que esa matriz es una representación de todo d , no se debe ver como un acceso a partes de d . Entonces, aunque esa operación probablemente solo funcione y dé el resultado esperado en cualquier implementación que uno pueda soñar, todavía es UB, como una cuestión de definición.

Segundo fragmento:

Este es en realidad un comportamiento bien definido, pero la implementación definió el resultado:

int main() { Derived<float, 10> d; assert(&d.rest[9] - &d.rest[0] == 9); assert(&d.rest[0] == &d.initial[1]); //(!) assert(&d.initial[1] - &d.initial[0] == 1); return 0; }

La línea marcada con (!) No es ub, pero su resultado es la implementación definida , ya que el relleno, la alineación y la instilación mencionada podrían desempeñar un papel. Pero si esa afirmación se mantiene, puede usar las dos partes del objeto como una matriz .

Sabría que el rest[0] se colocaría inmediatamente después de la initial[0] en la memoria. A primera vista , no podrías usar fácilmente la igualdad:

  • initial[1] apuntaría one-past-the-end of initial , desreferenciando es UB.
  • rest[-1] está claramente fuera de límites.

Pero entra en §3.9.2,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. [Nota: por ejemplo, se consideraría que la dirección que está más allá del final de una matriz (5.7) apunta a un objeto no relacionado del tipo de elemento de la matriz que podría estar ubicado en esa dirección.

Así que siempre que &initial[1] == &rest[0] , será binario igual que si hubiera solo una matriz, y todo estará bien.

Podría iterar sobre ambas matrices, ya que podría aplicar algún "interruptor de contexto de puntero" en los límites. Así que hasta su último fragmento: ¡el swap no es necesario!

Sin embargo, hay algunas advertencias: el rest[-1] es UB, y por lo tanto sería initial[2] , debido a §5.7,5 :

Si tanto el operando del puntero como el resultado apuntan a elementos del mismo objeto del arreglo, o uno más allá del último elemento del objeto del arreglo, la evaluación no producirá un desbordamiento; de lo contrario, el comportamiento no está definido .

(énfasis mío). Entonces, ¿cómo encajan estos dos juntos?

  • "Buen camino": &initial[1] está bien, y desde &initial[1] == &rest[0] puede tomar esa dirección y continuar incrementando el puntero para acceder a los otros elementos de rest , debido a §3.9.2 , 3
  • "Mal camino": la initial[2] es *(initial + 2) , pero desde §5.7,5, la initial +2 ya es UB y nunca se puede usar §3.9.2,3 aquí.

Juntos: tienes que pasar por el límite, tomarte un breve descanso para comprobar que las direcciones son iguales y luego puedes continuar.

¿El código siguiente (que realiza la aritmética del puntero a través de los límites del subobjeto) tiene un comportamiento bien definido para los tipos T para los que compila (que, en C ++ 11, no necesariamente tiene que ser POD ) o algún subconjunto de los mismos?

#include <cassert> #include <cstddef> template<typename T> struct Base { // ensure alignment union { T initial; char begin; }; }; template<typename T, size_t N> struct Derived : public Base<T> { T rest[N - 1]; char end; }; int main() { Derived<float, 10> d; assert(&d.rest[9] - &d.initial == 10); assert(&d.end - &d.begin == sizeof(float) * 10); return 0; }

LLVM usa una variación de la técnica anterior en la implementación de un tipo de vector interno que está optimizado para usar inicialmente la pila para matrices pequeñas pero cambia a una memoria intermedia asignada en pila una vez que supera la capacidad inicial. (La razón para hacerlo de esta manera no está clara en este ejemplo, pero aparentemente reduce la saturación del código de la plantilla, esto es más claro si mira el code ).

NOTA: Antes de que alguien se queje, esto no es exactamente lo que está haciendo y podría ser que su enfoque sea más conforme a los estándares que lo que he dado aquí, pero quería preguntar sobre el caso general.

Obviamente, funciona en la práctica, pero tengo curiosidad si hay algo en las garantías estándar para que ese sea el caso. Me inclino a decir que no, dado N3242 / expr.add :

Cuando se restan dos punteros a elementos del mismo objeto de matriz, el resultado es la diferencia de los subíndices de los dos elementos de matriz ... Además, si la expresión P apunta a un elemento de un objeto de matriz o al último elemento de un objeto de matriz, y la expresión Q apunta al último elemento del mismo objeto de matriz, la expresión ((Q) +1) - (P) tiene el mismo valor que ((Q) - (P)) + 1 y as - ((P) - ((Q) +1)), y tiene el valor cero si la expresión P señala uno pasado el último elemento del objeto de matriz, incluso si la expresión (Q) +1 no apunta a un elemento del objeto de la matriz. ... A menos que ambos punteros apuntan a elementos del mismo objeto de matriz, o uno más allá del último elemento del objeto de matriz, el comportamiento no está definido.

Pero teóricamente, la parte media de la cita anterior, combinada con el diseño de clase y las garantías de alineación, podría permitir que el siguiente ajuste (menor) sea válido:

#include <cassert> #include <cstddef> template<typename T> struct Base { T initial[1]; }; template<typename T, size_t N> struct Derived : public Base<T> { T rest[N - 1]; }; int main() { Derived<float, 10> d; assert(&d.rest[9] - &d.rest[0] == 9); assert(&d.rest[0] == &d.initial[1]); assert(&d.rest[0] - &d.initial[0] == 1); return 0; }

que combinado con varias otras disposiciones relacionadas con el diseño de la union , la convertibilidad hacia y desde el char * , etc., podría decirse que también hace que el código original sea válido. (El principal problema es la falta de transitividad en la definición de la aritmética del puntero dada anteriormente).

Alguien sabe con certeza? N3242 / expr.add parece aclarar que los punteros deben pertenecer al mismo "objeto de matriz" para que se defina, pero hipotéticamente podría darse el caso de que otras garantías en el estándar, cuando se combinan juntas, pudieran requerir una definición de todos modos en este caso para permanecer lógicamente auto-consistente. (No estoy apostando por eso, pero sería al menos concebible).

EDITAR : @MatthieuM plantea la objeción de que esta clase no es de diseño estándar y, por lo tanto, no se puede garantizar que no contenga ningún relleno entre el subobjeto base y el primer miembro del derivado, incluso si ambos están alineados con alignof(T) . No estoy seguro de cuán cierto es eso, pero eso abre las siguientes preguntas variantes:

  • ¿Se garantizaría que esto funcionaría si se eliminara la herencia?

  • Would &d.end - &d.begin >= sizeof(float) * 10 estarían garantizados aunque &d.end - &d.begin == sizeof(float) * 10 no lo fueran?

LAST EDIT @ArneMertz aboga por una lectura muy cercana de N3242 / expr.add (sí, sé que estoy leyendo un borrador, pero está lo suficientemente cerca), pero ¿el estándar realmente implica que lo siguiente tiene un comportamiento indefinido entonces si el intercambio línea se elimina? (mismas definiciones de clase que las anteriores)

int main() { Derived<float, 10> d; bool aligned; float * p = &d.initial[0], * q = &d.rest[0]; ++p; if((aligned = (p == q))) { std::swap(p, q); // does it matter if this line is removed? *++p = 1.0; } assert(!aligned || d.rest[1] == 1.0); return 0; }

Además, si == no es lo suficientemente fuerte, ¿qué pasa si aprovechamos el hecho de que std::less forma un orden total sobre los punteros, y cambiamos el siguiente condicional a:

if((aligned = (!std::less<float *>()(p, q) && !std::less<float *>()(q, p))))

¿El código supone que dos punteros iguales apuntan al mismo objeto de matriz realmente roto según una lectura estricta del estándar?

EDITAR Lo siento, solo quiero agregar un ejemplo más, para eliminar el problema de diseño estándar:

#include <cassert> #include <cstddef> #include <utility> #include <functional> // standard layout struct Base { float initial[1]; float rest[9]; }; int main() { Base b; bool aligned; float * p = &b.initial[0], * q = &b.rest[0]; ++p; if((aligned = (p == q))) { std::swap(p, q); // does it matter if this line is removed? *++p = 1.0; q = &b.rest[1]; // std::swap(p, q); // does it matter if this line is added? p -= 2; // is this UB? } assert(!aligned || b.rest[1] == 1.0); assert(p == &b.initial[0]); return 0; }