Skip to content

Commit 92fd416

Browse files
committed
document ownership of objects returned to r from modules
1 parent a6f354e commit 92fd416

4 files changed

Lines changed: 84 additions & 0 deletions

File tree

‎ChangeLog‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
2026-10-29 Kevin Ushey <kevinushey@gmail.com>
2+
3+
* vignettes/rmd/Rcpp-modules.Rmd: Document that objects of exposed
4+
classes returned by value or reference are copied, while a returned
5+
pointer is owned and eventually deleted by R, so that returning this or
6+
another pointer the C++ side still owns deletes the object twice (#1533)
7+
* inst/tinytest/cpp/Module.cpp: Add a class counting its destructions
8+
* inst/tinytest/test_module.R: Test that R deletes a returned pointer
9+
110
2026-10-23 Dirk Eddelbuettel <edd@debian.org>
211

312
* DESCRIPTION (Version, Date): Roll micro version and date

‎inst/tinytest/cpp/Module.cpp‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,26 @@ double Test_get_x_pointer(ModuleTest* x) {
137137
return x->value;
138138
}
139139

140+
// counts its destructions, to check who owns objects returned from C++
141+
class ModuleOwned {
142+
public:
143+
ModuleOwned(int id_) : id(id_) {}
144+
~ModuleOwned() { destroyed++; }
145+
int id;
146+
static int destroyed;
147+
};
148+
int ModuleOwned::destroyed = 0;
149+
150+
RCPP_EXPOSED_CLASS_NODECL(ModuleOwned)
151+
152+
ModuleOwned* ModuleOwned_create(int id) {
153+
return new ModuleOwned(id);
154+
}
155+
156+
int ModuleOwned_destroyed() {
157+
return ModuleOwned::destroyed;
158+
}
159+
140160
class ModuleGadget {
141161
public:
142162
ModuleGadget() {}
@@ -217,6 +237,13 @@ RCPP_MODULE(demoModule) {
217237
.method("get" , &ModuleRandomizer::get)
218238
;
219239

240+
class_<ModuleOwned>("ModuleOwned")
241+
.field_readonly("id", &ModuleOwned::id)
242+
;
243+
244+
function("ModuleOwned_create", &ModuleOwned_create);
245+
function("ModuleOwned_destroyed", &ModuleOwned_destroyed);
246+
220247
class_<ModuleGadget>("ModuleGadget")
221248
.constructor()
222249
.method("value", static_cast<void (ModuleGadget::*)(int)>(&ModuleGadget::value))

‎inst/tinytest/test_module.R‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ expect_equal(r$get(10), x10)
108108
r <- new( ModuleRandomizer )
109109
expect_error( r$get(10L), "not initialized" )
110110

111+
## a pointer returned from C++ is owned by the R object wrapping it, and
112+
## deleted when that object is garbage collected (#1533)
113+
destroyed <- ModuleOwned_destroyed()
114+
owned <- ModuleOwned_create( 7L )
115+
expect_equal( owned$id, 7L )
116+
rm( owned )
117+
invisible( gc() )
118+
expect_equal( ModuleOwned_destroyed(), destroyed + 1L )
119+
111120
# test.Module.flexible.semantics <- function( ){
112121
expect_equal( test_reference( seq(0,10) ), 11L )
113122
expect_equal( test_const_reference( seq(0,10) ), 11L )

‎vignettes/rmd/Rcpp-modules.Rmd‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,45 @@ bar$handleFoo(foo)
10131013
#> Got a Foo!
10141014
```
10151015

1016+
### Returning objects of exposed classes
1017+
1018+
With `RCPP_EXPOSED_WRAP` (or `RCPP_EXPOSED_CLASS`), functions and
1019+
methods can also return objects of a class exposed by a module. How
1020+
the object is passed to \proglang{R} depends on the return type:
1021+
1022+
- An object returned by value or by reference is _copied_, using the
1023+
copy constructor of the class, and the \proglang{R} object owns the
1024+
copy.
1025+
- A pointer is not copied: the \proglang{R} object takes _ownership_
1026+
of the object it points to, and deletes it when the \proglang{R}
1027+
object is garbage collected. This is what a function returning a
1028+
newly allocated object needs, like the factories shown above, but
1029+
it means that a pointer may only be returned when nothing else owns
1030+
or deletes the object it points to. Returning `this` (e.g. to chain
1031+
method calls), a pointer to a data member or to an element of a
1032+
container, or the same pointer from more than one call results in the
1033+
object being deleted more than once, which typically crashes
1034+
\proglang{R} during garbage collection.
1035+
1036+
```{Rcpp ownership, eval=FALSE}
1037+
class Counter {
1038+
public:
1039+
Counter() : n(0) {}
1040+
1041+
// fine: R owns, and eventually deletes, the new Counter
1042+
static Counter* create() { return new Counter; }
1043+
1044+
// wrong: the R object returned by inc() would delete *this
1045+
// a second time
1046+
Counter* inc() { n++; return this; }
1047+
1048+
// fine: return void, or a copy, and chain the calls in R
1049+
void increment() { n++; }
1050+
1051+
int n;
1052+
};
1053+
```
1054+
10161055
### Full example
10171056

10181057
<!-- TODO: maybe replace this by something from wls or RcppModels ? -->

0 commit comments

Comments
 (0)