class Agent {
public:
int fun();
};
// Declaración de la clase Model
class Model {
private:
Agent agent_;
int x_;
public:
inline static Model * instance_ = nullptr;
virtual int run();
Model(int x) : agent_(Agent()), x_(x) {};
// Función clave que permite acceder a `Model`
// desde llamadas dentro de la pila de ejecución.
static Model & get();
int get_x();
};
// Implementación de los métodos de Model
inline int Model::run() {
Model::instance_ = this;
return agent_.fun();
};
inline Model & Model::get() {return *instance_;};
inline int Model::get_x() {return x_;};
// Implementación del método de Agent
inline int Agent::fun() {return Model::get().get_x();};
// [[Rcpp::export]]
int test_scoping_cpp() {
// Crear una instancia de Model
Model Model(42);
// Ejecutar el Model
return Model.run();
}
// [[Rcpp::export]]
bool is_it_on() {
return Model::instance_ != nullptr;
}Appendix A — Notas sobre alcance en C++
Este capítulo presenta un tema más avanzado pero importante en C++: RAII y alcance (scoping). RAII significa Resource Acquisition Is Initialization, y es un patrón de programación que asegura que los recursos se liberen correctamente cuando ya no se necesitan. Desde la perspectiva de una persona usuaria de R, esto ofrece una buena oportunidad para trabajar con alcance en C++. Veámoslo con un ejemplo:
Comprobando si funciona o no:
test_scoping_cpp()[1] 42
¡Funciona! Separamos esto en partes. Primero, la clase Agent no tiene acceso explícito a la clase Model; en ninguna parte de su definición aparece como miembro. La clave está en la función static Model::get(), que puede ser llamada por cualquier función dentro de la pila de ejecución sin tener acceso directo a Model:
Model::run() -> Agent::fun() -> Model::get() -> Model::get_x()
Y la función de Agent accede al modelo mediante el miembro estático. Además, podemos ver que después de salir de Model::run(), la variable Model::instance_ sigue siendo no nula:
is_it_on()[1] TRUE
Ahora, esto no es completamente seguro, porque la variable instance_ seguiría disponible incluso después de llamar a la función Model::run(). Para resolverlo, podemos usar una clase auxiliar que restablezca instance_ a nullptr:
#include <Rcpp.h>
class Agent {
public:
int fun();
};
class Scope;
// Declaración de la clase Model
class Model_s {
friend Scope;
private:
Agent agent_;
int x_;
public:
inline static Model_s * instance_ = nullptr;
virtual int run();
Model_s(int x) : agent_(Agent()), x_(x) {};
static Model_s & get();
int get_x();
};
// Declaración e implementación del método Scope
class Scope {
private:
Model_s * prev_;
public:
explicit Scope(Model_s * model) : prev_(Model_s::instance_) {
Model_s::instance_ = model;
};
~Scope() {
Model_s::instance_ = prev_;
};
};
// Implementación de los métodos de Model_s
inline int Model_s::run() {
// Aquí está el punto importante. El constructor
// de `Scope` fija el valor de `instance_` para
// que sea la instancia actual de `Model`, pero
// una vez que salimos de la llamada, el destructor
// reasigna `instance_` a `nullptr`.
Scope scope(this);
return agent_.fun();
};
inline Model_s & Model_s::get() {return *instance_;};
inline int Model_s::get_x() {return x_;};
// Implementación del método de Agent
inline int Agent::fun() {return Model_s::get().get_x();};
// [[Rcpp::export]]
int test_scoping_cpp2() {
// Crear una instancia de Model
Model_s Model(42);
// Ejecutar el Model
return Model.run();
}
// [[Rcpp::export]]
bool is_it_on2() {
return Model_s::instance_ != nullptr;
}La clase simple Scope ofrece una forma elegante de controlar la duración de esta variable estática:
class Scope {
private:
Model_s * prev_;
public:
explicit Scope(Model_s * model) : prev_(Model_s::instance_) {
Model_s::instance_ = model;
};
~Scope() {
Model_s::instance_ = prev_;
};
};Al invocarse, establece el valor del miembro estático de Model_s durante la vida de Scope. Una vez que Model_s::run() termina, el destructor de Scope restablece el valor de Model_s::instance_ a su valor previo, nullptr en este caso. Aun así, esto permite llamadas anidadas, manteniendo el valor correcto de instance_ a medida que crece la pila de ejecución. Este es el resultado de la nueva implementación:
test_scoping_cpp2() # Verificando que funciona[1] 42
is_it_on2() # Y que queda apagado[1] FALSE
Mientras implementaba esto, estaba usando Model en ambos bloques de código C++. Aunque igual compila, el símbolo Model se comparte entre ambas implementaciones, lo que rompía mi código (la función is_it_on2() devolvía TRUE cuando debía devolver FALSE).
Versiones multihilo
Si usas computación paralela con OpenMP y similares, es importante ser cuidadoso. Por defecto, las copias de Model comparten el valor de _instance, lo que puede no ser deseable. En su lugar, se recomienda usar la palabra clave thread_local:
class Model {
static thread_local instance_;
}
// Debe inicializarse fuera de la clase
thread_local Model::instance_ = nullptr;Esto asegura que, si creas copias de Model, cada hilo tenga acceso a su propia copia.
Trabajando con múltiples versiones compiladas
Otra advertencia que he experimentado ocurre cuando hay dos objetos de biblioteca C++ diferentes (lo que Rcpp crea como .so, .o o .dll, según el sistema operativo). En particular, observé esto durante el desarrollo de los paquetes epiworldR y measles en R. Ambos tenían bibliotecas compiladas que dependían de la biblioteca de plantillas C++ epiworld, así que al cargarlos juntos, el acceso mediante miembro estático no funcionaba como se esperaba; en esos casos, es mejor ser más explícito y simplemente pasar Model como otro argumento; en otras palabras, no intentes ser “demasiado inteligente” y usa algo más confiable.