digitalmars.D - safe ref counted pointer
- vit (188/188) Jan 12 2022 Hello,
- Stanislav Blinov (23/46) Jan 12 2022 3) Leave `get()` `@system` and simply make a `@safe` caller:
Hello, I want implement safe ref counted pointer (similar to std::shared_ptr). Problem is that access to managed data of ref counted pointer is inherently unsafe because ref counted pointer can be released (managed data is destroyed) and scope reference/pointer can be still on the stack: Example: ```d import std.typecons; scope rc = RefCounted!int(5); (scope ref int data){ rc = RefCounted!int(42); data = -1; ///dangling reference! }(rc); ``` It look like i must choose from 2 options: 1) Method which can release managed data or move ownership of managed data are safe but methods accessing managed data must be system. 2) Method which can release managed data or move ownership of managed data are system but methods accessing managed data can be safe Are this 2 options valid? (in -dip1000) I implement ref counted pointer which use both options. SharedPtr with otpion 1. and ScopedSharedPtr with option 2. SharedPtr is convertable to ScopedSharedPtr by copy or move. ScopedSharedPtr is convertable to SharedPtr by copy. (Code ignore qualifiers, work only with integers, is thread local only and has not custom allocators...) Is use of trusted in this code valid? (git link: https://gist.github.com/run-dlang/1d18c71d0ad89409684969e7c155137e) ```d import core.lifetime : forward, move, emplace; import std.experimental.allocator.mallocator; import std.traits : isIntegral, isMutable; import std.stdio : writeln; void main(){ scope SharedPtr!long top = SharedPtr!long.make(-1); writeln("shared ptr:"); //shared ptr: () system{ scope SharedPtr!long x = SharedPtr!long.make(42); (scope ref long data) safe{ x.release(); ///release is safe x = SharedPtr!long.make(123); ///opAssign is safe top = move(x); ///move is safe //data = 314; ///dangling pointer }(x.get); ///get is system }(); writeln("scoped shared ptr:"); ///scoped shared ptr: () safe{ scope ScopedSharedPtr!long x = SharedPtr!long.make(654); (scope ref long data) safe{ //x.release(); ///release is system //x = SharedPtr!long.make(123); ///opAssign is system //top = move(x); ///opPostMove is system top = x; //copy is ok data = -data; //cannot be dangling pointer/reference }(x.get); //get is safe }(); } alias ScopedSharedPtr(T) = SharedPtr!(T, true); struct SharedPtr(T, bool scoped = false) if(isIntegral!T && isMutable!T){ //copy ctor: this(scope ref typeof(this) rhs) trusted{ if(rhs.impl){ this.impl = rhs.impl; this.impl.counter += 1; } } //forward ctor impl this(bool s)(scope auto ref SharedPtr!(T, s) rhs, typeof(null)) trusted{ if(rhs.impl){ this.impl = rhs.impl; static if(__traits(isRef, rhs)) this.impl.counter += 1; else rhs.impl = null; } } //forward ctor (constraint ignore move ctor) this(bool s)(scope auto ref SharedPtr!(T, s) rhs) trusted if(__traits(isRef, rhs) || s != scoped){ if(rhs.impl){ this.impl = rhs.impl; static if(__traits(isRef, rhs)) this.impl.counter += 1; else rhs.impl = null; } } //forward assignment void opAssign(bool s)(scope auto ref SharedPtr!(T, s) rhs)scope{ if((() trusted => cast(void*)&this is cast(void*)&rhs )()) return; this.release(); if(rhs.impl){ () trusted{ this.impl = rhs.impl; }(); static if(__traits(isRef, rhs)) this.impl.counter += 1; else rhs.impl = null; } } static auto make(Args...)(auto ref Args args) safe{ return typeof(this)(Impl.construct(forward!args)); } ///ScopedSharedPtr: static if(scoped){ // system move: void opPostMove(const ref typeof(this)) system{ } // system release void release()scope system{ this.release_impl(); } // safe get: property ref inout(T) get()inout return safe pure nothrow nogc{ assert(impl !is null); return impl.elm; } } ///SharedPtr: else{ // safe release void release()scope safe{ this.release_impl(); } // system get: property ref inout(T) get()inout return system pure nothrow nogc{ assert(impl !is null); return impl.elm; } } ~this() safe{ this.release_impl(); } private void release_impl()scope safe{ if(impl){ impl.counter -= 1; if(impl.counter == 0){ impl.destruct(); impl = null; } } } private alias Impl = ControlBlock!T; private Impl* impl; private this(Impl* impl) safe{ this.impl = impl; } } private struct ControlBlock(T){ T elm; int counter; static ControlBlock* construct(Args...)(auto ref Args args) safe{ writeln("alloc: ", args); void[] data = Mallocator.instance.allocate(ControlBlock.sizeof); ControlBlock* impl = (() trusted => cast(ControlBlock*)data.ptr )(); if(impl){ emplace(&impl.elm, forward!args); impl.counter = 1; } return impl; } void destruct() trusted{ writeln("delloc: ", elm); destroy(elm); const result = Mallocator.instance.deallocate((cast(void*)&this)[0 .. ControlBlock.sizeof]); assert(result); } } ```
Jan 12 2022
On Wednesday, 12 January 2022 at 21:17:13 UTC, vit wrote:Hello, I want implement safe ref counted pointer (similar to std::shared_ptr). Problem is that access to managed data of ref counted pointer is inherently unsafe because ref counted pointer can be released (managed data is destroyed) and scope reference/pointer can be still on the stack: Example: ```d import std.typecons; scope rc = RefCounted!int(5); (scope ref int data){ rc = RefCounted!int(42); data = -1; ///dangling reference! }(rc); ``` It look like i must choose from 2 options: 1) Method which can release managed data or move ownership of managed data are safe but methods accessing managed data must be system. 2) Method which can release managed data or move ownership of managed data are system but methods accessing managed data can be safe3) Leave `get()` ` system` and simply make a ` safe` caller: ```d struct SharedPtr(T) { // ... auto apply(Dg)(scope Dg dg) if (is(typeof(dg(typeof(this).init.get)))) { auto tmp = this; // borrow another reference return dg(ref () trusted { return get(); } ()); } // ... } void main() safe { scope rc = SharedPtr!int(5); rc.apply((scope ref data) { rc = SharedPtr!int(42); data = -1; // ok }); } ```
Jan 12 2022