Skip to content

[Rcpp-devel] Life-cycle of Rcpp::XPtr

7 messages · Simon Urbanek, Dirk Eddelbuettel, Ralf Stubner +1 more

#
Hi everybody,

I have a question concerning the file-cycle of Rcpp::XPtr: Consider a
XPtr with the default delete finalizer wrapping some pointer. If I use
the copy constructor to create another XPtr, this is pointing at the
same underlying object as expected. What happens if one of these
pointers goes out of scope and is at some point garbage collected? Is
the underlying object deleted leaving the other XPtr with a broken
pointer? Or is the object protected by the existence of the other
pointer? From my experiments I have the impression that the latter is
the case, which would be the desired behaviour. But it would be nice
if one could be sure.

Thanks
Ralf
#
The copy constructor doesn't actually create a copy, it only acts as a wrapper that preserves the same EXTPTR object, it is akin to increasing the reference count, so the C++ class wrapped in the EXTPTR is only released when the EXTPTR can be garbage-collected, i.e. all references are gone (including all "copies").
[Of course that is not true if you were to create two XPtrs with the same pointer and a finalizer each, but that would be a bad idea, obviously].

Cheers,
Simon
#
Hi Ralf,
On 23 September 2023 at 08:28, Ralf Stubner wrote:
| I have a question concerning the file-cycle of Rcpp::XPtr: Consider a
| XPtr with the default delete finalizer wrapping some pointer. If I use
| the copy constructor to create another XPtr, this is pointing at the
| same underlying object as expected. What happens if one of these
| pointers goes out of scope and is at some point garbage collected? Is
| the underlying object deleted leaving the other XPtr with a broken
| pointer? Or is the object protected by the existence of the other
| pointer? From my experiments I have the impression that the latter is
| the case, which would be the desired behaviour. But it would be nice
| if one could be sure.

I had done some experiments with 'expanded finalizers' that use more logging
(which I find quite convenient via `spdl` -- same nice spdlog interface now
from R and C++) so you could add some display of the pointer address, check
on nullness etc.

I have done a bit more work using XPtr in the context of the tiledb (and the
related, possibly upcoming tiledbsoma package not yet on CRAN) where I use an
enum (which requires C++17 in the use I have there) to 'type tag' each
external pointer and on each use check I have the correct type. I am not
thinking to a simple unordered_map from enum to string to also make more
informed errors (showing the type as string rather than enum int value) as
well as maybe an enhanced show or print at the R level. I have long been
meaning to farm that out into a new (simple) add-on package (see issue #1212)
and should get on with that.  (The 'still somewhat raw' XPtr extension in
tiledb is eg here:
https://github.com/TileDB-Inc/TileDB-R/blob/1e7bd2fa4e54f3e152c4fec3a65343df49d3f525/src/libtiledb.h#L80-L160
and the check_xptr_tag is then used throughout the files in src/.

Dirk
#
PS There is another neat use case where a shared_ptr is allocated.  Now we
cannot wrap a shared_ptr in an XPtr but ... we can stick the shared_ptr into
a struct, and allocate that with new and then make_xptr.  You are then back
to well-understood C++ semantics.

Dirk
#
On Sat, Sep 23, 2023 at 9:49?AM Simon Urbanek
<simon.urbanek at r-project.org> wrote:
Thanks Simon! This seems to be the behaviour I am looking for.

Ralf
#
Hi Dirk,
Thanks! That does sound interesting. I think for my current use-case
the existing functionality is sufficient. But i will have a look at
these ideas.

Ralf
1 day later
#
Hi Ralf,
On Sat, 23 Sept 2023 at 14:56, Dirk Eddelbuettel <edd at debian.org> wrote:
The answer is yes, XPtr works as a shared_ptr, so you can be sure that
the object will be protected until the last reference to it is
deleted. A quick check:

#include <Rcpp.h>
using namespace Rcpp;

class Test {
public:
  Test()  { Rcout << this << " created" << std::endl; }
  ~Test() { Rcout << this << " deleted" << std::endl; }
};

// [[Rcpp::export]]
SEXP test_new() {
  return XPtr<Test>(new Test());
}

// [[Rcpp::export]]
SEXP test_copy(SEXP x_) {
  XPtr<Test> x(x_);
  return x;
}

/*** R
x <- test_new()
#> 0x55a93e3845d0 created
y <- test_copy(x)
rm(x); invisible(gc())
rm(y); invisible(gc())
#> 0x55a93e3845d0 deleted
*/

Best,
I?aki