programacion nyquist indique frecuencia filtro ejemplos efecto cómo c++ gcc c++14

c++ - nyquist - Almacenamiento de pila de objetos pequeños, regla de aliasing estricto y comportamiento indefinido



frecuencia de nyquist (3)

La mejor opción es usar la instalación provista por el estándar para el almacenamiento alineado para la creación de objetos, que se denomina aligned_storage :

std::aligned_storage_t<64, sizeof(void*)> c; // ... new(&c) F(std::forward<F>(f)); reinterpret_cast<T*>(&c)->operator()(); reinterpret_cast<T*>(&c)->~T();

Example.

Si está disponible, debe usar std::launder para envolver su reinterpret_cast s: ¿Cuál es el propósito de std :: launder? ; Si std::launder no está disponible, puede asumir que su compilador es anterior a P0137 y que los reinterpret_cast s son suficientes según la regla de "puntos a" ( [basic.compound] / 3). Puedes probar std::launder #ifdef __cpp_lib_launder usando #ifdef __cpp_lib_launder ; example

Dado que se trata de una instalación estándar, tiene la garantía de que si la usa de acuerdo con la descripción de la biblioteca (es decir, como se indica anteriormente), no existe peligro de quemaduras.

Como beneficio adicional, esto también garantizará que se supriman todas las advertencias del compilador.

Un peligro que no cubre la pregunta original es que está convirtiendo la dirección de almacenamiento en un tipo base polimórfica de su tipo derivado. Esto solo es correcto si se asegura de que la base polimórfica tenga la misma dirección ( [ptr.launder] / 1: "Un objeto X que está dentro de su vida útil [...] se encuentra en la dirección A ") que el objeto completo en el momento de la construcción, ya que esto no está garantizado por la Norma (ya que un tipo polimórfico no es de diseño estándar). Puedes verificar esto con una assert :

auto* p = new(&c) derived<F>(std::forward<F>(f)); assert(static_cast<base*>(p) == std::launder(reinterpret_cast<base*>(&c)));

Sería más limpio utilizar la herencia no polimórfica con un manual vtable, como propone Yakk, ya que entonces la herencia será de diseño estándar y se garantiza que el subobjeto de la clase base tendrá la misma dirección que el objeto completo.

Si analizamos la implementación de aligned_storage , es equivalente a su alignas(sizeof(void*)) char c[64] , simplemente envuelto en una struct , y de hecho gcc puede cerrarse envolviendo su char c[64] una struct aunque estrictamente hablando después de P0137, debe usar caracteres unsigned char lugar de caracteres simples. Sin embargo, esta es un área de la Norma en rápida evolución, y esto podría cambiar en el futuro. Si utiliza el servicio proporcionado, tiene una mejor garantía de que seguirá funcionando.

Estoy escribiendo una envoltura de función borrada de tipo similar a std::function . (Sí, he visto implementaciones similares e incluso la propuesta p0288r0 , pero mi caso de uso es bastante limitado y algo especializado). El código muy simplificado a continuación ilustra mi implementación actual:

class Func{ alignas(sizeof(void*)) char c[64]; //align to word boundary struct base{ virtual void operator()() = 0; virtual ~base(){} }; template<typename T> struct derived : public base{ derived(T&& t) : callable(std::move(t)) {} void operator()() override{ callable(); } T callable; }; public: Func() = delete; Func(const Func&) = delete; template<typename F> //SFINAE constraints skipped for brevity Func(F&& f){ static_assert(sizeof(derived<F>) <= sizeof(c), ""); new(c) derived<F>(std::forward<F>(f)); } void operator () (){ return reinterpret_cast<base*>(c)->operator()(); //Warning } ~Func(){ reinterpret_cast<base*>(c)->~base(); //Warning } };

Compiled , GCC 6.1 advierte sobre un strict-aliasing :

warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing] return reinterpret_cast<T*>(c)->operator()();

También sé acerca de la strict-aliasing . Por otro lado, actualmente no conozco una mejor manera de hacer uso de la optimización de la pila de objetos pequeños. A pesar de las advertencias, todas mis pruebas pasan a GCC y Clang, (y un nivel adicional de indirección evita la advertencia de GCC). Mis preguntas son:

  • ¿Me quemaré eventualmente ignorando la advertencia de este caso?
  • ¿Hay una mejor manera de crear objetos en el lugar?

Ver ejemplo completo: Compiled


La otra respuesta es básicamente reconstruir lo que la mayoría de los compiladores hacen bajo el capó. Cuando almacena el puntero devuelto por la ubicación nueva, no es necesario crear vtables manualmente:

class Func{ struct base{ virtual void operator()() = 0; virtual ~base(){} }; template<typename T> struct derived : public base{ derived(T&& t) : callable(std::move(t)) {} void operator()() override{ callable(); } T callable; }; std::aligned_storage_t<64 - sizeof(base *), sizeof(void *)> data; base * ptr; public: Func() = delete; Func(const Func&) = delete; template<typename F> //SFINAE constraints skipped for brevity Func(F&& f){ static_assert(sizeof(derived<F>) <= sizeof(data), ""); ptr = new(static_cast<void *>(&data)) derived<F>(std::forward<F>(f)); } void operator () (){ return ptr->operator()(); } ~Func(){ ptr->~base(); } };

Pasar de derived<T> * a base * derived<T> * es perfectamente válido (N4431 §4.10 / 3):

Un prvalor de tipo "puntero a cv D", donde D es un tipo de clase, se puede convertir a un prvalor de tipo "puntero a cv B", donde B es una clase base (Cláusula 10) de D. [..]

Y como las funciones miembro respectivas son virtuales, llamarlas a través del puntero base en realidad llama a las funciones respectivas en la clase derivada.


Primero, usa std::aligned_storage_t . Para eso está destinado.

En segundo lugar, el tamaño exacto y el diseño de virtual tipos virtual y sus descendientes están determinados por el compilador. La asignación de una clase derivada en un bloque de memoria y luego la conversión de la dirección de ese bloque a un tipo base puede funcionar, pero no hay ninguna garantía de que funcionará en el estándar.

En particular, si tenemos la struct A {}; struct B:A{}; struct A {}; struct B:A{}; no hay garantía a menos que sea un diseño estándar de que un puntero a B puede ser reintepret como un puntero a A (especialmente a través de un void* ). Y las clases con s virtual en ellas no son de diseño estándar.

Así que la reinterpretación es un comportamiento indefinido.

Podemos solucionar esto.

struct func_vtable { void(*invoke)(void*) = nullptr; void(*destroy)(void*) = nullptr; }; template<class T> func_vtable make_func_vtable() { return { [](void* ptr){ (*static_cast<T*>(ptr))();}, // invoke [](void* ptr){ static_cast<T*>(ptr)->~T();} // destroy }; } template<class T> func_vtable const* get_func_vtable() { static const auto vtable = make_func_vtable<T>(); return &vtable; } class Func{ func_vtable const* vtable = nullptr; std::aligned_storage_t< 64 - sizeof(func_vtable const*), sizeof(void*) > data; public: Func() = delete; Func(const Func&) = delete; template<class F, class dF=std::decay_t<F>> Func(F&& f){ static_assert(sizeof(dF) <= sizeof(data), ""); new(static_cast<void*>(&data)) dF(std::forward<F>(f)); vtable = get_func_vtable<dF>(); } void operator () (){ return vtable->invoke(&data); } ~Func(){ if(vtable) vtable->destroy(&data); } };

Esto ya no se basa en las garantías de conversión de puntero. Simplemente requiere que void_ptr == new( void_ptr ) T(blah) .

Si está realmente preocupado por un alias estricto, almacene el valor de retorno de la new expresión como un void* , y transfiéralo a invoke y destroy lugar de &data . Eso va a ser irreprochable: el puntero devuelto desde new es el puntero al objeto recién construido. El acceso a los data cuya vida útil ha finalizado es probablemente inválido, pero antes también era inválido.

Cuando los objetos comienzan a existir y cuando terminan, el estándar es relativamente difuso. El último intento que he visto para resolver este problema es P0137-R1 , donde introduce T* std::launder(T*) para que los problemas de alias desaparezcan de una manera extremadamente clara.

El almacenamiento del puntero devuelto por new es la única forma que conozco de manera clara y sin ambigüedades que no se encuentre con ningún problema de alias de objetos antes de P0137.

La norma decía:

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

la pregunta es "¿la nueva expresión garantiza realmente que el objeto se crea en la ubicación en cuestión?" No pude convencerme a mí mismo de que afirmara tan inequívocamente. Sin embargo, en mis propias implementaciones de borrado de tipo, no almaceno ese puntero.

Prácticamente, lo anterior va a hacer lo mismo que muchas implementaciones de C ++ hacen con las tablas de funciones virtuales en casos simples como este, excepto que no se crea un RTTI.