c++ - ejemplo - ¿Hay alguna forma de hacer cumplir que las instancias solo están en la pila?
c++ lambda capture (3)
Tengo una clase de C ++ para la cual solo quiero que se cree una instancia en la pila. Estoy usando una API para acceder al contenido que se desarrolló en otro idioma (interpretado) que viene con su propia recolección de basura. Los mecanismos en este lenguaje saben lo suficiente como para dejar cualquier contenido al que solo haga referencia en la pila, y dado que esta clase nativa contiene dicha referencia, es de vital importancia que para un comportamiento correcto, el usuario de la clase nativa de C ++ lo haga. nunca trate de asignar una instancia en ningún otro lado.
Tenga en cuenta que no solo quiero prohibir que la instancia de mi clase se asigne con nueva (si eso fuera todo lo que necesitaba hacer, podría sobrecargar el new
operador de la clase y hacerlo privado, o eliminarlo explícitamente desde C ++ 11) , pero también para rechazar cualquier instancia global estática o posible de la clase también. La única forma válida de crear una instancia segura de esta clase debería estar en la pila, y me gustaría garantizarlo de alguna manera. Por lo que sé, hacer una new
privacidad o eliminarla tampoco impide que otra clase se declare con mi clase como una variable miembro y una instancia de la que se asigna en el montón.
La forma en que estoy gestionando esto ahora es tener la palabra "Local" como parte del nombre de la clase como un recordatorio para el usuario de que la instancia solo está destinada a ser utilizada en la pila, pero por supuesto, esto no es En realidad, el compilador o cualquier otro mecanismo lo impone, y preferiría una solución que sea más exigible.
Idealmente, quiero asegurar esto en el momento de la compilación y no compilarlo si se usa incorrectamente. Si esto simplemente no es posible, lanzar una excepción en tiempo de ejecución cuando se construye la instancia sigue siendo un respaldo aceptable. Las soluciones que funcionan en C ++ 11 o C ++ 14 están bien.
Tenga en cuenta que esta pregunta definitivamente NO es la misma que this , que solo quería evitar allocaton con new
Descargo de responsabilidad: ''stack'' no es parte del estándar c ++ que yo sepa, ahí tenemos ASDV (variables de duración de almacenamiento automático). ABI podría definir stack. Tenga en cuenta que a veces estos se pasan en registros, lo que creo que está bien en su caso.
Definir un método de fábrica CPS (estilo de paso de continuación):
class A {
public:
template<typename F, typename... Args>
static auto cps_make(F f, Args&&... args) {
return f(A(std::forward<Args>(args)...));
}
private:
A(/* ... */) {}
A(const A&) = delete;
A(A&&) = delete;
};
Uso: pase una lambda tomando A y los parámetros de ctor de AEg
return A::cps_make([&](A a) {
/* do something with a */
return true;
});
Los argumentos de función son siempre ASDV dentro.
Cómo funciona el código: cps_make toma un funtor (generalmente un lambda) que toma una instancia del tipo dado; y parámetros ctor opcionales. Crea la instancia (reenviando cualquier parámetro opcional al ctor), llama al funtor y devuelve lo que devuelve el funtor. Como el funtor puede ser un lambda en C ++ 11, no rompe el flujo de código normal.
La belleza de CPS es que puedes tener un polimorfismo estático simplemente usando un auto-lambda en C ++ 14: tu cps_make () puede crear casi todo lo que desees (jerarquía, variante, cualquier, etc.). Luego, guarda la sobrecarga virtual para las jerarquías cerradas. Incluso puedes tener un lambda para flujo normal y uno si ctor fallará; esto es útil cuando las excepciones son no-go.
El inconveniente es que actualmente no se pueden usar directamente las instrucciones de flujo de control del alcance externo dentro de la lambda. / * Sugerencia: estamos trabajando en ello. * /
Está bien, así que aquí está mi opinión:
struct stack_marker
{
thread_local static uint8_t* marker;
uint8_t stk;
stack_marker()
{
if (marker != nullptr)
{
throw std::runtime_error("second twice marker! don''t do it");
}
marker = &stk;
}
};
thread_local uint8_t* stack_marker::marker = nullptr;
void sort3(uint8_t* (&a)[3]); //sorts 3 pointers, see gist
class only_on_stack
{
uint8_t place;
public:
NO_INLINE only_on_stack(int x)
{
uint8_t a;
if (!stack_marker::marker)
{
// not initialized yet, either forgot to put stack_marker in main
// or we are running before main, which is static storage
//throw std::runtime_error("only on stack object created in non-stack");
std::cout << x << ": I''m NOT on stack/n";
return;
}
uint8_t* ptrs[] = {
stack_marker::marker,
&place,
&a
};
sort3(ptrs);
if (ptrs[1] == &place) // place must be in the middle
{
std::cout << x << ": I''m on stack/n";
}
else
{
//throw std::runtime_error("only_on_stack object created in non-stack");
std::cout << x << ": I''m NOT on stack/n";
}
}
};
only_on_stack static_storage(1);
thread_local only_on_stack tl_storage(4);
int NO_INLINE stuff()
{
only_on_stack oos(2);
}
int main()
{
stack_marker mrk;
stuff();
auto test = new only_on_stack(3);
tl_storage; // access thread local to construct, or gcc omits it
}
Es cierto que mi solución no es la más limpia de todas, pero te permite seguir usando la sintaxis de objeto local normal.
Básicamente, el truco consiste en poner 2 objetos adicionales en la pila, aparte de nuestro objeto: uno al principio del hilo y otro al constructor. Por lo tanto, uno de los objetos se crea en la pila después de nuestro objeto y uno de ellos antes . Con esta información, podríamos simplemente verificar el orden de las direcciones de estos 3 objetos. Si el objeto está realmente en la pila, la dirección debe estar en el medio.
Sin embargo, C ++ no define el orden de direcciones de los objetos en un alcance de función, por lo tanto, hacer algo como esto:
int main()
{
int a;
int b;
int c;
}
No garantiza que &b
esté en el medio de &a
y &c
.
Para solucionar este problema, podemos mantener a
en la función principal y mover c
en una función de forzado no alineado diferente :
void NO_INLINE foo()
{
int b;
int c;
}
int main()
{
int a;
foo();
}
En este caso, dado que el compilador no puede conocer las variables locales de foo
en main
, &a
> &b
, &c
o &a
< &b
, &c
. Al aplicar lo mismo a c
moviéndolo a otra función no alineable, podríamos garantizar que & b se encuentra en medio de &a
y &c
.
En mi implementación, la función de stuff
es la función foo
y la función a la que movemos c
es el constructor de only_on_stack
.
La implementación real de trabajo está aquí: https://gist.github.com/FatihBAKIR/dd125cf4f06cbf13bb4434f79e7f1d43
Debería funcionar si la pila crece hacia abajo o hacia arriba, independientemente del tipo de archivo objeto y, con suerte, de la ABI, siempre que el compilador no reordene las variables locales de las funciones no en línea.
Esto se probó con -O3
en g ++ - 6 en Linux y el último sonido en mac os x. Debería funcionar en MSVC, ojalá alguien pueda probarlo.
La salida de ambos es:
1: I''m NOT on stack
2: I''m on stack
3: I''m NOT on stack
4: I''m NOT on stack
Básicamente, el uso es un objeto stack_marker
al principio de cada subproceso ( main
incluido), se llama a otra función que no se puede alinear y se usa como su punto de entrada real.
Una posibilidad es permitir solo variables temporales (con tiempo de vida prolongado), algo así como:
class A
{
private:
A() = default;
A(const A&) = delete;
A(A&&) = delete;
A& operator =(const A&) = delete;
A& operator =(A&&) = delete;
public:
static A Make() { return {}; }
};
auto&& g = A::Make(); // possible :/
int main() {
auto&& a = A::Make(); // possible
#if 0
new A(); // error
struct InnerA
{
A a; // error
};
#endif
}
Ya no será válido en C ++ 17 con elisión de copia garantizada.