Struct hackear equivalente en C++
arrays c++11 (6)
La estructura donde se tiene una serie de longitud 0 como el último miembro de una estructura de C90 y C99 es bien conocida, y con la introducción de miembros de matriz flexible en C99, incluso tenemos una forma estandarizada de usarla con []
. Desafortunadamente, C ++ no proporciona tal construcción, y (al menos con Clang 3.4 ), compilar una estructura con [0]
o []
producirá una advertencia de compilación con --std=c++11 -pedantic
:
$ cat test.cpp
struct hack {
char filler;
int things[0];
};
$ clang++ --std=c++11 -pedantic test.cpp
/test.cpp:3:14: warning: zero size arrays are an extension [-Wzero-length-array]
int things[0];
y de manera similar
$ cat test.cpp
struct fam {
char filler;
int things[];
};
$ clang++ --std=c++11 -pedantic test.cpp
/test.cpp:3:7: warning: flexible array members are a C99 feature [-Wc99-extensions]
int things[];
Mi pregunta entonces es esta; diga que quiero tener una estructura que contenga una matriz de tamaño variable como el último elemento en C ++. ¿Qué es lo correcto para hacer dado un compilador que soporta ambos? ¿Debo utilizar la estructura hack [0]
(que es una extensión del compilador) o la FAM []
(que es una característica de C99)? Por lo que yo entiendo, tampoco funcionará, pero estoy tratando de averiguar cuál es el mal menor.
Además, antes de que las personas empiecen a sugerir mantener un int*
en una parte de memoria asignada por separado en la estructura, no es una respuesta satisfactoria. Quiero asignar una sola pieza de memoria para contener tanto mi estructura como los elementos de la matriz. Usar un std :: vector también cae en la misma categoría. Si se pregunta por qué no quiero usar un puntero, la respuesta de la R. a otra pregunta le da una buena visión general.
Ha habido algunas preguntas similares en otros lugares, pero ninguna da una respuesta a esta pregunta en particular:
- ¿Los miembros de la matriz flexible son válidos en C ++? : Muy similar, pero la pregunta es si FAM es válido en C ++ (no). Estoy buscando una buena razón para elegir uno o el otro.
- Variante conforme de la antigua "estructura hackeada" : propone una alternativa, pero no es bonita ni siempre es correcta (¿qué sucede si se agrega relleno a la estructura?). Acceder a los elementos más tarde tampoco es tan limpio como hacer
e.things[42]
.
C ++ no tiene el concepto de "arreglos flexibles". La única forma de tener una matriz flexible en C ++ es usar una matriz dinámica, lo que lo lleva a usar int* things
. Necesitará un parámetro de tamaño si está intentando leer estos datos de un archivo para poder crear la matriz del tamaño adecuado (o usar un std::vector
y seguir leyendo hasta que llegue al final del flujo).
El hack de "matriz flexible" mantiene la localidad espacial (es decir, tiene la memoria asignada en un bloque contiguo al resto de la estructura), que se pierde cuando se le obliga a usar memoria dinámica. Realmente no hay una manera elegante de evitar eso (por ejemplo, podría asignar un búfer grande, pero tendría que hacerlo lo suficientemente grande como para contener cualquier número de elementos que quisiera, y si los datos reales que se leen son más pequeños que los buffer, no se desperdiciaría espacio asignado).
Además, antes de que las personas empiecen a sugerir mantener un int * en una parte de la memoria asignada por separado en la estructura, no es una respuesta satisfactoria. Quiero asignar una sola pieza de memoria para contener tanto mi estructura como los elementos de la matriz. Usar un std :: vector también cae en la misma categoría.
Así es como lo harías en C ++. Puede votarlo todo lo que quiera, pero el hecho es que una extensión no estándar no funcionará cuando se mude a un compilador que no lo admite. Si mantiene el estándar (por ejemplo, evite el uso de hacks específicos del compilador), es menos probable que encuentre este tipo de problemas.
Esto es C ++, por lo que las plantillas están disponibles:
template <int N>
struct hack {
int filler;
int thing [N];
};
Casting entre diferentes punteros a diferentes instancias será el problema difícil, entonces.
Hay al menos una ventaja para los miembros de la matriz flexible sobre las matrices de longitud cero cuando el compilador está colgado.
struct Strukt1 {
int fam[];
int size;
};
struct Strukt2 {
int fam[0];
int size;
};
Aquí clang producirá un error si ve Strukt1
pero no lo hará si ve Strukt2
. gcc e icc aceptan sin errores y errores msvc en cualquier caso. gcc hace un error si el código se compila como C.
Lo mismo se aplica para este ejemplo similar pero menos obvio:
struct Strukt3 {
int size;
int fam[];
};
strukt Strukt4 {
Strukt3 s3;
int i;
};
Lo primero que viene a la mente es NO , no escriba C en C ++. En el 99.99% de los casos, este hack
no es necesario, no hará ninguna mejora notable en el rendimiento con solo mantener un std::vector
y complicará su vida y la de los demás mantenedores del proyecto en el que implementa esto.
Si desea un enfoque estándar compatible, proporcione un tipo de envoltorio que asigne dinámicamente una porción de memoria lo suficientemente grande como para contener el hack
(menos la matriz) más N*sizeof(int)
por el equivalente de la matriz (no olvide asegurarse) alighnment apropiado). La clase tendría accesores que asignan los miembros y los elementos de la matriz a la ubicación correcta en la memoria.
Ignorando la alineación y el código de la placa de la caldera para que la interfaz sea agradable y la implementación segura:
template <typename T>
class DataWithDynamicArray {
void *ptr;
int* array() {
return static_cast<int*>(static_cast<char*>(ptr)+sizeof(T)); // align!
}
public:
DataWithDynamicArray(int size) : ptr() {
ptr = malloc(sizeof(T) + sizeof(int)*size); // force correct alignment
new (ptr) T();
}
~DataWithDynamicArray() {
static_cast<T*>(ptr)->~T();
free(ptr);
}
// copy, assignment...
int& operator[](int pos) {
return array()[pos];
}
T& data() {
return *static_cast<T*>(ptr);
}
};
struct JustSize { int size; };
DataWithDynamicArray<JustSize> x(10);
x.data().size = 10
for (int i = 0; i < 10; ++i) {
x[i] = i;
}
Ahora realmente no lo implementaría de esa manera (¡evitaría implementarlo en absoluto!), Como por ejemplo, el tamaño debería ser parte del estado de DataWithDynamicArray
...
Esta respuesta se proporciona solo como un ejercicio, para explicar que se puede hacer lo mismo sin extensiones, pero tenga en cuenta que es solo un ejemplo de juguete que tiene muchos problemas que incluyen, entre otros, seguridad o alineación de excepciones (y, sin embargo, es mejor que forzar la usuario para hacer el malloc
con el tamaño correcto). El hecho de que pueda hacerlo no significa que deba hacerlo , y la verdadera pregunta es si necesita esta función y si lo que está tratando de hacer es un buen diseño o no.
Puede obtener más o menos el mismo efecto utilizando una función miembro y un reinterpret_cast
:
int* buffer() { return reinterpret_cast<int*>(this + 1); }
Esto tiene un defecto importante: no garantiza la alineación correcta. Por ejemplo, algo como:
struct Hack
{
char size;
int* buffer() { return reinterpret_cast<int*>(this + 1); }
};
Es probable que devuelva un puntero desalineado. Puede solucionar esto colocando los datos en la estructura en una unión con el tipo cuyo puntero está devolviendo. Si tienes C ++ 11, puedes declarar:
struct alignas(alignof(int)) Hack
{
char size;
int* buffer() { return reinterpret_cast<int*>(this + 1); }
};
(Creo. Nunca he intentado esto, y podría tener algunos detalles de la sintaxis errónea).
Este idioma tiene un segundo defecto importante: no hace nada para garantizar que el campo de tamaño se corresponda con el tamaño real del búfer y, lo que es peor, no hay una forma real de usar el new
aquí. Para corregir esto, de alguna manera, puede definir un operator new
específico de clase operator new
y operator delete
:
struct alignas(alignof(int)) Hack
{
void* operator new( size_t, size_t n );
void operator delete( void* );
Hack( size_t n );
char size;
int* buffer() { return reinterpret_cast<int*>(this + 1); }
};
El código del cliente tendrá que usar la ubicación nueva para asignar:
Hack* hack = new (20) Hack(20);
El cliente todavía tiene que repetir el tamaño, pero no puede ignorarlo.
También hay técnicas que se pueden usar para evitar la creación de instancias que no se asignan dinámicamente, etc., para terminar con algo como:
struct alignas(alignof(int)) Hack
{
private:
void operator delete( void* p )
{
::operator delete( p );
}
// ban all but dynamic lifetime (and also inheritance, member, etc.)
~Hack() = default;
// ban arrays
void* operator new[]( size_t ) = delete;
void operator delete[]( void* p ) = delete;
public:
Hack( size_t n );
void* operator new( size_t, size_t n )
{
return ::operator new( sizeof(Hack) + n * sizeof(int) );
}
char size;
// Since dtor is private, we need this.
void deleteMe() { delete this; }
int* buffer() { return reinterpret_cast<int*>(this + 1); }
};
Dados los peligros fundamentales de una clase así, es discutible si se necesitan tantas medidas de protección. Incluso con ellos, solo es realmente utilizable por alguien que entiende completamente todas las restricciones y está prestando atención cuidadosamente. En todos los casos, excepto en los casos extremos, en el código de muy bajo nivel, simplemente haría del búfer un std::vector<int>
y terminaría con él. En todos, excepto en el código de nivel más bajo, la diferencia en el rendimiento no valdría la pena el riesgo y el esfuerzo.
EDITAR:
Como ejemplo, la implementación de g ++ de std::basic_string
usa algo muy similar al anterior, con una struct
contiene un recuento de referencia, el tamaño actual y la capacidad actual (tres size_t
), seguidas directamente por el búfer de caracteres. Y como se escribió mucho antes de que C ++ 11 y alignas
/ alignof
, algo como std::basic_string<double>
se bloquee en algunos sistemas (por ejemplo, un Sparc). (Si bien técnicamente es un error, la mayoría de las personas no lo consideran un problema crítico).
Si realmente sientes la necesidad de usar un hack, ¿por qué no solo usar
struct hack {
char filler;
int things[1];
};
seguido por
hack_p = malloc(sizeof(struct hack)+(N-1)*sizeof int));
O ni siquiera te preocupes por el -1 y vive con un poco de espacio extra.