From 2798a3ed6841c98904c22453fcc5a94216e2f3e9 Mon Sep 17 00:00:00 2001 From: Kevin Ushey Date: Thu, 29 Oct 2026 11:24:49 -0700 Subject: [PATCH] document ownership of objects returned to r from modules --- ChangeLog | 7 ++++++ inst/tinytest/cpp/Module.cpp | 27 +++++++++++++++++++++++ inst/tinytest/test_module.R | 9 ++++++++ vignettes/rmd/Rcpp-modules.Rmd | 39 ++++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+) diff --git a/ChangeLog b/ChangeLog index 02f61fba4..44e7985f1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -52,6 +52,13 @@ * inst/tinytest/cpp/Module.cpp (Module_scope_is_cleared): New * inst/tinytest/test_module.R: Test loading the failing module + * 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 eb9573c37..603171c74 100644 --- a/inst/tinytest/cpp/Module.cpp +++ b/inst/tinytest/cpp/Module.cpp @@ -173,6 +173,26 @@ double ModuleBase_value(const ModuleBase& 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() {} @@ -283,6 +303,13 @@ RCPP_MODULE(demoModule) { function("ModuleBase_value", &ModuleBase_value); + 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 4186cf1e1..8508ce51e 100644 --- a/inst/tinytest/test_module.R +++ b/inst/tinytest/test_module.R @@ -167,6 +167,15 @@ expect_error( r$get(10L), "not initialized" ) expect_error( Rcpp::sourceCpp("cpp/ModuleInitError.cpp"), "no such class" ) expect_true( Module_scope_is_cleared() ) +## 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