I have a C++ codebase that I'm exposing to R using Rcpp modules. Specifically, I use an interface pattern where the class(es) I expose is actually an abstraction layer on top of the underlying object, which is the implementation.
The class(es) I'm dealing with also interact with each other, and have methods that take as arguments shared pointers to objects. I'm having trouble figuring out the right way to expose these methods to R.
Eg here is some code. The TestClass::combine
method takes a pointer to another TestClass
object and does stuff with it. When I try to compile this code, I get compiler errors (see below) when I add the corresponding interface method ITestClass::combine
to the module.
Implementation:
class TestClass
{
public:
TestClass(int const& n, double const& x)
: n(n), x(x)
{}
const double get_x() {
return x;
}
double combine(std::shared_ptr<TestClass> obj) {
return x + obj->get_x();
}
protected:
int n;
double x;
};
Interface:
//' @export ITestClass
class ITestClass
{
public:
ITestClass(int const& in_n, double const& in_x)
: impl(in_n, in_x)
{}
double get_x() {
return impl.get_x();
}
double combine(ITestClass obj) {
return impl.combine(obj.get_object_ptr());
}
std::shared_ptr<TestClass> get_object_ptr() {
std::shared_ptr<TestClass> ptr(&impl);
return ptr;
}
private:
TestClass impl;
};
RCPP_MODULE(RTestClassModule)
{
class_<ITestClass>("ITestClass")
.constructor<int, double>()
.method("get_x", &ITestClass::get_x, "get_x")
.method("combine", &ITestClass::combine, "combine"); // this line errors out
}
A sample of the errors I get:
In file included from C:/Rlib/Rcpp/include/Rcpp/as.h:25,
from C:/Rlib/Rcpp/include/RcppCommon.h:168,
from C:/Rlib/Rcpp/include/Rcpp.h:27,
from interface1.cpp:2:
C:/Rlib/Rcpp/include/Rcpp/internal/Exporter.h: In instantiation of 'Rcpp::traits::Exporter<T>::Exporter(SEXP) [with T = testpkg::ITestClass; SEXP = SEXPREC*]':
C:/Rlib/Rcpp/include/Rcpp/as.h:87:41: required from 'T Rcpp::internal::as(SEXP, Rcpp::traits::r_type_generic_tag) [with T = testpkg::ITestClass; SEXP = SEXPREC*]'
C:/Rlib/Rcpp/include/Rcpp/as.h:152:31: required from 'T Rcpp::as(SEXP) [with T = testpkg::ITestClass; SEXP = SEXPREC*]'
C:/Rlib/Rcpp/include/Rcpp/InputParameter.h:34:43: required from 'Rcpp::InputParameter<T>::operator T() [with T = testpkg::ITestClass]'
C:/Rlib/Rcpp/include/Rcpp/module/Module_generated_CppMethod.h:111:69: required from 'SEXPREC* Rcpp::CppMethod1<Class, RESULT_TYPE, U0>::operator()(Class*, SEXPREC**) [with Class = testpkg::ITestClass; RESULT_TYPE = double; U0 = testpkg::ITestClass; SEXP = SEXPREC*]'
C:/Rlib/Rcpp/include/Rcpp/module/Module_generated_CppMethod.h:109:10: required from here
C:/Rlib/Rcpp/include/Rcpp/internal/Exporter.h:31:31: error: no matching function for
call to 'testpkg::ITestClass::ITestClass(SEXPREC*&)'
Exporter( SEXP x ) : t(x){}
^
interface1.cpp:17:5: note: candidate: 'testpkg::ITestClass::ITestClass(SEXP, const int&, const double&)'
ITestClass(SEXP in_date, int const& in_n, double const& in_x)
^~~~~~~~~~
interface1.cpp:17:5: note: candidate expects 3 arguments, 1 provided
interface1.cpp:14:7: note: candidate: 'constexpr testpkg::ITestClass::ITestClass(const testpkg::ITestClass&)'
class ITestClass
^~~~~~~~~~
interface1.cpp:14:7: note: no known conversion for argument 1 from 'SEXP' {aka 'SEXPREC*'} to 'const testpkg::ITestClass&'
interface1.cpp:14:7: note: candidate: 'constexpr testpkg::ITestClass::ITestClass(testpkg::ITestClass&&)'
interface1.cpp:14:7: note: no known conversion for argument 1 from 'SEXP' {aka 'SEXPREC*'} to 'testpkg::ITestClass&&'
How do I define ITestClass::combine
so that it can be called from R?
I found a better solution, one that has the preferred interface for combine
and doesn't seem to run into problems with garbage collection.
A couple of points:
TestClass
object in impl
, I store a std::shared_ptr<TestClass>
. This is referenced directly, rather than creating new shared ptrs from scratch (which crash R when they get destroyed)..pointer
member that is a pointer to the underlying C++ object. So I can dereference that to get the impl
member.New interface:
//' @export ITestClass2
class ITestClass2
{
public:
ITestClass2(int const& in_n, double const& in_x)
: impl(in_n, in_x))
{}
double get_x()
{
return impl->get_x();
}
double combine(Environment obj)
{
SEXP objptr = obj[".pointer"];
ITestClass2* ptr = (ITestClass2*) R_ExternalPtrAddr(objptr);
return impl->combine(ptr->get_object_ptr());
}
// this doesn't need to be seen from R
protected:
std::shared_ptr<TestClass> get_object_ptr()
{
return impl;
}
private:
std::shared_ptr<TestClass> impl;
};
RCPP_MODULE(RTestClassModule2)
{
class_<ITestClass2>("ITestClass2")
.constructor<int, double>()
.method("get_x", &ITestClass2::get_x, "get_x")
.method("combine", &ITestClass2::combine, "combine")
;
}
Call this in R as follows:
obj <- new(ITestClass2, 1, pi)
obj2 <- new(ITestClass2, 2, exp(1))
obj$combine(obj2)