diff --git a/ChangeLog b/ChangeLog index 402732946..9a6d7dcbb 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,12 @@ +2026-10-29 Kevin Ushey + + * vignettes/rmd/Rcpp-modules.Rmd: Document that objects of exposed + classes returned by value or reference are copied, while a returned + pointer is owned and eventually deleted by R, so that returning this or + another pointer the C++ side still owns deletes the object twice (#1533) + * inst/tinytest/cpp/Module.cpp: Add a class counting its destructions + * inst/tinytest/test_module.R: Test that R deletes a returned pointer + 2026-10-23 Dirk Eddelbuettel * DESCRIPTION (Version, Date): Roll micro version and date diff --git a/inst/tinytest/cpp/Module.cpp b/inst/tinytest/cpp/Module.cpp index 7f3132820..00011d9ac 100644 --- a/inst/tinytest/cpp/Module.cpp +++ b/inst/tinytest/cpp/Module.cpp @@ -137,6 +137,26 @@ double Test_get_x_pointer(ModuleTest* x) { return x->value; } +// counts its destructions, to check who owns objects returned from C++ +class ModuleOwned { +public: + ModuleOwned(int id_) : id(id_) {} + ~ModuleOwned() { destroyed++; } + int id; + static int destroyed; +}; +int ModuleOwned::destroyed = 0; + +RCPP_EXPOSED_CLASS_NODECL(ModuleOwned) + +ModuleOwned* ModuleOwned_create(int id) { + return new ModuleOwned(id); +} + +int ModuleOwned_destroyed() { + return ModuleOwned::destroyed; +} + class ModuleGadget { public: ModuleGadget() {} @@ -217,6 +237,13 @@ RCPP_MODULE(demoModule) { .method("get" , &ModuleRandomizer::get) ; + class_("ModuleOwned") + .field_readonly("id", &ModuleOwned::id) + ; + + function("ModuleOwned_create", &ModuleOwned_create); + function("ModuleOwned_destroyed", &ModuleOwned_destroyed); + class_("ModuleGadget") .constructor() .method("value", static_cast(&ModuleGadget::value)) diff --git a/inst/tinytest/test_module.R b/inst/tinytest/test_module.R index db0edfc60..6db751110 100644 --- a/inst/tinytest/test_module.R +++ b/inst/tinytest/test_module.R @@ -108,6 +108,15 @@ expect_equal(r$get(10), x10) r <- new( ModuleRandomizer ) expect_error( r$get(10L), "not initialized" ) +## a pointer returned from C++ is owned by the R object wrapping it, and +## deleted when that object is garbage collected (#1533) +destroyed <- ModuleOwned_destroyed() +owned <- ModuleOwned_create( 7L ) +expect_equal( owned$id, 7L ) +rm( owned ) +invisible( gc() ) +expect_equal( ModuleOwned_destroyed(), destroyed + 1L ) + # test.Module.flexible.semantics <- function( ){ expect_equal( test_reference( seq(0,10) ), 11L ) expect_equal( test_const_reference( seq(0,10) ), 11L ) diff --git a/vignettes/rmd/Rcpp-modules.Rmd b/vignettes/rmd/Rcpp-modules.Rmd index 364e081a3..07bd21a31 100644 --- a/vignettes/rmd/Rcpp-modules.Rmd +++ b/vignettes/rmd/Rcpp-modules.Rmd @@ -1013,6 +1013,45 @@ bar$handleFoo(foo) #> Got a Foo! ``` +### Returning objects of exposed classes + +With `RCPP_EXPOSED_WRAP` (or `RCPP_EXPOSED_CLASS`), functions and +methods can also return objects of a class exposed by a module. How +the object is passed to \proglang{R} depends on the return type: + +- An object returned by value or by reference is _copied_, using the + copy constructor of the class, and the \proglang{R} object owns the + copy. +- A pointer is not copied: the \proglang{R} object takes _ownership_ + of the object it points to, and deletes it when the \proglang{R} + object is garbage collected. This is what a function returning a + newly allocated object needs, like the factories shown above, but + it means that a pointer may only be returned when nothing else owns + or deletes the object it points to. Returning `this` (e.g. to chain + method calls), a pointer to a data member or to an element of a + container, or the same pointer from more than one call results in the + object being deleted more than once, which typically crashes + \proglang{R} during garbage collection. + +```{Rcpp ownership, eval=FALSE} +class Counter { +public: + Counter() : n(0) {} + + // fine: R owns, and eventually deletes, the new Counter + static Counter* create() { return new Counter; } + + // wrong: the R object returned by inc() would delete *this + // a second time + Counter* inc() { n++; return this; } + + // fine: return void, or a copy, and chain the calls in R + void increment() { n++; } + + int n; +}; +``` + ### Full example