Forma correcta de devolver un puntero a un objeto `nuevo` desde una función Rcpp

9

Considere 1) una clase personalizada con una impresión de memoria potencialmente grande, y 2) una función de nivel superior que realiza un preprocesamiento, luego crea y devuelve un nuevo objeto de nuestra clase personalizada. Para evitar una copia innecesaria por valor, la función asigna el objeto y le devuelve un puntero.

Según una discusión anterior , parece que la forma correcta de devolver un puntero a un objeto recién creado es envolverlo Rcpp::XPtr<>. Sin embargo, R lo ve efectivamente externalptr, y estoy luchando por encontrar la forma adecuada de lanzarlo con lo moderno RCPP_EXPOSED_CLASSy lo moderno.RCPP_MODULE forma de hacer las cosas.

La alternativa es devolver el puntero sin formato. Pero entonces no estoy 100% seguro de que la memoria del objeto se limpie correctamente. Corrí valgrinda buscar pérdidas de memoria y no encontré ninguna. Sin embargo, ¿quién hace la limpieza? R?

test.cpp

#include <Rcpp.h>

// Custom class
class Double {
public:
  Double( double v ) : value(v) {}
  double square() {return value*value;}
private:
  double value;
};

// Make the class visible
RCPP_EXPOSED_CLASS(Double)

// Option 1: returning raw pointer
Double* makeDouble( double x ) {
  Double* pd = new Double(x);
  return pd;
}

// Option 2: returning XPtr<>
SEXP makeDouble2( double x ) {
  Double* pd = new Double(x);
  Rcpp::XPtr<Double> ptr(pd);
  return ptr;
}

RCPP_MODULE(double_cpp) {
  using namespace Rcpp;

  function( "makeDouble", &makeDouble );
  function( "makeDouble2", &makeDouble2 );

  class_<Double>("Double")
    .constructor<double>("Wraps a double")
    .method("square", &Double::square, "square of value")
    ;
}

En R

Rcpp::sourceCpp("test.cpp")
d1 <- makeDouble(5.4)     # <-- who cleans this up???
# C++ object <0x56257d628e70> of class 'Double' <0x56257c69cf90>
d1$square()
# 29.16

d2 <- makeDouble2(2.3)
# <pointer: 0x56257d3c3cd0>
d2$square()
# Error in d2$square : object of type 'externalptr' is not subsettable

Mi pregunta es si Rcpp::Xptr<>es la forma correcta de devolver punteros, y si es así, ¿cómo hago para que R vea el resultado Double, no externalptr? Alternativamente, si devolver un puntero sin formato no causa problemas de memoria, ¿quién limpia el objeto que crea la función?

Artem Sokolov
fuente
Sí, probablemente desee Rcpp::XPtrcrear un puntero externo a partir del código C ++. Y quieres lanzarlo double *o lo que sea tu carga útil. Debería haber ejemplos aquí, en la Galería, en GitHub ... ¿Quizás con una búsqueda motivada pueda encontrar algo lo suficientemente cerca?
Dirk Eddelbuettel
Hola @DirkEddelbuettel El elenco realmente necesita ser CustomClass*. La aplicación real es una estructura de datos personalizada sin equivalente de R y todas las interacciones se realizan a través de la funcionalidad expuesta por RCPP_MODULE. La coincidencia más cercana que encontró mi búsqueda motivada fue una publicación de hace 7 años , donde parece que necesito definir un template <> CustomClass* as()convertidor. Sin embargo, no tengo claro cómo debería interactuar RCPP_MODULEy RCPP_EXPOSED_CLASS, especialmente porque pensé que este último ya había definido wrap()y as().
Artem Sokolov
La publicación de Romain de ese mismo hilo también es muy útil, pero desafortunadamente destaca el uso de objetos directamente, en lugar del manejo de punteros.
Artem Sokolov
1
Sé que he hecho cosas similares, pero ahora no estoy seguro de cuál es el mejor ejemplo. Puede configurar claramente un objeto 'singleton' y ajustarlo como un módulo (RcppRedis); Creo que he hecho lo que usted describe en uno o dos trabajos anteriores, pero ahora no puedo pensar en un buen ejemplo público. Por otra parte, los diversos contenedores de bases de datos y el paquete de acceso lo hacen. No es el tema más pequeño, así que ¿tal vez comenzar con una implementación de juguete / simulación y construir desde allí?
Dirk Eddelbuettel
Utilizando RCPP_EXPOSED_CLASSy RCPP_MODULEes realmente la forma de hacerlo? Nunca he usado o visto eso antes.
F. Privé

Respuestas:

7

Creo que tiene sentido mirar los diferentes enfoques por separado. Esto hace que la distinción sea más clara. Tenga en cuenta que esto es bastante similar a la discusión en la viñeta Módulos Rcpp.

Al usar Rcpp::XPtr, tiene su clase y proporciona funciones de C ++ exportadas para cada método que desea exponer:

#include <Rcpp.h>

// Custom class
class Double {
public:
    Double( double v ) : value(v) {}
    double square() {return value*value;}
private:
    double value;
};

// [[Rcpp::export]]
Rcpp::XPtr<Double> makeDouble(double x) {
    Double* pd = new Double(x);
    Rcpp::XPtr<Double> ptr(pd);
    return ptr;
}

// [[Rcpp::export]]
double squareDouble(Rcpp::XPtr<Double> x) {
    return x.get()->square();
}

/***R
(d2 <- makeDouble(5.4))
squareDouble(d2)
*/

Salida:

> Rcpp::sourceCpp('59384221/xptr.cpp')

> (d2 <- makeDouble(5.4))
<pointer: 0x560366699b50>

> squareDouble(d2)
[1] 29.16

Tenga en cuenta que en R el objeto es solo un "puntero". Puede agregar una clase S4 / RC / R6 / ... en el lado R si desea algo más agradable.

Ajustar el puntero externo en una clase en el lado R es algo que obtienes gratis al usar módulos Rcpp:

#include <Rcpp.h>

// Custom class
class Double {
public:
    Double( double v ) : value(v) {}
    double square() {return value*value;}
private:
    double value;
};

RCPP_MODULE(double_cpp) {
    using namespace Rcpp;

    class_<Double>("Double")
        .constructor<double>("Wraps a double")
        .method("square", &Double::square, "square of value")
    ;
}

/***R
(d1 <- new(Double, 5.4))
d1$square()
*/

Salida:

> Rcpp::sourceCpp('59384221/modules.cpp')

> (d1 <- new(Double, 5.4))
C++ object <0x560366452eb0> of class 'Double' <0x56036480f320>

> d1$square()
[1] 29.16

También se admite el uso de un método de fábrica en lugar de un constructor en C ++ pero con un uso idéntico en el lado R:

#include <Rcpp.h>

// Custom class
class Double {
public:
    Double( double v ) : value(v) {}
    double square() {return value*value;}
private:
    double value;
};

Double* makeDouble( double x ) {
    Double* pd = new Double(x);
    return pd;
}

RCPP_MODULE(double_cpp) {
    using namespace Rcpp;

    class_<Double>("Double")
        .factory<double>(makeDouble, "Wraps a double")
        .method("square", &Double::square, "square of value")
    ;
}

/***R
(d1 <- new(Double, 5.4))
d1$square()
*/

Salida:

> Rcpp::sourceCpp('59384221/modules-factory.cpp')

> (d1 <- new(Double, 5.4))
C++ object <0x5603665aab80> of class 'Double' <0x5603666eaae0>

> d1$square()
[1] 29.16

Finalmente, RCPP_EXPOSED_CLASSresulta útil si desea combinar una función de fábrica del lado R con los módulos Rcpp, ya que esto crea las extensiones Rcpp::asy Rcpp::wrapnecesarias para pasar los objetos de un lado a otro entre R y C ++. La fábrica se puede exportar a través de functioncomo lo hizo o utilizando los atributos de Rcpp, lo que me parece más natural:

#include <Rcpp.h>

// Custom class
class Double {
public:
    Double( double v ) : value(v) {}
    double square() {return value*value;}
private:
    double value;
};

// Make the class visible
RCPP_EXPOSED_CLASS(Double)

// [[Rcpp::export]]
Double makeDouble( double x ) {
    Double d(x);
    return d;
}

RCPP_MODULE(double_cpp) {
    using namespace Rcpp;

    class_<Double>("Double")
        .method("square", &Double::square, "square of value")
    ;
}

/***R
(d1 <- makeDouble(5.4))
d1$square()
*/

Salida:

> Rcpp::sourceCpp('59384221/modules-expose.cpp')

> (d1 <- makeDouble(5.4))
C++ object <0x560366ebee10> of class 'Double' <0x560363d5f440>

> d1$square()
[1] 29.16

Con respecto a la limpieza: Ambos Rcpp::XPtrmódulos y Rcpp registran un finalizador predeterminado que llama al destructor del objeto. También puede agregar un finalizador personalizado si es necesario.

Me resulta difícil dar una recomendación para uno de estos enfoques. Quizás sea mejor probar cada uno de ellos con un ejemplo simple y ver qué le parece más natural de usar.

Ralf Stubner
fuente
2
Muy buenas cosas. Estás en una buena racha aquí.
Dirk Eddelbuettel
Gracias. ¡Esto es extremadamente útil! Creo que factoryes la pieza clave del conector que me he estado perdiendo.
Artem Sokolov
Como pequeño seguimiento, ¿sabes si functiontambién registra un finalizador, o es solo factory ?
Artem Sokolov
1
@ArtemSokolov AFAIK el finalizador predeterminado que llama al destructor es generado por class_<T>y es independiente de cómo se crea el objeto.
Ralf Stubner