Właściwy sposób na zwrócenie wskaźnika do obiektu `new` z funkcji Rcpp

9

Rozważ 1) klasę niestandardową z potencjalnie dużym drukiem pamięci oraz 2) funkcję najwyższego poziomu, która wykonuje wstępne przetwarzanie, a następnie tworzy i zwraca nowy obiekt naszej klasy niestandardowej. Aby uniknąć niepotrzebnego kopiowania według wartości, funkcja przydziela obiekt i zwraca do niego wskaźnik.

Na podstawie poprzedniej dyskusji wydaje się, że właściwym sposobem na zwrócenie wskaźnika do nowo utworzonego obiektu jest owinięcie go Rcpp::XPtr<>. Jednak R następnie widzi to skutecznie jako externalptr, i staram się znaleźć właściwy sposób, aby rzucić to na nowoczesność RCPP_EXPOSED_CLASSi RCPP_MODULEsposób robienia rzeczy.

Alternatywą jest zwrócenie surowego wskaźnika. Ale nie jestem w 100% pewien, że pamięć obiektów zostanie odpowiednio wyczyszczona. Pobiegłem valgrindsprawdzić, czy nie ma wycieków pamięci, ale nie znalazłem żadnych. Kto jednak sprząta? 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")
    ;
}

W 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

Moje pytanie brzmi: czy Rcpp::Xptr<>jest właściwy sposób zwracania wskaźników, a jeśli tak, to w jaki sposób sprawić, aby R zobaczył wynik jako Doublenie externalptr? Alternatywnie, jeśli zwrócenie surowego wskaźnika nie powoduje problemów z pamięcią, kto czyści obiekt utworzony przez funkcję?

Artem Sokolov
źródło
Tak, prawdopodobnie chcesz Rcpp::XPtrutworzyć zewnętrzny wskaźnik z kodu C ++. I chcesz to rzucić, double *czy cokolwiek innego. Tu powinny być przykłady, w Galerii, na GitHub ... Może dzięki zmotywowanemu wyszukiwaniu możesz znaleźć coś wystarczająco blisko?
Dirk Eddelbuettel,
Cześć @DirkEddelbuettel Obsada naprawdę musi być CustomClass*. Prawdziwa aplikacja to niestandardowa struktura danych bez odpowiednika R, a wszystkie interakcje odbywają się za pośrednictwem funkcji udostępnionej przez RCPP_MODULE. Najbliższym dopasowaniem, jakie znalazłem przy wyszukiwaniu motywowanym, był post sprzed 7 lat , w którym wydaje się, że muszę zdefiniować template <> CustomClass* as()konwerter. Nie jestem jednak pewien, w jaki sposób powinien on wchodzić w interakcje RCPP_MODULEi RCPP_EXPOSED_CLASS, zwłaszcza że myślałem, że to drugie zostało już zdefiniowane wrap()i as().
Artem Sokolov,
Post Romaina z tego samego wątku jest również bardzo pomocny, ale niestety podkreśla bezpośrednio użycie obiektów, a nie obsługę wskaźników.
Artem Sokolov,
1
Wiem, że zrobiłem podobne rzeczy, ale teraz nie jestem pewien, jaki to najlepszy przykład. Możesz wyraźnie ustawić obiekt „singleton” i zawinąć go jako moduł (RcppRedis); Myślę, że zrobiłem to, co opisałeś w poprzedniej pracy lub dwóch, ale nie mogę teraz wymyślić dobrego publicznego przykładu. Z drugiej strony - różne owijarki baz danych i pakiet dostępu to robią. Nie najmniejszy temat, więc może zacznij od implementacji zabawki / makiety i stamtąd buduj?
Dirk Eddelbuettel,
Używanie RCPP_EXPOSED_CLASSi czy RCPP_MODULEto naprawdę sposób, aby to zrobić? Nigdy wcześniej tego nie używałem ani nie widziałem.
F. Privé,

Odpowiedzi:

7

Myślę, że sensownie jest osobno spojrzeć na różne podejścia. To czyni rozróżnienie wyraźniejszym. Zauważ, że jest to dość podobne do dyskusji w winiecie Moduły Rcpp.

Podczas korzystania Rcpp::XPtrmasz klasę i udostępniasz wyeksportowane funkcje C ++ dla każdej metody, którą chcesz udostępnić:

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

Wynik:

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

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

> squareDouble(d2)
[1] 29.16

Zauważ, że w R obiekt jest tylko „wskaźnikiem”. Możesz dodać klasę S4 / RC / R6 / ... po stronie R, jeśli chcesz czegoś ładniejszego.

Zawijanie zewnętrznego wskaźnika w klasę po stronie R to coś, co otrzymujesz za darmo, używając modułów 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()
*/

Wynik:

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

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

> d1$square()
[1] 29.16

Obsługiwane jest również użycie metody fabrycznej zamiast konstruktora w C ++, ale z identycznym użyciem po stronie 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()
*/

Wynik:

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

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

> d1$square()
[1] 29.16

Wreszcie RCPP_EXPOSED_CLASSprzydaje się, jeśli chcesz połączyć funkcję fabryczną po stronie R z modułami Rcpp, ponieważ tworzy to Rcpp::asi Rcpp::wraprozszerzenia potrzebne do przekazywania obiektów z powrotem między R i C ++. Fabrykę można wyeksportować functiontak jak ty lub używając atrybutów Rcpp, co uważam za bardziej naturalne:

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

Wynik:

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

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

> d1$square()
[1] 29.16

Odnośnie czyszczenia: zarówno Rcpp::XPtrModuły, jak i Rcpp rejestrują domyślny finalizator, który wywołuje destruktor obiektu. W razie potrzeby możesz również dodać niestandardowy finalizator.

Trudno mi zalecić jedno z tych podejść. Może najlepiej wypróbować każdy z nich na prostym przykładzie i przekonać się, co jest bardziej naturalne w użyciu.

Ralf Stubner
źródło
2
Bardzo fajne rzeczy. Jesteś na fali.
Dirk Eddelbuettel,
Dziękuję Ci. To jest bardzo pomocne! Myślę, że factoryto kluczowy element złącza, którego mi brakowało.
Artem Sokolov,
Jako małe uzupełnienie, czy wiesz, czy function rejestruje także finalizator, czy tylko to factory ?
Artem Sokolov,
1
@ArtemSokolov AFAIK domyślny finalizator, który wywołuje destruktor, jest generowany przez class_<T> i jest niezależny od sposobu tworzenia obiektu.
Ralf Stubner,