Une bonne façon de renvoyer un pointeur vers un objet `new` à partir d'une fonction Rcpp

9

Considérons 1) une classe personnalisée avec une impression de mémoire potentiellement volumineuse et 2) une fonction de niveau supérieur qui effectue un prétraitement, puis crée et renvoie un nouvel objet de notre classe personnalisée. Pour éviter une copie inutile par valeur, la fonction alloue l'objet et lui renvoie un pointeur à la place.

Sur la base d'une discussion précédente , il semble que la bonne façon de renvoyer un pointeur sur un objet nouvellement créé est de l'envelopper Rcpp::XPtr<>. Cependant, R le voit alors efficacement externalptr, et j'ai du mal à trouver la bonne façon de le mouler avec la manière moderne RCPP_EXPOSED_CLASSet la RCPP_MODULEfaçon de faire les choses.

L'alternative est de renvoyer le pointeur brut. Mais je ne suis pas sûr à 100% que la mémoire de l'objet soit correctement nettoyée. J'ai couru valgrindpour tester les fuites de mémoire et il n'en a pas trouvé. Mais qui fait le nettoyage? 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")
    ;
}

Dans 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

Ma question est de savoir si Rcpp::Xptr<>est la bonne façon de renvoyer des pointeurs, et si oui, comment puis-je obtenir R pour voir le résultat Double, non externalptr? Alternativement, si le retour d'un pointeur brut ne cause pas de problèmes de mémoire, qui nettoie l'objet créé par la fonction?

Artem Sokolov
la source
Oui, vous souhaitez probablement Rcpp::XPtrcréer un pointeur externe à partir du code C ++. Et vous voulez le faire faire double *ou quelle que soit votre charge utile. Il devrait y avoir des exemples ici, à la galerie, au GitHub ... Peut-être qu'avec une recherche motivée, vous pouvez trouver quelque chose d'assez proche?
Dirk Eddelbuettel
Salut @DirkEddelbuettel Le casting doit vraiment l'être CustomClass*. L'application réelle est une structure de données personnalisée sans équivalent R et toutes les interactions se font via la fonctionnalité exposée par le RCPP_MODULE. La correspondance la plus proche que ma recherche motivée a trouvée était un article d'il y a 7 ans , où il semble que je doive définir un template <> CustomClass* as()convertisseur. Cependant, je ne sais pas comment il devrait interagir avec RCPP_MODULEet RCPP_EXPOSED_CLASS, d'autant plus que je pensais que ce dernier était déjà défini wrap()et as().
Artem Sokolov
Le message de Romain de ce même fil est également très utile, mais malheureusement, il met en évidence l'utilisation d'objets directement, plutôt que la gestion des pointeurs.
Artem Sokolov
1
Je sais que j'ai fait des trucs similaires mais maintenant je ne sais pas quel est le meilleur exemple. Vous pouvez clairement configurer un objet 'singleton' et envelopper comme un module (RcppRedis); Je pense avoir fait ce que vous décrivez lors d'un ou deux emplois antérieurs, mais je ne peux pas penser à un bon exemple public maintenant. Là encore - les différents wrappers de base de données et le package d'accès le font. Ce n'est pas le plus petit sujet, alors peut-être commencer par une implémentation jouet / maquette et construire à partir de là?
Dirk Eddelbuettel
Utiliser RCPP_EXPOSED_CLASSet RCPP_MODULEest-ce vraiment le moyen de le faire? Je n'ai jamais utilisé ou vu cela auparavant.
F. Privé

Réponses:

7

Je pense qu'il est logique d'examiner les différentes approches séparément. Cela rend la distinction plus claire. Notez que cela est assez similaire à la discussion dans la vignette des modules Rcpp.

Lorsque Rcpp::XPtrvous utilisez, vous avez votre classe et fournissez des fonctions C ++ exportées pour chaque méthode que vous souhaitez exposer:

#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)
*/

Production:

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

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

> squareDouble(d2)
[1] 29.16

Notez que dans R, l'objet n'est qu'un "pointeur". Vous pouvez ajouter une classe S4 / RC / R6 / ... sur le côté R si vous voulez quelque chose de plus agréable.

Envelopper le pointeur externe dans une classe du côté R est quelque chose que vous obtenez gratuitement en utilisant les modules 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()
*/

Production:

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

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

> d1$square()
[1] 29.16

Il est également pris en charge d'utiliser une méthode d'usine au lieu d'un constructeur en C ++ mais avec une utilisation identique du côté 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()
*/

Production:

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

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

> d1$square()
[1] 29.16

Enfin, RCPP_EXPOSED_CLASSest pratique si vous souhaitez combiner une fonction d'usine côté R avec des modules Rcpp, car cela crée les extensions Rcpp::aset Rcpp::wrapnécessaires pour passer des objets entre R et C ++. L'usine pourrait être exportée via functioncomme vous l'avez fait ou en utilisant les attributs Rcpp, ce que je trouve plus naturel:

#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()
*/

Production:

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

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

> d1$square()
[1] 29.16

Concernant le nettoyage: les Rcpp::XPtrmodules Rcpp et Rcpp enregistrent un finaliseur par défaut qui appelle le destructeur de l'objet. Vous pouvez également ajouter un finaliseur personnalisé si nécessaire.

J'ai du mal à formuler une recommandation pour l'une de ces approches. Il est peut-être préférable d'essayer chacun d'eux sur un exemple simple et de voir ce que vous trouvez plus naturel à utiliser.

Ralf Stubner
la source
2
Très beau truc. Vous êtes sur une lancée ici.
Dirk Eddelbuettel
Je vous remercie. C'est extrêmement utile! Je pense que factoryc'est le connecteur clé qui me manquait.
Artem Sokolov
À titre de suivi, savez-vous s'il functionenregistre également un finaliseur, ou est-ce seulement factory ?
Artem Sokolov
1
@ArtemSokolov AFAIK le finaliseur par défaut qui appelle le destructeur est généré par class_<T>et est indépendant de la façon dont l'objet est créé.
Ralf Stubner