Maneira correta de retornar um ponteiro para um objeto `novo` a partir de uma função Rcpp

9

Considere 1) uma classe personalizada com uma impressão de memória potencialmente grande e 2) uma função de nível superior que realiza algum pré-processamento, depois cria e retorna um novo objeto de nossa classe personalizada. Para evitar cópias desnecessárias por valor, a função aloca o objeto e retorna um ponteiro para ele.

Com base em uma discussão anterior , parece que a maneira correta de retornar um ponteiro para um objeto recém-criado é envolvê-lo Rcpp::XPtr<>. No entanto, R o vê efetivamente como externalptr, e estou lutando para encontrar a maneira correta de interpretá-la com a maneira moderna RCPP_EXPOSED_CLASSe RCPP_MODULEde fazer as coisas.

A alternativa é retornar o ponteiro bruto. Mas não tenho 100% de certeza de que a memória do objeto seja devidamente limpa. Corri valgrindpara testar vazamentos de memória e não encontrei nenhum. No entanto, quem faz a limpeza? 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")
    ;
}

Em 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

Minha pergunta é se Rcpp::Xptr<>é a maneira correta de retornar ponteiros e, se sim, como faço para que R veja o resultado como Doublenão externalptr? Como alternativa, se o retorno de um ponteiro bruto não causa problemas de memória, quem limpa o objeto que a função cria?

Artem Sokolov
fonte
Sim, você provavelmente deseja Rcpp::XPtrcriar um ponteiro externo a partir do código C ++. E você deseja transmiti-lo double *ou seja qual for sua carga útil. Deve haver exemplos aqui, na Galeria, no GitHub ... Talvez com uma pesquisa motivada você possa encontrar algo próximo o suficiente?
Dirk Eddelbuettel
Oi @DirkEddelbuettel O elenco realmente precisa ser CustomClass*. O aplicativo real é uma estrutura de dados customizada sem equivalente em R e todas as interações são feitas através da funcionalidade exposta pelo RCPP_MODULE. A correspondência mais próxima que minha pesquisa motivada encontrou foi uma postagem de 7 anos atrás , onde parece que eu preciso definir um template <> CustomClass* as()conversor. No entanto, não estou claro como ele deve interagir RCPP_MODULEe RCPP_EXPOSED_CLASS, principalmente porque pensei que o último já definiu wrap()e as().
Artem Sokolov
A publicação de Romain desse mesmo tópico também é muito útil, mas infelizmente destaca diretamente o uso de objetos, em vez de manipular ponteiros.
Artem Sokolov
11
Eu sei que fiz coisas semelhantes, mas agora não sei qual é o melhor exemplo disso. Você pode configurar claramente um objeto 'singleton' e agrupar como um módulo (RcppRedis); Acho que fiz o que você descreveu em um ou dois empregos anteriores, mas não consigo pensar em um bom exemplo público agora. Então, novamente - os vários wrappers de banco de dados e pacote de acesso fazem isso. Não é o menor tópico, então talvez comece com uma implementação de brinquedo / simulação e construa a partir daí?
Dirk Eddelbuettel
Usando RCPP_EXPOSED_CLASSe RCPP_MODULEé realmente a maneira de fazê-lo? Eu nunca usei ou vi isso antes.
F. Privé

Respostas:

7

Eu acho que faz sentido olhar para as diferentes abordagens separadamente. Isso torna a distinção mais clara. Observe que isso é bastante semelhante à discussão na vinheta dos módulos Rcpp.

Ao usar, Rcpp::XPtrvocê tem sua classe e fornece funções C ++ exportadas para todos os métodos que você deseja expor:

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

Resultado:

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

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

> squareDouble(d2)
[1] 29.16

Observe que em R o objeto é apenas um "ponteiro". Você pode adicionar uma classe S4 / RC / R6 / ... no lado R se quiser algo mais agradável.

Agrupar o ponteiro externo em uma classe no lado R é algo que você obtém gratuitamente usando os 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()
*/

Resultado:

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

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

> d1$square()
[1] 29.16

Também é suportado o uso de um método de fábrica em vez de um construtor em C ++, mas com uso idêntico no 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()
*/

Resultado:

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

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

> d1$square()
[1] 29.16

Por fim, RCPP_EXPOSED_CLASSé útil se você deseja combinar uma função de fábrica do lado R com os módulos Rcpp, pois isso cria as extensões Rcpp::ase Rcpp::wrapnecessárias para passar os objetos entre R e C ++. A fábrica pode ser exportada via functioncomo você fez ou usando os Atributos Rcpp, o que acho mais 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()
*/

Resultado:

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

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

> d1$square()
[1] 29.16

Em relação à limpeza: Rcpp::XPtrOs módulos Rcpp e Rcpp registram um finalizador padrão que chama o destruidor do objeto. Você também pode adicionar um finalizador personalizado, se necessário.

Acho difícil fazer uma recomendação para uma dessas abordagens. Talvez seja melhor tentar cada um deles em um exemplo simples e ver o que você acha mais natural de usar.

Ralf Stubner
fonte
2
Coisas muito legais. Você está em um rolo aqui.
Dirk Eddelbuettel
Obrigado. Isso é extremamente útil! Eu acho que factoryé a peça chave do conector que estou perdendo.
Artem Sokolov
Como um pequeno acompanhamento, você sabe se functiontambém registra um finalizador, ou é apenas factory ?
Artem Sokolov
11
@ArtemSokolov AFAIK, o finalizador padrão que chama o destruidor é gerado class_<T>e é independente de como o objeto é criado.
Ralf Stubner