Deep copying smart pointer

3,184 views
Skip to first unread message

Jonathan Coe

unread,
Jan 7, 2016, 2:51:35 AM1/7/16
to std-pr...@isocpp.org
A deep copying smart pointer that uses type erasure to call the copy constructor of derived types would allow the compiler-generated copy constructor to copy an object composed of polymorphic components correctly.

When component objects are owned by deep-copying pointers, they will be copied correctly without any extra code being required.

I'm currently working with pre c++11 code with lots of composite objects with user-defined destructors and copy constructors. Using unique pointers will get rid of the need for user-defined destructors. Using deep copying pointers will get rid of the need for user-defined copy constructors too.

Would this be of interest to others?

Regards?

Jonathan

Ville Voutilainen

unread,
Jan 7, 2016, 9:06:27 AM1/7/16
to ISO C++ Standard - Future Proposals
Yes, it would. There's an existing proposal at
http://open-std.org/JTC1/SC22/WG21/docs/papers/2012/n3339.pdf
but that one doesn't do type erasure. I have done some implementation work
on a type-erasing approach, but I didn't polish it far enough to make
it a proposal.

Jonathan Coe

unread,
Jan 7, 2016, 12:15:02 PM1/7/16
to std-pr...@isocpp.org
> --
>
> ---
> You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
> To post to this group, send email to std-pr...@isocpp.org.
> Visit this group at https://groups.google.com/a/isocpp.org/group/std-proposals/.

Bengt Gustafsson

unread,
Jan 7, 2016, 5:35:24 PM1/7/16
to ISO C++ Standard - Future Proposals
How can this be implemented? Can you explain this briefly? What would the limitations be?

Jonathan Coe

unread,
Jan 7, 2016, 5:49:19 PM1/7/16
to std-pr...@isocpp.org
I have a preliminary implementation. I use a buffer to store a type-erased implementation object templated on the derived type. Raw pointers can be passed in to the constructor and stored without the need for copying of the pointee.

Limits I can think of are inability to define functions as constexpr (due to the use of a buffer) and the need for derived types to be copy constructible themselves.

The first limit may be a QoI issue. The second seems pretty fundamental.

Regards,

Jonathan

On 7 January 2016 at 22:35, Bengt Gustafsson <bengt.gu...@beamways.com> wrote:
How can this be implemented? Can you explain this briefly? What would the limitations be?

Nevin Liber

unread,
Jan 7, 2016, 6:00:10 PM1/7/16
to std-pr...@isocpp.org
On 7 January 2016 at 16:49, Jonathan Coe <jb...@me.com> wrote:
I have a preliminary implementation. I use a buffer to store a type-erased implementation object templated on the derived type.

Do you fall back on the heap if the object is too large for the buffer?  Of course, that would imply allocator support in a proposal.  Given that derived types may be arbitrarily large, I don't see any way around that.
 
Raw pointers can be passed in to the constructor and stored without the need for copying of the pointee.

Does that transfer ownership, and if so, how does the clone_ptr know how to "clean up" what is pointed to?

And if it doesn't transfer ownership, this sounds like a very complicated ownership model.

I'm not a fan of "sometimes owned" semantics, as I've had to fix far too many bugs based on it in other people's code.
--
 Nevin ":-)" Liber  <mailto:ne...@eviloverlord.com>  +1-847-691-1404

Jonathan Coe

unread,
Jan 7, 2016, 6:12:03 PM1/7/16
to std-pr...@isocpp.org
On 7 January 2016 at 22:59, Nevin Liber <ne...@eviloverlord.com> wrote:
On 7 January 2016 at 16:49, Jonathan Coe <jb...@me.com> wrote:
I have a preliminary implementation. I use a buffer to store a type-erased implementation object templated on the derived type.

Do you fall back on the heap if the object is too large for the buffer?  Of course, that would imply allocator support in a proposal.  Given that derived types may be arbitrarily large, I don't see any way around that.

The buffer stores a type-erasure implementation object that contains the pointer passed into the constructor (not the pointee) so the size is known for any derived type (pointer + vtable pointer).

 
Raw pointers can be passed in to the constructor and stored without the need for copying of the pointee.

Does that transfer ownership, and if so, how does the clone_ptr know how to "clean up" what is pointed to?

 
The pointer passed in is owned (like shared_ptr and unique_ptr).
The deep_ptr unambiguously owns the pointer it is passed so will clean it up (like unique_ptr or boost::scoped_ptr).
Type erasure is used to call the derived class destructor (like shared_ptr).

Jon 
 
And if it doesn't transfer ownership, this sounds like a very complicated ownership model.

I'm not a fan of "sometimes owned" semantics, as I've had to fix far too many bugs based on it in other people's code.
--
 Nevin ":-)" Liber  <mailto:ne...@eviloverlord.com>  +1-847-691-1404

--

Nevin Liber

unread,
Jan 7, 2016, 6:39:44 PM1/7/16
to std-pr...@isocpp.org
On 7 January 2016 at 17:12, Jonathan Coe <jb...@me.com> wrote:
On 7 January 2016 at 22:59, Nevin Liber <ne...@eviloverlord.com> wrote:
On 7 January 2016 at 16:49, Jonathan Coe <jb...@me.com> wrote:
I have a preliminary implementation. I use a buffer to store a type-erased implementation object templated on the derived type.

Do you fall back on the heap if the object is too large for the buffer?  Of course, that would imply allocator support in a proposal.  Given that derived types may be arbitrarily large, I don't see any way around that.

The buffer stores a type-erasure implementation object that contains the pointer passed into the constructor (not the pointee) so the size is known for any derived type (pointer + vtable pointer).

Ah, I see. 

 
Raw pointers can be passed in to the constructor and stored without the need for copying of the pointee.

Does that transfer ownership, and if so, how does the clone_ptr know how to "clean up" what is pointed to?

 
The pointer passed in is owned (like shared_ptr and unique_ptr).
The deep_ptr unambiguously owns the pointer it is passed so will clean it up (like unique_ptr or boost::scoped_ptr).
Type erasure is used to call the derived class destructor (like shared_ptr).

That isn't the problem.

Both unique_ptr and shared_ptr also have a deleter that knows how to clean up.  However, neither has to do allocation, while clone_ptr does.  Would you be proposing that the allocator be part of the type, require polymorphic allocators (from Lib Fundamentals 2) or do you have some other scheme in mind?

Ville Voutilainen

unread,
Jan 7, 2016, 6:43:48 PM1/7/16
to ISO C++ Standard - Future Proposals
On 8 January 2016 at 01:39, Nevin Liber <ne...@eviloverlord.com> wrote:
>> The pointer passed in is owned (like shared_ptr and unique_ptr).
>> The deep_ptr unambiguously owns the pointer it is passed so will clean it
>> up (like unique_ptr or boost::scoped_ptr).
>> Type erasure is used to call the derived class destructor (like
>> shared_ptr).
> That isn't the problem.
> Both unique_ptr and shared_ptr also have a deleter that knows how to clean
> up. However, neither has to do allocation, while clone_ptr does. Would you
> be proposing that the allocator be part of the type, require polymorphic
> allocators (from Lib Fundamentals 2) or do you have some other scheme in
> mind?

I have outlined a scheme where the allocation happens in a cloner, which is
also a deleter. That way people who want allocator support can do it in a custom
cloner. I have a default cloner that just allocates on free store, but
the implementation
allows bypassing that functionality. The implementation is very much
incomplete, but
it has some bits that Jonathan's implementation doesn't have and vice versa.

The current implementation I have actually tries to support
allocators. I want to drop
that, because propagation traits don't mix with type erasure, so I
plan to remove specific
allocator support and provide the most minimal of allocator support,
which is just supporting
custom cloners.

Ville Voutilainen

unread,
Jan 7, 2016, 6:45:03 PM1/7/16
to ISO C++ Standard - Future Proposals
On 8 January 2016 at 01:43, Ville Voutilainen
<ville.vo...@gmail.com> wrote:
>> Both unique_ptr and shared_ptr also have a deleter that knows how to clean
>> up. However, neither has to do allocation, while clone_ptr does. Would you
>> be proposing that the allocator be part of the type, require polymorphic
>> allocators (from Lib Fundamentals 2) or do you have some other scheme in
>> mind?
>
> I have outlined a scheme where the allocation happens in a cloner, which is
> also a deleter. That way people who want allocator support can do it in a custom
> cloner. I have a default cloner that just allocates on free store, but
> the implementation
> allows bypassing that functionality. The implementation is very much
> incomplete, but
> it has some bits that Jonathan's implementation doesn't have and vice versa.
>
> The current implementation I have actually tries to support
> allocators. I want to drop
> that, because propagation traits don't mix with type erasure, so I
> plan to remove specific
> allocator support and provide the most minimal of allocator support,
> which is just supporting
> custom cloners.

I should probably add that the cloner is also type-erased, quite like
the deleter
of a shared_ptr is.

Andrew Tomazos

unread,
Jan 7, 2016, 7:38:33 PM1/7/16
to std-pr...@isocpp.org
On Thu, Jan 7, 2016 at 8:51 AM, Jonathan Coe <jonath...@gmail.com> wrote:
A deep copying smart pointer that uses type erasure to call the copy constructor of derived types would allow the compiler-generated copy constructor to copy an object composed of polymorphic components correctly.

Do you mean something like:

std::derived<Animal> animal1 = std::make_derived<Animal>(dog); // Dog deduced from dog
std::derived<Animal> animal2 = animal1;  // calls Dog::Dog(const Dog&)

If so, couldn't std::derived<B> be a simple adapter around std::any, with the added invariant that the contained object be derived from B.

Or do I miss something?

Ville Voutilainen

unread,
Jan 7, 2016, 8:14:12 PM1/7/16
to ISO C++ Standard - Future Proposals
That would be a different thing. Sean Parent has envisioned an Any<T>
almost like that,
but having this thing be a smart pointer allows for certain benefits,
if we so choose:
- it can always do a noexcept move, even for types that are not movable
- it doesn't need to clone the incoming value on
construction/assignment, it can just adopt
the pointer
- Any<T> could do it as well, but as opposed to a an actual wrapper on any,
it doesn't necessarily need to require CopyConstructible for T.

Bengt Gustafsson

unread,
Jan 8, 2016, 5:26:51 AM1/8/16
to ISO C++ Standard - Future Proposals
I still don't see a solution to the main cloning problem. I read n3339 but the "strawman" code was too much to take in, it would be better with a very slimmed down version which shows what it can accomplish. I assume that we can never get this to work:

class Base {
};

class Sub : public Base {
};

Base* b = new Sub;
clone_ptr
<Base> p = b;
clone_ptr
<Base> p2 = p;      // No knowledge of the existence of class Sub exists here...


So I assume that you shoot for a lesser target, but exactly what is that target and how do you prevent people from trying to use it as in my example? How many of the interesting use cases remain if my example doesn't work?

My take on this would be to introduce a trait on Base which indicates the name of a virtual method in Base that can be used for cloning. Maybe this is what you indend, but I haven't seen a comprehensible description.

While small object optimization can be interesting in this scenario I think that is a separate discussion. (it would require a CloneAt(void*, Base*) method as well as a ByteCount() method in Base.

The problem with the virtual clone method is that it is so easy to forget to reimplement it in each subclass. Can anyone come up with a language feature which automates this?

Jonathan Coe

unread,
Jan 8, 2016, 6:22:06 AM1/8/16
to std-pr...@isocpp.org

> On 8 Jan 2016, at 10:26, Bengt Gustafsson <bengt.gu...@beamways.com> wrote:
>
> I still don't see a solution to the main cloning problem. I read n3339 but the "strawman" code was too much to take in, it would be better with a very slimmed down version which shows what it can accomplish. I assume that we can never get this to work:
>
> class Base {
> };
>
> class Sub : public Base {
> };
>
> Base* b = new Sub;
> clone_ptr<Base> p = b;
> clone_ptr<Base> p2 = p; // No knowledge of the existence of class Sub exists here...
>

That's an important point. My (updated) design only allows construction of deep_ptr from another deep_ptr or using a make_deep_ptr function template that (like make_unique) takes constructor arguments. This 'lesser target' will meet my requirements.

Bengt Gustafsson

unread,
Jan 8, 2016, 6:40:46 AM1/8/16
to ISO C++ Standard - Future Proposals
That's what I suspected. Only allowing construction with a make_ function seems rather restrictive though. The N3339 seems to require an explicit Cloner template parameter to indicate how to clone the pointees. The trait idea could be combined with your make_ function (which we do want anyway for consistency) so that IF the trait is defined for a base class of the T class of the pointer then a reset() function taking a T* is available on the pointer. The trait is not to have a default implementation:

template<typename T> struct clone_trait;


// Application code to make Base clonable:


template<> struct clone_trait<Base> {
   
static Base* clone(const Base* src) { return src->Clone(); }   // Assuming Base has a Clone method.
};




Or maybe, to avoid the namespace issues better, an overloadable function with a fixed name. I assume your make_ function stores a function pointer to the copy constructor. It should be possible to wrap either this or the trait method into an object that can be stored in the clone_ptr for future use.

Jonathan Coe

unread,
Jan 8, 2016, 6:51:07 AM1/8/16
to std-pr...@isocpp.org
My implementation is linked below. Some details are implementation artefacts and may not be required.

https://github.com/jbcoe/deep_ptr

I have included tests which make use of the (excellent) catch testing framework. This is included as a submodule.

Jon

Nevin ":-)" Liber

unread,
Jan 8, 2016, 9:04:41 AM1/8/16
to std-pr...@isocpp.org
On Jan 8, 2016, at 4:26 AM, Bengt Gustafsson <bengt.gu...@beamways.com> wrote:

I still don't see a solution to the main cloning problem. I read n3339 but the "strawman" code was too much to take in, it would be better with a very slimmed down version which shows what it can accomplish. I assume that we can never get this to work:

class Base {
};

class Sub : public Base {
};

Base* b = new Sub;
clone_ptr
<Base> p = b;
clone_ptr
<Base> p2 = p;      // No knowledge of the existence of

Doesn't shared_ptr suffer from the same issue?  Seems okay in practice. 
-- 

Jonathan Coe

unread,
Jan 8, 2016, 10:30:19 AM1/8/16
to std-pr...@isocpp.org
A similar issue with shared_ptr can be saved by virtual destructors. 

I'm not proposing that we add virtual copy constructors.

Jon
--

Bengt Gustafsson

unread,
Jan 8, 2016, 3:17:09 PM1/8/16
to ISO C++ Standard - Future Proposals
Nevin: The deep_ptr is supposed to clone the pointee which the shared_ptr never does. That's the problem here: How can the exact subclass of the pointee be known at the later time when the cloning is to take place.

Many other languages support the eqivalent of a virtual copy ctor, and it would be simple to add to C++. Of course it would be totally nonlogical to allow virtual on exactly the copy ctor but in practice it would feel right, I think.

Ville Voutilainen

unread,
Jan 8, 2016, 3:25:57 PM1/8/16
to ISO C++ Standard - Future Proposals
On 8 January 2016 at 22:17, Bengt Gustafsson
<bengt.gu...@beamways.com> wrote:
> Nevin: The deep_ptr is supposed to clone the pointee which the shared_ptr never does. That's the problem here: How can the exact subclass of the pointee be known at the later time when the cloning is to take place.
>
> Many other languages support the eqivalent of a virtual copy ctor, and it would be simple to add to C++. Of course it would be totally nonlogical to allow virtual on exactly the copy ctor but in practice it would feel right, I think.


If you have a type hierarchy that can do polymorphic copies (via a
virtual clone() or some such), then
you can make it work. Other than that, the issue is exactly the same
as with shared_ptr, even though
shared_ptr doesn't clone. To explain how it's exactly the same issue
with shared_ptr, consider:

struct B {}; // note, no virtual destructor
struct D : B {~D();};

int main()
{
shared_ptr<B> b{new D()}; // this works fine
B* b2 = new D();
shared_ptr<B> b3{b2}; // this doesn't work
}

It's not an issue with cloning. It's an issue with type erasure. If
you create a cloned_ptr<B>
from a B*, all knowledge of the fact that the B* was actually really a
D* has been lost already.

Bengt Gustafsson

unread,
Jan 8, 2016, 6:31:33 PM1/8/16
to ISO C++ Standard - Future Proposals
Of course, but it is stlil a big difference in that you can easily add a virtual destructor to the baseclass and then it will automatically work for all subclasses. If you just adhere to this shuffling around the pointee between different shared_ptr instances is ok. And it was my bad not to include a virtual destructor in my example. What I wanted to show was that cloning could never work even with this in place (just as you remarked).

As for your shared_ptr example (as an aside), wouldn't it be nice to be able to flag that using a trait has_virtual_dtor<T> (needed on both types when doing a cross-type assignment between shared_ptrs). I guess this could break some code, but more often would reveal bugs not hitherto detected...

After reading Jonathans implementation I doubt that this can even work for the multiple-inheritance case. I don't mean just his code but in general: The U* inside the inner<U> object gets properly cast after the first copy operation, but after the second copying it wont, as the creation of the third inner object gets the template parameter of the second, not the first. It's late now and I may not be thinking straight, but at this point I think it is impossible (for multiple inheritance) without help from the pointee.

Bengt Gustafsson

unread,
Jan 8, 2016, 6:37:43 PM1/8/16
to ISO C++ Standard - Future Proposals
By the way, not even your first construction works, Ville, unless there is some magic I didn't know of in shared_ptr. Doesn't it just do "delete _ptr" in the last destructor? At least cppreference.com does not mention anything more advanced:


They refer to "delete expression" which links to the page for "delete _ptr"...


Den fredag 8 januari 2016 kl. 21:25:57 UTC+1 skrev Ville Voutilainen:

Ville Voutilainen

unread,
Jan 8, 2016, 6:54:40 PM1/8/16
to ISO C++ Standard - Future Proposals
On 9 January 2016 at 01:31, Bengt Gustafsson
<bengt.gu...@beamways.com> wrote:
> Of course, but it is stlil a big difference in that you can easily add a
> virtual destructor to the baseclass and then it will automatically work for
> all subclasses. If you just adhere to this shuffling around the pointee
> between different shared_ptr instances is ok. And it was my bad not to
> include a virtual destructor in my example. What I wanted to show was that
> cloning could never work even with this in place (just as you remarked).

Cloning works fine if you give a cloned_ptr<B> a D* instead of a B*. If you want
to make cloning work when you give a cloned_ptr<B> a B*, you must also provide
a custom cloner that can clone via a B*, using a virtual clone() or
some such facility.
I don't see why we should try to perform further magic in cloned_ptr
to solve this problem.

>
> As for your shared_ptr example (as an aside), wouldn't it be nice to be able
> to flag that using a trait has_virtual_dtor<T> (needed on both types when
> doing a cross-type assignment between shared_ptrs). I guess this could break
> some code, but more often would reveal bugs not hitherto detected...

That breaks existing code that uses shared_ptr with hierarchies without virtual
destructors, and that code works just fine. It also seemingly breaks
shared_ptr<void>
being a universal wrapper for any type.

> After reading Jonathans implementation I doubt that this can even work for
> the multiple-inheritance case. I don't mean just his code but in general:
> The U* inside the inner<U> object gets properly cast after the first copy
> operation, but after the second copying it wont, as the creation of the
> third inner object gets the template parameter of the second, not the first.
> It's late now and I may not be thinking straight, but at this point I think
> it is impossible (for multiple inheritance) without help from the pointee.

I haven't looked at his implementation in such detail, but the implementation
I have retains the original type identity in the original cloner,
which gets cloned as well,
so multiple copies don't break it. That's one of the reasons I wanted
to pursue a
type-erasing deep-copying smart pointer to begin with; doing it
without type erasure
makes it freakishly hard to do the right thing across multiple copies.

Ville Voutilainen

unread,
Jan 8, 2016, 6:56:16 PM1/8/16
to ISO C++ Standard - Future Proposals
On 9 January 2016 at 01:37, Bengt Gustafsson
<bengt.gu...@beamways.com> wrote:
> By the way, not even your first construction works, Ville, unless there is
> some magic I didn't know of in shared_ptr. Doesn't it just do "delete _ptr"

There's no "magic". It invokes the operator() of a default_delete<D>,
which does the right
thing. The incoming deleter (which is defaulted) is type-erased and
invoked indirectly.
The "first construction" I wrote absolutely works, and that's how it's
specified.

Jonathan Coe

unread,
Jan 8, 2016, 6:58:07 PM1/8/16
to std-pr...@isocpp.org
On 8 January 2016 at 23:31, Bengt Gustafsson <bengt.gu...@beamways.com> wrote:
Of course, but it is stlil a big difference in that you can easily add a virtual destructor to the baseclass and then it will automatically work for all subclasses. If you just adhere to this shuffling around the pointee between different shared_ptr instances is ok. And it was my bad not to include a virtual destructor in my example. What I wanted to show was that cloning could never work even with this in place (just as you remarked).

As for your shared_ptr example (as an aside), wouldn't it be nice to be able to flag that using a trait has_virtual_dtor<T> (needed on both types when doing a cross-type assignment between shared_ptrs). I guess this could break some code, but more often would reveal bugs not hitherto detected...

After reading Jonathans implementation I doubt that this can even work for the multiple-inheritance case. I don't mean just his code but in general: The U* inside the inner<U> object gets properly cast after the first copy operation, but after the second copying it wont, as the creation of the third inner object gets the template parameter of the second, not the first. It's late now and I may not be thinking straight, but at this point I think it is impossible (for multiple inheritance) without help from the pointee.


If construction is only possible through a factory method, then the static type and dynamic type of the object must be the same. 

To clarify, your earlier example was of a pointer whose dynamic type differed from the static type used in the pointer constructor.

1 Base* b = new Sub;
2 clone_ptr
<Base> p = b;
3 clone_ptr
<Base> p2 = p;  

I do not allow assignment or construction from raw pointers, only from deep_ptrs (clone_ptrs) so line 2 will not compile.

I don't see how multiple inheritance complicates this, can you elaborate?
 

Den fredag 8 januari 2016 kl. 21:25:57 UTC+1 skrev Ville Voutilainen:
On 8 January 2016 at 22:17, Bengt Gustafsson
<bengt.gu...@beamways.com> wrote:
> Nevin: The deep_ptr is supposed to clone the pointee which the shared_ptr never does. That's the problem here: How can the exact subclass of the pointee be known at the later time when the cloning is to take place.
>
> Many other languages support the eqivalent of a virtual copy ctor, and it would be simple to add to C++. Of course it would be totally nonlogical to allow virtual on exactly the copy ctor but in practice it would feel right, I think.


If you have a type hierarchy that can do polymorphic copies (via a
virtual clone() or some such), then
you can make it work. Other than that, the issue is exactly the same
as with shared_ptr, even though
shared_ptr doesn't clone. To explain how it's exactly the same issue
with shared_ptr, consider:

struct B {};  // note, no virtual destructor
struct D : B {~D();};

int main()
{
    shared_ptr<B> b{new D()}; // this works fine
    B* b2 = new D();
    shared_ptr<B> b3{b2}; // this doesn't work
}

It's not an issue with cloning. It's an issue with type erasure. If
you create a cloned_ptr<B>
from a B*, all knowledge of the fact that the B* was actually really a
D* has been lost already.

--

Ville Voutilainen

unread,
Jan 8, 2016, 7:07:26 PM1/8/16
to ISO C++ Standard - Future Proposals
On 9 January 2016 at 01:56, Ville Voutilainen
Ah, I see that the specification is not so easy to read. The constructor used
is the templated one, which takes a Y* p. Cryptic specification aside,
shared_ptr
will destroy it by doing a delete p (or the as-if-equivalent of that
expression), which
means it will destroy a Y*, not a T*. Hence there's no requirement for a virtual
destructor and the code works as expected.

Ville Voutilainen

unread,
Jan 8, 2016, 7:08:28 PM1/8/16
to ISO C++ Standard - Future Proposals
On 9 January 2016 at 01:58, Jonathan Coe <jb...@me.com> wrote:
> If construction is only possible through a factory method, then the static
> type and dynamic type of the object must be the same.

Seems draconian, and there's also .reset().

> To clarify, your earlier example was of a pointer whose dynamic type
> differed from the static type used in the pointer constructor.
>
> 1 Base* b = new Sub;
> 2 clone_ptr<Base> p = b;
> 3 clone_ptr<Base> p2 = p;
>
> I do not allow assignment or construction from raw pointers, only from
> deep_ptrs (clone_ptrs) so line 2 will not compile.

You do not allow construction from raw pointers? :) Surely that's not the case.

Jonathan Coe

unread,
Jan 8, 2016, 7:12:07 PM1/8/16
to std-pr...@isocpp.org
I can't see how to ensure that the deep copy is correct unless the static and dynamic types of the pointer can be guaranteed to be equal at compile time.

It seems necessary and meets my (rather simple) requirements.

Jon

Ville Voutilainen

unread,
Jan 8, 2016, 7:20:51 PM1/8/16
to ISO C++ Standard - Future Proposals
On 9 January 2016 at 02:12, Jonathan Coe <jb...@me.com> wrote:
> I can't see how to ensure that the deep copy is correct unless the static
> and dynamic types of the pointer can be guaranteed to be equal at compile
> time.
>
> It seems necessary and meets my (rather simple) requirements.

Huh? You seem to be saying that a cloned_ptr<B> can't construct from a D*.
Or that a cloned_ptr<void> can't construct from anything. The deep copy can be
made perfectly correct in those cases just like the "deep destruction" can be
made safe in shared_ptr; via a constructor template, construct a cloner that
clones and destroys a D*, not a B*, erase its type and store it.

Jonathan Coe

unread,
Jan 8, 2016, 7:24:36 PM1/8/16
to std-pr...@isocpp.org
I agree that it can be made safe but it cannot be guaranteed to be safe.

I'm looking for stricter requirements than shared_ptr's destructor as I cannot make the problem go away by introducing virtual copy constructors. Any 'issues' with shared_ptr can be fixed with the introduction of virtual destructors.

Jon

Jonathan Coe

unread,
Jan 8, 2016, 7:29:49 PM1/8/16
to std-pr...@isocpp.org
On 9 January 2016 at 00:20, Ville Voutilainen <ville.vo...@gmail.com> wrote:
The potential issue here is that the D* points to a further derived DD*. If I copy it as a D I will slice it and do the wrong thing.
I need to know the dynamic type of a pointer to copy the pointee correctly.

(apologies for earlier top post. GMail makes it very easy to do.)

Ville Voutilainen

unread,
Jan 8, 2016, 7:31:16 PM1/8/16
to ISO C++ Standard - Future Proposals
On 9 January 2016 at 02:24, Jonathan Coe <jb...@me.com> wrote:
> I agree that it can be made safe but it cannot be guaranteed to be safe.
>
> I'm looking for stricter requirements than shared_ptr's destructor as I
> cannot make the problem go away by introducing virtual copy constructors.
> Any 'issues' with shared_ptr can be fixed with the introduction of virtual
> destructors.

Assuming that you can introduce a virtual destructor. That's not the case for
void, nor is it the case for hierarchies where the base class has a non-virtual
protected destructor, for instance. And yes, you can make the problem go away
by introducing a "virtual copy constructor", it just cannot be an
actual constructor,
and you need to pass a custom cloner. :)

Ville Voutilainen

unread,
Jan 8, 2016, 7:35:08 PM1/8/16
to ISO C++ Standard - Future Proposals
On 9 January 2016 at 02:29, Jonathan Coe <jb...@me.com> wrote:
>> Huh? You seem to be saying that a cloned_ptr<B> can't construct from a D*.
>> Or that a cloned_ptr<void> can't construct from anything. The deep copy
>> can be
>> made perfectly correct in those cases just like the "deep destruction" can
>> be
>> made safe in shared_ptr; via a constructor template, construct a cloner
>> that
>> clones and destroys a D*, not a B*, erase its type and store it.
> The potential issue here is that the D* points to a further derived DD*. If
> I copy it as a D I will slice it and do the wrong thing.
> I need to know the dynamic type of a pointer to copy the pointee correctly.


I'm aware of that issue, but the capability of using a cloned_ptr<B>
with any derived type in the hierarchy is much more compelling to me
than absolute
safety. Granted, it's possible to first construct a cloned_ptr<DD> and
then construct
a cloned_ptr<B> from that, but I think the
converting-pointer-constructor requiring
the dynamic type to be B* seems overkill to me.

Jonathan

unread,
Jan 8, 2016, 7:41:55 PM1/8/16
to std-pr...@isocpp.org
Safety vs utility is a tough call. I'll work on a detailed proposal and weigh up the pros and cons of different approaches.

Patrice Roy

unread,
Jan 8, 2016, 9:57:11 PM1/8/16
to std-pr...@isocpp.org
Whenever I've done similar things, I went with:

#include <type_traits>
struct Cloneable
{
   virtual Cloneable* clone() const = 0;
   virtual ~Cloneable() = default;
protected:
   Cloneable(const Cloneable&) = default;
};

struct Copyer
{
   template <class T> T* operator()(const T &obj) const { return new T{obj}; }
};

struct Cloner
{
   template <class T> T* operator()(const T &obj) const { return obj.clone(); }
};

template <class T>
   struct dup_type
   {
      using type = std:conditional_t<
         std::is_convertible<T*, Cloneable*>::value, Cloner, Copyer
      >;
   };

template <class T, class Dup = typename dup_type<T>::type>
   class dup_ptr
   {
      T *ptr;
      Dup dup;
   // ...
   public:
      dup_ptr(const dup_ptr &other)
         : ptr{ other.ptr? dup(*other.ptr) : nullptr }
      {
      }
   // ...
   };

The things that have always annoyed me were (a) using raw pointers, since smart pointer return types in this case would not be covariant in the clone() method, and (b) the reliance on a specific interface, although there might be a traits-based solution to this (there's a trade-off here, so I'm not convinced it would work for all relevant use-cases).

Apart from this, though, it works fine, and it's been useful in real-life use-cases, so it might be useful to refine all this and make it into something standard.

Cheers!


Germán Diago

unread,
Jan 8, 2016, 10:33:02 PM1/8/16
to ISO C++ Standard - Future Proposals
This may be of interest for the proposal: http://mnmlstc.github.io/core/memory.html


Patrice Roy

unread,
Jan 8, 2016, 10:52:42 PM1/8/16
to std-pr...@isocpp.org
Glad if it is. It works, it works well, and I use it in specialized contexts. The problem is that the drawbacks (mentioned in my previous message) are a pain for standard types, at least in my mind.

I'm one of those who finds things such as the interaction between using ... and IDisposable ... in C

2016-01-08 22:33 GMT-05:00 Germán Diago <germa...@gmail.com>:
This may be of interest for the proposal: http://mnmlstc.github.io/core/memory.html


Patrice Roy

unread,
Jan 8, 2016, 10:53:05 PM1/8/16
to std-pr...@isocpp.org
... C# unpleasant (sorry for the accidental «send»)

David Krauss

unread,
Jan 9, 2016, 2:40:16 AM1/9/16
to std-pr...@isocpp.org
On 2016–01–09, at 8:41 AM, Jonathan <jonath...@gmail.com> wrote:

On 9 Jan 2016, at 00:35, Ville Voutilainen <ville.vo...@gmail.com> wrote:

I'm aware of that issue, but the capability of using a cloned_ptr<B>
with any derived type in the hierarchy is much more compelling to me
than absolute
safety. Granted, it's possible to first construct a cloned_ptr<DD> and
then construct
a cloned_ptr<B> from that, but I think the
converting-pointer-constructor requiring
the dynamic type to be B* seems overkill to me.

Safety vs utility is a tough call. I'll work on a detailed proposal and weigh up the pros and cons of different approaches. 

There shouldn’t be any safety shortfall here. If you want, the constructor template could assert that the dynamic and static types of its argument are equal using typeid. However, C++ typically allows (dangerous) slicing, so it would be an unusual restriction. For non-polymorphic types, typeid wouldn’t work, but in such cases, is the most-derived object actually part of the value or not? Not that the danger level is zero, but it’s probably better to follow precedents.

Best practice with an ownership facility like value_ptr is simply to apply it early, before any slicing happens.

I explored the interaction between custom allocation and cloning handles in P0043R0. For a program using memory pools, getting the custom allocator to make a clone is only half the battle. It’s also important to maintain pool invariants by transferring objects between pools. But, as Ville mentioned, propagation traits don’t mix with type erasure. Library Fundamentals polymorphic memory resources (PMR) help by setting a fixed propagation policy, but that policy is container-like and not “ptr”-like. A PMR value_ptr would solve a slightly different problem than one with a built-in, value-semantic cloner. Both problems are useful to solve.

You might consider the P0043 approach of splitting functionality between separate value_ptr and value_container templates. Let value_ptr implement allocator type erasure and cloning but not propagation traits nor pool transfers. It satisfies the 95% of folks who don’t want to think about allocation. Let value_container guarantee that its value is kept in a particular pool, defined by a classic allocator. This cleanly handles the remaining cases, including PMR. The two templates would be interoperable, so you could take a pooled resource, pass it to a pool-unaware library, get a clone back, and move the clone to a new pool.

Bengt Gustafsson

unread,
Jan 9, 2016, 1:19:47 PM1/9/16
to ISO C++ Standard - Future Proposals
Here is your code:

struct B {};  // note, no virtual destructor 
struct D : B {~D();}; 

int main() 

    shared_ptr<B> b{new D()}; // this works fine 
    ...
}

The "magic" I refer to here is that a destroyer for D rather than B is apparently embedded in the newly created control block of the shared_ptr<B> being created. Of course this is not real magic (there is no such thing) but it makes a shared_ptr "better" at doing a correct destruction than a B* would be which seems to be somewhat out of the scope of what you would expect a shared pointer to do, i.e. managing a reference count.

Ville Voutilainen

unread,
Jan 9, 2016, 1:33:58 PM1/9/16
to ISO C++ Standard - Future Proposals
On 9 January 2016 at 20:19, Bengt Gustafsson
<bengt.gu...@beamways.com> wrote:
> Here is your code:
>
> struct B {}; // note, no virtual destructor
> struct D : B {~D();};
>
> int main()
> {
> shared_ptr<B> b{new D()}; // this works fine
> ...
> }
>
> The "magic" I refer to here is that a destroyer for D rather than B is
> apparently embedded in the newly created control block of the shared_ptr<B>

Indeed. b could be shared_ptr<void>, and it would still work.

> being created. Of course this is not real magic (there is no such thing) but
> it makes a shared_ptr "better" at doing a correct destruction than a B*
> would be which seems to be somewhat out of the scope of what you would
> expect a shared pointer to do, i.e. managing a reference count.

Well, it just so happens that managing reference count for anything is quite
convenient, hence shared_ptr<void>, and the mechanism to make that work
also conveniently makes class hierarchies without virtual destructors work.
Since shared_ptr can do that, I see no reason why cloned_ptr shouldn't be
able to do it too.

Bengt Gustafsson

unread,
Jan 9, 2016, 1:47:35 PM1/9/16
to ISO C++ Standard - Future Proposals, jb...@me.com
Regarding the problems with multiple inheritance:

the general problem with multiple inheritance is that casting between base and subclass can involve addition of an offset to the this pointer.

A naive inplementation of clone_ptr would create an inner object (in your parlance) which contains a T* to the original T and a copy constructor calling virtual method. When the original deep_ptr<T> is assigned into another deep_ptr<T2>  where T2 is the second base of T the get() of the cloned inner object would still return the non-offsetted T* which does not point at the T2 part, and fails.

Your implementation is better than this, which is possible as the operator= between deep_ptr<T> and deep_ptr<T2> knows of both these types and creates not a plain clone of the incoming inner object but a deep_ptr<T2>::inner<T> object, which knows how to do the cast.

However, on the second copy to a new deep_prt<T3> which has some other offset of pointers there is a bad thing happening. In your case this is in (for instance) when you do

 reinterpret_cast<const typename deep_ptr<U>::inner *>(&u.buffer_)->copy(&buffer_);

This copy() call causes a new deep_prt<T2>::inner_impl<T>() to be created rather than the deep_ptr<T3>::inner_impl<T> that is required. This will create an offsetting error when get() is called, or with virtual bases, worse. Unfortunately I can't see a way around this problem, except doing some offset arithmetic to sum up all the differences in the different copy steps, and this still doesn't handle virtual base classes.

to try to make this more understandable, in this code the erroneous vtbl is inserted:


void copy(void *buffer) const override {
 
new (buffer) inner_impl(new U(*u_));
 
}

This code is called in the inner object inside the deep_ptr<T2> but with the buffer pointing to the member of the deep_prt<T3> so when the get() method does its reinterpret_cast in:

 const T *get() const {
     
return reinterpret_cast<const inner *>(&buffer_)->get();
 
}

It erroneously casts a deep_ptr<T2>::inner to a deep_ptr<T3>::inner, thus misinterpreting the T2* that the inner object returns as a T3* with the offset error as result.

This is what I think wlil happen, I haven't actually tested it and it is really tricky code... I hope it works better! Also it seems that the current shared_ptr would have the same issue which someone surely would have discovered by now... still I can't figure out how to get around it.

Bengt Gustafsson

unread,
Jan 9, 2016, 1:51:48 PM1/9/16
to ISO C++ Standard - Future Proposals

Well, it just so happens that managing reference count for anything is quite
convenient, hence shared_ptr<void>, and the mechanism to make that work
also conveniently makes class hierarchies without virtual destructors work.
Since shared_ptr can do that, I see no reason why cloned_ptr shouldn't be
able to do it too.

I hope so but maybe it is not so easy, see the post that I just sent with a compromising example (I think). Maybe there are some
restrictions on multiple inheritance or maybe I'm just plain wrong, but so far I can't see how this could work in a general case even for shared_ptr. (I'm not refering to shared_ptr<void> then which obviously does not have to know anything about casting between bases and subclasses).

Jonathan Coe

unread,
Jan 9, 2016, 3:06:38 PM1/9/16
to std-pr...@isocpp.org


On 9 Jan 2016, at 18:47, Bengt Gustafsson <bengt.gu...@beamways.com> wrote:

Regarding the problems with multiple inheritance:

the general problem with multiple inheritance is that casting between base and subclass can involve addition of an offset to the this pointer.

A naive inplementation of clone_ptr would create an inner object (in your parlance) which contains a T* to the original T and a copy constructor calling virtual method. When the original deep_ptr<T> is assigned into another deep_ptr<T2>  where T2 is the second base of T the get() of the cloned inner object would still return the non-offsetted T* which does not point at the T2 part, and fails.

Your implementation is better than this, which is possible as the operator= between deep_ptr<T> and deep_ptr<T2> knows of both these types and creates not a plain clone of the incoming inner object but a deep_ptr<T2>::inner<T> object, which knows how to do the cast.

However, on the second copy to a new deep_prt<T3> which has some other offset of pointers there is a bad thing happening. In your case this is in (for instance) when you do

 reinterpret_cast<const typename deep_ptr<U>::inner *>(&u.buffer_)->copy(&buffer_);

This copy() call causes a new deep_prt<T2>::inner_impl<T>() to be created rather than the deep_ptr<T3>::inner_impl<T> that is required. This will create an offsetting error when get() is called, or with virtual bases, worse. Unfortunately I can't see a way around this problem, except doing some offset arithmetic to sum up all the differences in the different copy steps, and this still doesn't handle virtual base classes.


I don't think I see the problem.
deep_ptr<A>::inner_impl<T> is the same for any choice of A. The important type info is T which is retained. If T has multiple virtual bases then it knows how to apply offsets to return pointers correctly. The choice of A has no bearing on this. 

I'll add Gustaffsons-dilemma as a test case.

Bengt Gustafsson

unread,
Jan 9, 2016, 6:33:10 PM1/9/16
to ISO C++ Standard - Future Proposals
No it's not: the offset addition that it has to do to convert from U to T in get() may differ (using the tpl parameter names of your code). The problem is not in copy() itself but in that copy instates an inner object with the wrong T in the receiving object, and the offset to add depends on the T and U combination: Wrong T, potentially wrong offset. (I hope I'm wrong here but you haven't convinced me yet).

Why don't you settle this by testing? If you can't get it to fail I will give it a shot tomorrow, to late now.

Ville Voutilainen

unread,
Jan 9, 2016, 6:49:33 PM1/9/16
to ISO C++ Standard - Future Proposals
On 10 January 2016 at 01:33, Bengt Gustafsson <bengt.gu...@beamways.com> wrote:
No it's not: the offset addition that it has to do to convert from U to T in get() may differ (using the tpl parameter names of your code). The problem is not in copy() itself but in that copy instates an inner object with the wrong T in the receiving object, and the offset to add depends on the T and U combination: Wrong T, potentially wrong offset. (I hope I'm wrong here but you haven't convinced me yet).

The offset depends on the call entry's type(*) and the covariant type the type-erased override
actually returns. shared_ptr works fine with the case you think is problematic, because
the offset is adjusted at run-time. I don't know whether Jonathan's implementation gets it
right with its reinterpret_cast dance, but shared_ptr and my implementation do.

(*) For a cloned_ptr<B1>, that type is B1*. For cloned_ptr<B2>, that type is B2*. The run-time
offset adjustment does the right thing for both, even though the override of the pointer-getting
function in the type-erased implementation returns a D* (assuming struct D : B1, B2 {}).

This doesn't even have much to do with multiple inheritance, the same issue would arise
with

struct B {};

struct D : B {};

struct E : D {};

and converting E* to D* or B*, depending on who's calling. :)

Jonathan Coe

unread,
Jan 9, 2016, 7:11:02 PM1/9/16
to std-pr...@isocpp.org
On 9 January 2016 at 23:33, Bengt Gustafsson <bengt.gu...@beamways.com> wrote:
No it's not: the offset addition that it has to do to convert from U to T in get() may differ (using the tpl parameter names of your code). The problem is not in copy() itself but in that copy instates an inner object with the wrong T in the receiving object, and the offset to add depends on the T and U combination: Wrong T, potentially wrong offset. (I hope I'm wrong here but you haven't convinced me yet).

Why don't you settle this by testing? If you can't get it to fail I will give it a shot tomorrow, to late now.


I have settled this by testing. You are correct.

I've restricted my design, following Germán Diago's example, somewhat so that copies and assignment from different types cannot take place. This resolves "Gustafsson's dilemma" albeit somewhat unsatisfactorarily. The restricted design is still fit for my original purpose. I'll add notes on this to the draft proposal. Thanks for your continued interest and contribution.

Ville Voutilainen

unread,
Jan 9, 2016, 7:21:25 PM1/9/16
to ISO C++ Standard - Future Proposals
On 10 January 2016 at 02:11, Jonathan Coe <jb...@me.com> wrote:
Why don't you settle this by testing? If you can't get it to fail I will give it a shot tomorrow, to late now.


I have settled this by testing. You are correct.

I've restricted my design, following Germán Diago's example, somewhat so that copies and assignment from different types cannot take place. This resolves "Gustafsson's dilemma" albeit somewhat unsatisfactorarily. The restricted design is still fit for my original purpose. I'll add notes on this to the draft proposal. Thanks for your continued interest and contribution.

So, are you saying that the test at the end of
 https://github.com/jbcoe/deep_ptr/blob/master/TestDeepPtr.cpp
fails with your implementation?

David Krauss

unread,
Jan 10, 2016, 5:36:10 AM1/10/16
to std-pr...@isocpp.org, Jonathan Coe

On 2016–01–10, at 7:49 AM, Ville Voutilainen <ville.vo...@gmail.com> wrote:

The offset depends on the call entry's type(*) and the covariant type the type-erased override
actually returns. shared_ptr works fine with the case you think is problematic, because
the offset is adjusted at run-time. I don't know whether Jonathan's implementation gets it
right with its reinterpret_cast dance, but shared_ptr and my implementation do.

shared_ptr stores the pointer to the complete object (or the value provided along with a custom deleter) in the control block. The result of get() is exactly the one stored locally in the shared_ptr object, and the argument to the deleter is exactly the control block slot. Neither ever gets an offset adjustment, and there need not be any relationship at all between the two pointer values.

Like shared_ptr, value_ptr type-erases its deleter, meaning it potentially goes on the heap. So the value_ptr class certainly needs a pointer to a private heap block. I think it would save a lot of trouble to let value_ptr work like a shared_ptr with a reference count fixed at 1, and cloning functionality tacked on. Specifically, cache the value of get(), and put a heap-allocated control block in charge of destruction and cloning. Then,

—  sizeof(value_ptr<T>)==2*sizeof(void*), which is the minimum.
get() is implemented by a single fetch instruction.
— There’s room for all the type-erasure you might want.
— In the common case of no casts and no custom allocator/deleter, the control block pointer can be nullptr, and fall back to default behavior.

Jonathan Coe

unread,
Jan 10, 2016, 5:39:12 AM1/10/16
to std-pr...@isocpp.org
The test 'Gustafssons dilemma' breaks with my implementation in version 643d9635b2895f7bb77dc4fb9e9597c1b358a4e4.


It's not clear to me why this breaks. 

I don't need the ability to construct/assign derived-type deep_ptrs from base-type deep_ptr so have removed this functionality from my newer simpler implementation (no buffer). 
 

It would be useful to understand which limitations are fundamental and which are facets of the implementation.

Ville Voutilainen

unread,
Jan 10, 2016, 9:07:24 AM1/10/16
to ISO C++ Standard - Future Proposals
On 10 January 2016 at 12:39, Jonathan Coe <jb...@me.com> wrote:
> The test 'Gustafssons dilemma' breaks with my implementation in version
> 643d9635b2895f7bb77dc4fb9e9597c1b358a4e4.
>
> https://github.com/jbcoe/deep_ptr/tree/643d9635b2895f7bb77dc4fb9e9597c1b358a4e4
>
> It's not clear to me why this breaks.
>
> I don't need the ability to construct/assign derived-type deep_ptrs from
> base-type deep_ptr so have removed this functionality from my newer simpler
> implementation (no buffer).

Well, while that's backwards (such a conversion requires a cast, quite
like static_pointer_cast),
conversions from derived-type pointers to base-type pointers should work.

> https://github.com/jbcoe/deep_ptr/tree/827bfaadff635f4b1bcb9e13ec013187150b8ce9
> It would be useful to understand which limitations are fundamental and which
> are facets of the implementation.


That particilar breakage is due to your implementation. The test works
with the approach
I have.

Ville Voutilainen

unread,
Jan 10, 2016, 9:12:54 AM1/10/16
to ISO C++ Standard - Future Proposals
On 10 January 2016 at 12:36, David Krauss <pot...@gmail.com> wrote:
> shared_ptr stores the pointer to the complete object (or the value provided
> along with a custom deleter) in the control block. The result of get() is
> exactly the one stored locally in the shared_ptr object, and the argument to
> the deleter is exactly the control block slot. Neither ever gets an offset
> adjustment, and there need not be any relationship at all between the two
> pointer values.

The offset adjustment happens in get().

> Like shared_ptr, value_ptr type-erases its deleter, meaning it potentially
> goes on the heap. So the value_ptr class certainly needs a pointer to a
> private heap block. I think it would save a lot of trouble to let value_ptr
> work like a shared_ptr with a reference count fixed at 1, and cloning
> functionality tacked on.

That's what my current implementation does...

>Specifically, cache the value of get(), and put a

...although I don't know why this bit would be significant.

What I'm currently looking at is the annoyance of having to dynamically allocate
a delegating cloner, which wraps the incoming cloner. That delegating cloner
is what makes the conversions work, since it'll delegate cloning/deleting a B*
to a cloner that wants a D*. Having all the allocations of the cloner
it self performed
by the cloner is straightforward, having the allocation of the delegating cloner
not so much. Solving that problem would bring forth complete minimal allocator
support.

David Krauss

unread,
Jan 10, 2016, 9:34:19 AM1/10/16
to std-pr...@isocpp.org
On 2016–01–10, at 10:12 PM, Ville Voutilainen <ville.vo...@gmail.com> wrote:

On 10 January 2016 at 12:36, David Krauss <pot...@gmail.com> wrote:
shared_ptr stores the pointer to the complete object (or the value provided
along with a custom deleter) in the control block. The result of get() is
exactly the one stored locally in the shared_ptr object, and the argument to
the deleter is exactly the control block slot. Neither ever gets an offset
adjustment, and there need not be any relationship at all between the two
pointer values.

The offset adjustment happens in get().


There’s no casting. Perhaps you’re thinking of something other than shared_ptr?

Like shared_ptr, value_ptr type-erases its deleter, meaning it potentially
goes on the heap. So the value_ptr class certainly needs a pointer to a
private heap block. I think it would save a lot of trouble to let value_ptr
work like a shared_ptr with a reference count fixed at 1, and cloning
functionality tacked on.

That's what my current implementation does...

Specifically, cache the value of get(), and put a

...although I don't know why this bit would be significant.

It saves a trip through type erasure.

What I'm currently looking at is the annoyance of having to dynamically allocate
a delegating cloner, which wraps the incoming cloner. That delegating cloner
is what makes the conversions work, since it'll delegate cloning/deleting a B*
to a cloner that wants a D*.

Use a std::function inside the control block. A chain of conversions might entail a nested chain of std::functions. There are optimizations: you could be fancy and collapse the chain for simple offset arithmetic. Eliminate the entire control block and use static defaults for the common case of no conversion or customization, as I mentioned. But, I’d implement the general case first and optimize after.

Ville Voutilainen

unread,
Jan 10, 2016, 9:40:12 AM1/10/16
to ISO C++ Standard - Future Proposals
On 10 January 2016 at 16:34, David Krauss <pot...@gmail.com> wrote:
> The offset adjustment happens in get().
>
> Here’s libc++ shared_ptr::get:
> https://github.com/llvm-mirror/libcxx/blob/master/include/memory#L2714
> Here’s libstdc++:
> https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/shared_ptr_base.h#L1063

I have no idea why you're pointing me to libc++'s unique_ptr.

> There’s no casting. Perhaps you’re thinking of something other than
> shared_ptr?

There's no casting, the conversion is implicit, and the covariant
return of the override
in the type-erased implementation does the adjustment.

> Specifically, cache the value of get(), and put a
>
>
> ...although I don't know why this bit would be significant.
>
>
> It saves a trip through type erasure.

Ah, so nothing significant design-wise.

>
> What I'm currently looking at is the annoyance of having to dynamically
> allocate
> a delegating cloner, which wraps the incoming cloner. That delegating cloner
> is what makes the conversions work, since it'll delegate cloning/deleting a
> B*
> to a cloner that wants a D*.
>
>
> Use a std::function inside the control block. A chain of conversions might

That doesn't help.

Ville Voutilainen

unread,
Jan 10, 2016, 9:52:44 AM1/10/16
to ISO C++ Standard - Future Proposals
On 10 January 2016 at 16:40, Ville Voutilainen
<ville.vo...@gmail.com> wrote:
> On 10 January 2016 at 16:34, David Krauss <pot...@gmail.com> wrote:
>> The offset adjustment happens in get().
>>
>> Here’s libc++ shared_ptr::get:
>> https://github.com/llvm-mirror/libcxx/blob/master/include/memory#L2714
>> Here’s libstdc++:
>> https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/shared_ptr_base.h#L1063
>
> I have no idea why you're pointing me to libc++'s unique_ptr.
>
>> There’s no casting. Perhaps you’re thinking of something other than
>> shared_ptr?
>
> There's no casting, the conversion is implicit, and the covariant
> return of the override
> in the type-erased implementation does the adjustment.

Well, ok, libstdc++ shared_ptr does the offset adjustment when it
stores the incoming
pointer, and gives the original pointer to the refcount, which stores
that original pointer
and deleter, thus achieving the caching you mention below. So no need
to do it on every
get().

David Krauss

unread,
Jan 10, 2016, 10:04:08 AM1/10/16
to std-pr...@isocpp.org

On 2016–01–10, at 10:52 PM, Ville Voutilainen <ville.vo...@gmail.com> wrote:

Well, ok, libstdc++ shared_ptr does the offset adjustment when it
stores the incoming
pointer, and gives the original pointer to the refcount, which stores
that original pointer
and deleter, thus achieving the caching you mention below. So no need
to do it on every
get().

libc++ and libstdc++ work the same. If you use a converting constructor, then that encapsulates a pointer conversion. If you use another overload [util.smartptr.shared.const] §20.8.2.2.1/13, then the “cached” value is whatever you want it to be — no pointer conversion or offset adjustment need exist. Such a constructor would be impossible for value_ptr, but reasonable parity could be achieved by a constructor taking an arbitrary functor (e.g. an accessor wrapped in a lambda) which is used per clone to generate cached pointers.

The lazy approach to value_ptr design would be to reuse shared_ptr implementation, and the results should be good. Or so I think. Maybe I’ll take a shot, but there are other things I should be doing now.

Ville Voutilainen

unread,
Jan 10, 2016, 10:12:33 AM1/10/16
to ISO C++ Standard - Future Proposals
On 10 January 2016 at 17:04, David Krauss <pot...@gmail.com> wrote:
>
> On 2016–01–10, at 10:52 PM, Ville Voutilainen <ville.vo...@gmail.com>
> wrote:
>
> Well, ok, libstdc++ shared_ptr does the offset adjustment when it
> stores the incoming
> pointer, and gives the original pointer to the refcount, which stores
> that original pointer
> and deleter, thus achieving the caching you mention below. So no need
> to do it on every
> get().
>
>
> libc++ and libstdc++ work the same. If you use a converting constructor,
> then that encapsulates a pointer conversion. If you use another overload
> [util.smartptr.shared.const] §20.8.2.2.1/13, then the “cached” value is
> whatever you want it to be — no pointer conversion or offset adjustment need
> exist. Such a constructor would be impossible for value_ptr, but reasonable

Which part of it is impossible for value_ptr?

> parity could be achieved by a constructor taking an arbitrary functor (e.g.
> an accessor wrapped in a lambda) which is used per clone to generate cached
> pointers.

Sounds overly complicated. As long as the cloner has the original
pointer type, like
shared_ptr deleter does, it works just fine, and the 'cached' pointer
can be of the type
the topmost level expects.

>
> The lazy approach to value_ptr design would be to reuse shared_ptr
> implementation, and the results should be good. Or so I think. Maybe I’ll
> take a shot, but there are other things I should be doing now.

I already have a value_ptr that is using shared_ptr internally to do
type erasure.

David Krauss

unread,
Jan 10, 2016, 10:40:33 AM1/10/16
to std-pr...@isocpp.org
On 2016–01–10, at 11:12 PM, Ville Voutilainen <ville.vo...@gmail.com> wrote:

Which part of it is impossible for value_ptr?

The premise of shared_ptr<T>::shared_ptr(const shared_ptr<Y>& r, T* p) is to pass a p which is owned by r. If the same thing were done for value_ptr, the clone of r wouldn’t own p. So the equivalent would instead be value_ptr<T>::value_ptr(const value_ptr<Y>& r, F f) where F is an accessor taking Y* and returning T*. Complicated, maybe, but very general, since the various pointer conversions can be synthesized by F accessors.

The lazy approach to value_ptr design would be to reuse shared_ptr
implementation, and the results should be good. Or so I think. Maybe I’ll
take a shot, but there are other things I should be doing now.

I already have a value_ptr that is using shared_ptr internally to do
type erasure.

Hmm, so why not delegate value_ptr::get() to the encapsulated shared_ptr and let it mediate between differing types? Guess I’d have to take a look. There are a bunch of things going on, I’m certainly not saying it’s trivial…

Ville Voutilainen

unread,
Jan 10, 2016, 10:44:08 AM1/10/16
to ISO C++ Standard - Future Proposals
On 10 January 2016 at 17:40, David Krauss <pot...@gmail.com> wrote:
>
> On 2016–01–10, at 11:12 PM, Ville Voutilainen <ville.vo...@gmail.com>
> wrote:
>
> Which part of it is impossible for value_ptr?
>
>
> The premise of shared_ptr<T>::shared_ptr(const shared_ptr<Y>& r, T* p) is to
> pass a p which is owned by r. If the same thing were done for value_ptr, the
> clone of r wouldn’t own p. So the equivalent would instead be
> value_ptr<T>::value_ptr(const value_ptr<Y>& r, F f) where F is an accessor
> taking Y* and returning T*. Complicated, maybe, but very general, since the
> various pointer conversions can be synthesized by F accessors.

I haven't considered supporting that particular function, and will not
support it
unless someone explains why it's essential.

> I already have a value_ptr that is using shared_ptr internally to do
> type erasure.
>
>
> Hmm, so why not delegate value_ptr::get() to the encapsulated shared_ptr and
> let it mediate between differing types? Guess I’d have to take a look. There
> are a bunch of things going on, I’m certainly not saying it’s trivial…


I already do that delegation. The "Gustafsson Dilemma" doesn't exist,
there are multiple
ways to avoid it: either do what shared_ptr does or use a covariant
return in a type-erased
implementation. The former is better.

Bengt Gustafsson

unread,
Jan 10, 2016, 11:41:50 AM1/10/16
to ISO C++ Standard - Future Proposals
Ok, I think I have straightened all this out. as you have concluded the trick that makes shared_ptr work is that in each copy construction/assignment step the offset addition is done to get a pointer valid for the new T, while the contorl block holds the original pointer it knows how to delete.

Not so easy for a deep_ptr. What we have is a function that can clone the original pointer and return a new pointer to the original type. But this pointer may be several copy construction steps away from the T of the current deep_ptr being copied. The memory of how those conversions were done is long lost.

It is however, as someone mentioned, possible to preserve a chain of pointer converters, but it requires heap storage (which can be small-object optimized). I have a sample implementation which does this, attached.

I admit that my original "Gustafsson dilemma" code required downcasts but as you astute people have concluded the problem arises even with only upcasts in the face of multiple inheritance (I didn't even know of the covariant return type issue with vtbls so I didn't consider that). Attached is a test program with upcasts that breaks unless you have the caster chain in place.

I assume that my function pointer vector can be replaced by some lambda expression stored in a std::function but that's a detail, the idea is the same.
bg_deep_ptr.h
deep_ptr_breaker.cpp

Ville Voutilainen

unread,
Jan 10, 2016, 12:16:38 PM1/10/16
to ISO C++ Standard - Future Proposals
On 10 January 2016 at 18:41, Bengt Gustafsson
<bengt.gu...@beamways.com> wrote:
> Ok, I think I have straightened all this out. as you have concluded the
> trick that makes shared_ptr work is that in each copy
> construction/assignment step the offset addition is done to get a pointer
> valid for the new T, while the contorl block holds the original pointer it
> knows how to delete.
>
> Not so easy for a deep_ptr. What we have is a function that can clone the
> original pointer and return a new pointer to the original type. But this
> pointer may be several copy construction steps away from the T of the
> current deep_ptr being copied. The memory of how those conversions were done
> is long lost.

That's not the issue. A templated conversion constructor can take the
pointer type
from the source cloner, and convert at that point.

>
> It is however, as someone mentioned, possible to preserve a chain of pointer
> converters, but it requires heap storage (which can be small-object
> optimized). I have a sample implementation which does this, attached.

I use a chain of converters because I need to *hold* the different cloners, not
because there would be an issue with invoking them when converting.

The allocation problem is real, though. A cloner can clone the value,
clone itself,
destroy the value (and destroy the cloner itself, although my
prototype doesn't do
that bit yet) fine, but allocating the delegate is a different matter.

I'll attach the current version I have. It's very much a work-in-progress.
cloned_ptr.cpp

Ville Voutilainen

unread,
Jan 10, 2016, 12:18:45 PM1/10/16
to ISO C++ Standard - Future Proposals
On 10 January 2016 at 19:16, Ville Voutilainen
<ville.vo...@gmail.com> wrote:
> I use a chain of converters because I need to *hold* the different cloners, not
> because there would be an issue with invoking them when converting.

And also, the converter is needed to adapt the original cloner to the
new one, sure.
That takes care of multiple conversions, the original type is forever
retained. One-step
conversions would work otherwise, too, but multi-step needs the adaptation, and
the specific-type in the target also needs the adaptation so that the
cloner can be
stored in the target.

Jonathan Coe

unread,
Jan 10, 2016, 12:18:49 PM1/10/16
to std-pr...@isocpp.org
I have updated my implementation. As Ville said, the failing Gustafsson's dilemma test was a facet of my poor implementation.
Tests now pass.

My implementation propagates constness. This is not necessary but to my mind makes sense as a deep_ptr is intended to extend the logical state of a object. What do others think?

I have made the pointer constructor public and explicit - we should be consistent with shared_ptr.

Implementation is here: https://github.com/jbcoe/deep_ptr

--

Ville Voutilainen

unread,
Jan 10, 2016, 12:22:16 PM1/10/16
to ISO C++ Standard - Future Proposals
On 10 January 2016 at 19:18, Jonathan Coe <jb...@me.com> wrote:
> I have updated my implementation. As Ville said, the failing Gustafsson's
> dilemma test was a facet of my poor implementation.
> Tests now pass.

Looks promising.

> My implementation propagates constness. This is not necessary but to my mind
> makes sense as a deep_ptr is intended to extend the logical state of a
> object. What do others think?

I am gravitating towards not propagating const in this type, since
other smart pointers
don't do that, and propagate_const can be applied on top.

Thiago Macieira

unread,
Jan 10, 2016, 12:32:20 PM1/10/16
to std-pr...@isocpp.org
On Sunday 10 January 2016 17:44:04 Ville Voutilainen wrote:
> I already do that delegation. The "Gustafsson Dilemma" doesn't exist,
> there are multiple
> ways to avoid it: either do what shared_ptr does or use a covariant
> return in a type-erased
> implementation. The former is better.

Covariant returns will not work if the relationship between the two pointer
types crosses a virtual base or is not visible.

Example:

struct A { };
struct B { }
struct C: A, virtual B {};

shared_ptr<A> p1 = make_shared<C>();
shared_ptr<C> p2 = static_pointer_cast<C>(p1);
shared_ptr<B> p3 = static_pointer_cast<B>(p2);

There's no visible relationship between classes A and B, so shared_ptr<B>
cannot possibly store a pointer to A. If it stored a pointer to C, it would
require a dynamic_cast to cast from C to B.

The only reasonable answer is that a shared_ptr<B> has a pointer to B.

--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center
PGP/GPG: 0x6EF45358; fingerprint:
E067 918B B660 DBD1 105C 966C 33F5 F005 6EF4 5358

Ville Voutilainen

unread,
Jan 10, 2016, 12:36:46 PM1/10/16
to ISO C++ Standard - Future Proposals
On 10 January 2016 at 19:32, Thiago Macieira <thi...@macieira.org> wrote:
> On Sunday 10 January 2016 17:44:04 Ville Voutilainen wrote:
>> I already do that delegation. The "Gustafsson Dilemma" doesn't exist,
>> there are multiple
>> ways to avoid it: either do what shared_ptr does or use a covariant
>> return in a type-erased
>> implementation. The former is better.
>
> Covariant returns will not work if the relationship between the two pointer
> types crosses a virtual base or is not visible.
>
> Example:
>
> struct A { };
> struct B { }
> struct C: A, virtual B {};
>
> shared_ptr<A> p1 = make_shared<C>();
> shared_ptr<C> p2 = static_pointer_cast<C>(p1);
> shared_ptr<B> p3 = static_pointer_cast<B>(p2);
>
> There's no visible relationship between classes A and B, so shared_ptr<B>
> cannot possibly store a pointer to A. If it stored a pointer to C, it would
> require a dynamic_cast to cast from C to B.

I don't see anything alarming in any of that.

> The only reasonable answer is that a shared_ptr<B> has a pointer to B.

Well, it certainly looks like converting in a converting constructor
is in many ways
superior to using a covariant return type.

Jonathan Coe

unread,
Jan 10, 2016, 12:39:03 PM1/10/16
to std-pr...@isocpp.org
Agreed.

Ville Voutilainen

unread,
Jan 10, 2016, 12:43:50 PM1/10/16
to ISO C++ Standard - Future Proposals
On 10 January 2016 at 19:39, Jonathan Coe <jb...@me.com> wrote:
>> > My implementation propagates constness. This is not necessary but to my
>> > mind
>> > makes sense as a deep_ptr is intended to extend the logical state of a
>> > object. What do others think?
>>
>> I am gravitating towards not propagating const in this type, since
>> other smart pointers
>> don't do that, and propagate_const can be applied on top.
> Agreed.


Another thing: const propagation is easy to slap on top with propagate_const,
but if the type does it built-in and the user for whatever reason
doesn't want it,
bypassing the const propagation is.. ..interesting, to say the least, right?

Jonathan Coe

unread,
Jan 10, 2016, 1:09:19 PM1/10/16
to std-pr...@isocpp.org

Bengt Gustafsson

unread,
Jan 12, 2016, 7:27:14 AM1/12/16
to ISO C++ Standard - Future Proposals, jb...@me.com
Ok, so now we know that this can be done. Lets focus on the functionality.

I was wondering whether it is really logical that deep_ptr takes ownership of the original pointer and that there is a make_deep_ptr function. Every other copying step a copy of the source is made, so why not the first time? Continuing this line of reasoning I was wondering whether it wouldn't be appropriate to have an emplace<U>() method rather than make_deep_ptr function for the first step. Maybe this brings it too close to std::any?

In the long run I think we have to consider how many such similar constructs to have. I don't know exactly how std::any is supposed to work, but I suspect that it is a non-template class which can hold a value of any type and copies that value when itself is copied. Thus if it is initialized from a pointer it will only copy the pointer rather than the pointee (which makes cloning via base pointer a non-issue).

I'm suspecting that there are proposals for some kind of type_erased<T> class where T is a baseclass of whatever object it hosts. Without operator.() this may not be such an interesting proposal as it would have to work as a pointer (access via ->) anyway, but with operator.() it seems very nice. The question is what is the design space that a deep_ptr would fill given these other constructs? It seems that deep_prt is a type_erased without operator. but which acts more like a pointer when being initialized (assuming that type_erased is inited via emplace<U>()).

Ville Voutilainen

unread,
Jan 12, 2016, 8:19:01 AM1/12/16
to ISO C++ Standard - Future Proposals
On 12 January 2016 at 14:27, Bengt Gustafsson
<bengt.gu...@beamways.com> wrote:
> Ok, so now we know that this can be done. Lets focus on the functionality.
> I was wondering whether it is really logical that deep_ptr takes ownership
> of the original pointer and that there is a make_deep_ptr function. Every

That happens to be convenient, to wrap dynamically allocated resources so that
they get cloned on copy.

> other copying step a copy of the source is made, so why not the first time?

Why would it perform such a pointless extra copy?

> In the long run I think we have to consider how many such similar constructs
> to have. I don't know exactly how std::any is supposed to work, but I
> suspect that it is a non-template class which can hold a value of any type
> and copies that value when itself is copied. Thus if it is initialized from
> a pointer it will only copy the pointer rather than the pointee (which makes
> cloning via base pointer a non-issue).

It makes any cloning a non-issue, since it won't clone anything, and is thus
not suitable for the purpose cloned_ptr serves.

Patrice Roy

unread,
Jan 12, 2016, 10:40:07 PM1/12/16
to std-pr...@isocpp.org
In an «any» implementation, the copying can be hidden internally in such a way that non-cloneable types like int can be stored. For example, any can have an internal cloneable class from which derived classes are type-aware templates that know how to copy the stored value:

class any
{
   struct hook
   {
      virtual hook * clone() const = 0;
      virtual ~hook() = default;
   };
   template <class T>
      class datum : public hook
      {
          T value;
          datum * clone() const override { return new datum{ *this }; }
      protected:
         datum(const datum&) = default;
      public:
         datum(const T &value)  : value{ value }
            { }
         // ...
      };
   // ...
   hook *p;
public:
   template <class T>
      any(const T & value)
         : p { new datum<T>{ value } }
      {
      }
   // ...       :
};

The real design issue in an «any» type is retrieving the stored value (it can be done with RTTI but that's not acceptable in some use cases). The cloning is an internal issue that can be resolved without clone_ptr (although if clone_ptr simplifies the pattern at no cost, then why not?).



Bengt Gustafsson

unread,
Jan 13, 2016, 9:50:39 AM1/13/16
to ISO C++ Standard - Future Proposals
That happens to be convenient, to wrap dynamically allocated resources so that
they get cloned on copy.

> other copying step a copy of the source is made, so why not the first time?

Why would it perform such a pointless extra copy?

I think this is a matter of point of view. Maybe you have the data you want to transport via the deep_ptr in a by value way. Maybe you can emplace it directly into the deep_ptr. This aside I found a annoying use case which indicates the way we may need to go: downcasting. We have discussed upcasting a lot and the implications this has with multiple inheritance or covariant return types. But take a look at downcasting. As we can't downcast a smart pointer the usual way is to use get() to get the raw pointer, downcast it and give it to the receptor:

deep_ptr<Base> bp = make_deep<Sub>();
deep_ptr<Sub> sp = static_cast<Sub*>(bp.get());

 Problem #1: Noone copies the data.
 Problem #2: If bp had contained a further subclass SubSub of Sub whomever that tried to manually do the copy in the process would likely slice the object into a Sub. There is no real way of knowing what subclass it is if you just get the bp assumed to contain some sort of Sub.

One way of solving this would be to introduce a method:
 template<typename U> deep_ptr<U> deep_ptr<T>::cast<U>()

This method would perform the copying itself and return the copy. On the other hand, with my initial copying idea the classical idiom described above would work.

A more exotic possiblity that sprung to my mind when thinking about this would be to view static_cast and dynamic_cast as overloadable operators, so that casting of smart pointers (including deep_ptr) could be written as for plain pointers:

shared_ptr<Base> bp = new Base;
shared_ptr
<Sub> sp = static_cast<shared_ptr<Sub>>(bp);


// A shot at how to write the definition:


template<typename T> class shared_ptr {
   
template<typename U> operator static_cast<shared_ptr<U>>() { return shared_ptr<U>(--the control block-,
                                                                                     
static_cast<U*>(get())); }
};


This is of course a separate proposal but I just wanted to see what the reaction here might be. I didn't put must thought into how to actually write the declaration.

Another idea was prompted by Patrice's writing on std::any. Here also a way to specify cloning of incoming polymorphic data would be vital to avoid slicing when initiating an std::any from a polymorphic baseclass reference (I assume a copy is made here). We obviously don't want to have separate systems to specify cloning methods for std::any, std::deep_ptr and possibly std::type_erased_value. I see a few possibilities:

1. Allow virtual on copy constructors, similar to how it is allowed on destructors. (The copy constructor being the one that can get implemented automatically). While this is not really logical as the class of the new object is taken from the parameter (there is no 'this' yet!) it would be very easy to implement and very useful. The main problem is that it would do a new so some kind of allocator support will be wished for. Also in some cases you may want to slice the source, how can this be specified?

2. Define a std::clone() method* that can be overloaded by polymorphic base classes with cloning needs. any, deep_ptr and type_erased finds the implementation if there is one. The "namespace std:" so famous for std::swap may occur, I guess. The main drawback however is that there is nothing to prevent accidental non-overriding of the clone method in subclasses (unless a must_override specifier is invented of course).

3. Define some other magic way than virtual on the baseclass copy ctor to indicate clonability. A positional keyword after the class name maybe. Then std::clone is magic in that it can access a hidden vtbl entry to do cloning without slicing. To me this seems just like a more complicated way of achieving 1.


* The default implementation can check against polymorphic calls using typeid() in debug builds if desired:

template<typename T> T* std::clone(const T* src)
 
{
     
assert(typeid(*src) == typeid(T));  // Base case does not allow subclasses as they can't be cloned without slicing
     
return new T(*src);
 
}


Ville Voutilainen

unread,
Jan 13, 2016, 10:05:34 AM1/13/16
to ISO C++ Standard - Future Proposals
On 13 January 2016 at 16:50, Bengt Gustafsson
<bengt.gu...@beamways.com> wrote:
>> Why would it perform such a pointless extra copy?
> I think this is a matter of point of view. Maybe you have the data you want
> to transport via the deep_ptr in a by value way. Maybe you can emplace it
> directly into the deep_ptr. This aside I found a annoying use case which
> indicates the way we may need to go: downcasting. We have discussed
> upcasting a lot and the implications this has with multiple inheritance or
> covariant return types. But take a look at downcasting. As we can't downcast
> a smart pointer the usual way is to use get() to get the raw pointer,
> downcast it and give it to the receptor:
> deep_ptr<Base> bp = make_deep<Sub>();
> deep_ptr<Sub> sp = static_cast<Sub*>(bp.get());

Well, that's every bit as daft as using static_cast with a shared_ptr.
It won't work.
It needs a specialized cast that can do the right thing for the smart pointer.
None of that means that every adoption of a pointer should always
clone the incoming
value.

> One way of solving this would be to introduce a method:
> template<typename U> deep_ptr<U> deep_ptr<T>::cast<U>()
> This method would perform the copying itself and return the copy. On the

Yes, that seems like a very good approach.

> other hand, with my initial copying idea the classical idiom described above
> would work.

I don't find it a "classical idiom".

> A more exotic possiblity that sprung to my mind when thinking about this
> would be to view static_cast and dynamic_cast as overloadable operators, so
> that casting of smart pointers (including deep_ptr) could be written as for
> plain pointers:
>
> shared_ptr<Base> bp = new Base;
> shared_ptr<Sub> sp = static_cast<shared_ptr<Sub>>(bp);
>
>
> // A shot at how to write the definition:
>
>
> template<typename T> class shared_ptr {
> template<typename U> operator static_cast<shared_ptr<U>>() { return
> shared_ptr<U>(--the control block-,
>
> static_cast<U*>(get())); }
> };
>
>
> This is of course a separate proposal but I just wanted to see what the
> reaction here might be. I didn't put must thought into how to actually write
> the declaration.

If you want a generalized cast, that's attainable without having to
change static_cast
to be an operator.

> Another idea was prompted by Patrice's writing on std::any. Here also a way
> to specify cloning of incoming polymorphic data would be vital to avoid
> slicing when initiating an std::any from a polymorphic baseclass reference
> (I assume a copy is made here). We obviously don't want to have separate
> systems to specify cloning methods for std::any, std::deep_ptr and possibly
> std::type_erased_value. I see a few possibilities:
>
> 1. Allow virtual on copy constructors, similar to how it is allowed on
> destructors. (The copy constructor being the one that can get implemented

That continues to make no sense.

> automatically). While this is not really logical as the class of the new
> object is taken from the parameter (there is no 'this' yet!) it would be
> very easy to implement and very useful. The main problem is that it would do

Very easy to implement? How?

> 2. Define a std::clone() method* that can be overloaded by polymorphic base
> classes with cloning needs. any, deep_ptr and type_erased finds the

What purpose does it serve to define it in the standard? Which base class
would actually declare it?

> 3. Define some other magic way than virtual on the baseclass copy ctor to
> indicate clonability. A positional keyword after the class name maybe. Then
> std::clone is magic in that it can access a hidden vtbl entry to do cloning
> without slicing. To me this seems just like a more complicated way of
> achieving 1.

Well, "to indicate clonability", you either
1) make sure your class is valid for the default cloner (which means
CopyConstructible)
2) provide a custom cloner that does what you want

Why would we make wider changes to the language? Those cover a very large amount
of use cases in practice or even in imagination.

Jonathan Coe

unread,
Jan 13, 2016, 10:29:15 AM1/13/16
to std-pr...@isocpp.org
I have added support for static_pointer_cast, shared_pointer_cast and dynamic_pointer_cast to my reference implementation.

Jon

Bengt Gustafsson

unread,
Jan 23, 2016, 4:55:02 AM1/23/16
to ISO C++ Standard - Future Proposals
Jon, you made it perfectly clear why overloadable static_cast and dynamic_cast are desirable. You now introduced three different names for just one of these casts (I don't know why, really) but wouldn't it be much better to be able to overload the current names with functions for other data types (typically smart pointers). My earlier suggestion to do this in the class head was maybe not the wisest choice, a free function overload seems more logical in retrospect.

template<typename T, typename U> shared_ptr<T> static_cast(const shared_ptr<U>& src)
{
   
return shared_ptr<T>(static_cast<T*>(src.get()); // In reality shared_ptr would need to share the control block ptr.
}


While it is currently possible to static_cast a shared_ptr if you for some reason inherit a class from it, introducing the above would cause compile time error for those cases, so it is not that bad.

The basic principle for such a feature would work as operator,(): If no user defined cast function can be found the current, built in cast is used.

As for the built in clone functionality I maintain it is quite simple to implement, but maybe not as simple as I thought at the time. The issue is that when you create a subclass object it will likely be larger than the baseclass object you have the pointer/reference to. Also, to avoid breaking code that is deliberately slicing the incoming reference we need to differentiate the syntax from the case where slciing is desired:

Base* slice(Base* src)
{
   
return new Base(*src);
}



One way of doing this is to offer a 'virtual new' like this:

Base* clone(Base* src)
{
   
return virtual new Base(*src);
}

this means that operator new reaches into the vtbl of src to find a) the size of the malloc block to get, and b) the address of the copy ctor of the runtime class of src. While this works for regular new it is problematic for placement new as you supposedly conjure up a suitably sized malloc block before newing, but how to know its size? We could allow 'virtual sizeof' to handle this:

Base* placement_clone(Base* src)
{
    char* addr = new char[virtual sizeof(*src)];
    
return virtual new(addr) Base(*src);
}

Note that we still need virtual new to get the right copy ctor.

I see this as a workable solution, but I don't like that 'virtual new base' actually constructs some other class, which is non-intuitive. As alternatives there could be a std::clone() function with magic properties or type_info could be augmented with size() and copy_construct() methods:

Base* typeid_clone(Base* src)
{
    char* addr = new char[typeid(*src).size()];
    
return typeid(*src).copy_construct(addr, src); // copy_construct must return a Base* as it may be offset from addr*.
}


A design decision is whether the two vtbl entries pointing to the copy ctor and the size are to be added as soon as a class has a virtual method (as for the type_info pointer) or only if the base class copy ctor is declared virtual (as for the destructor). I would lean towards the automatic option despite its slight overhead even if not used.

Another possibility is to just wait for the reflection functionality to materialize, which seems to be a long wait. Also, AFAIK the reflection will only be at compile time which doesn't help this situation. 

*) copy_construct (or virtual new) must use the same information that dynamic_cast does to find out the offset from the actual type of src to Base after creating the copy. I haven't looked into if there could be some insurmountable problem with Base being a virtual base of *src's runtime type but I don't think it should be any worse than dynamic_cast.

Whatever language feature is at the root of this functionality I suspect that a std::clone() is the logical user visible API to present to programmers. to do a 'placement clone' you'd have to fall back on the language's basic functionality.

So in fact we are looking at three proposals here: deep_ptr as such, smart pointer casts and cloning. I don't think it would be wise to write them into one paper.

Nevin Liber

unread,
Jan 23, 2016, 5:21:44 AM1/23/16
to std-pr...@isocpp.org
On 23 January 2016 at 03:55, Bengt Gustafsson <bengt.gu...@beamways.com> wrote:
Jon, you made it perfectly clear why overloadable static_cast and dynamic_cast are desirable. You now introduced three different names for just one of these casts

What three names?  static_pointer_cast and dynamic_pointer_cast are already part of the shared_ptr interface, have been since its introduction in C++11, as well as for many years before that as part of Boost shared_ptr.

You really ought to look at the current smart pointers in the standard before attempting to propose a new one.
 
(I don't know why, really) but wouldn't it be much better to be able to overload the current names with functions for other data types (typically smart pointers).

Why invent a complicated language feature if we can not only get by w/o it, but the library feature is easily extended to other smart pointers not provided by the standard.

As for the built in clone functionality I maintain it is quite simple to implement, but maybe not as simple as I thought at the time.

If it is "quite simple", do you have an implementation we can try?
 
One way of doing this is to offer a 'virtual new' like this:

*sigh*

So in fact we are looking at three proposals here: deep_ptr as such, smart pointer casts and cloning. I don't think it would be wise to write them into one paper.

Jonathan can do the pure LEWG proposal of deep_ptr and *_pointer_casts (like for shared_ptr, they do go together) and leave the "quite simple" language changes to you.  How does that sound?
--
 Nevin ":-)" Liber  <mailto:ne...@cplusplusguy.com+1-847-691-1404

Jonathan Coe

unread,
Jan 24, 2016, 4:29:00 AM1/24/16
to std-pr...@isocpp.org
My plan is to keep deep_ptr a pure library proposal and to keep is as close in design to shared_ptr as possible.

Please note that my reference implementation currently lags behind the draft paper I'm working on - I'll be sure to post once I update the code.

Jon
 

--
 Nevin ":-)" Liber  <mailto:ne...@cplusplusguy.com+1-847-691-1404

--

Bengt Gustafsson

unread,
Jan 24, 2016, 9:45:51 AM1/24/16
to ISO C++ Standard - Future Proposals, ne...@cplusplusguy.com
well, I was confused by Jon's typo, he wrote shared_pointer_cast as one of his names, where I suppose it should have been const_pointer_cast. Also, he failed to mention that these were overloads of existing functions for shared pointers.

Nevertheless, I had never seen these, which still sort of points at the inconvenience of not being able to overload the more well known static_cast names. In the teaching situation it may be an uphill battle to get students to understand that to cast a pointer you use static_cast while to cast a smart pointer (which is actually a class) you use static_pointer_cast. this said, it is of course less motivated to take the trouble of writing a proposal in the light of established function names parallel to the built in operators.

You are *sighing* at the hard problems with cloning I have supposedly missed, and there may indeed be such problems, I have only been a C++ programmer for 22 years, what do I know. It would however be slightly more constructive to give a little hint at what the problems that are so hard to overcome might be. While you're at it you may care to describe in layman's words why letting typeid(value) returning a metaclass object with more complete functionality than the current type_info is such an impossibly ridiculous idea that a totally different RTTI system has to be invented in som distant future.

If you are not able to convince me that implementing cloning (maybe using type_info extensions) is very hard I may indeed take a stab at implementing it in Clang and writing a proposal.

Jonathan. I understand your urge to make this a pure library feature, this is always easiest. I do think that the deep_ptr poses some new issues regarding polymorphic cloning that makes it much more useful with this in the language. These issues don't appear with shared_ptr which doesn't do cloning, or with std::any() which does not have as polymorphic looking an API, although it is quite easy to happen to slice the object with an any too it happens immediately on the construction of the any object and can be detected using the typeid equality test.

Nevin Liber

unread,
Jan 25, 2016, 5:01:07 PM1/25/16
to Bengt Gustafsson, ISO C++ Standard - Future Proposals
On 24 January 2016 at 08:45, Bengt Gustafsson <bengt.gu...@beamways.com> wrote:
Also, he failed to mention that these were overloads of existing functions for shared pointers.

I don't think it is the job of this proposal to teach people about shared_ptr.  I most certainly expect the people in LEWG who would be evaluating this proposal to know enough about shared_ptr, unique_ptr and even observer_ptr to make an informed decision on whether or not to accept this proposal and what its interface should be to fit in with the rest of the standard library.

And it isn't like we haven't pointed you to shared_ptr numerous times in this discussion.
 
Nevertheless, I had never seen these, which still sort of points at the inconvenience of not being able to overload the more well known static_cast names. In the teaching situation it may be an uphill battle to get students to understand that to cast a pointer you use static_cast while to cast a smart pointer (which is actually a class) you use static_pointer_cast.

If you want this behavior, make a separate proposal.  Or make a library proposal for *_pointer_cast to work with raw pointers.
 
You are *sighing* at the hard problems with cloning I have supposedly missed, and there may indeed be such problems, I have only been a C++ programmer for 22 years, what do I know. It would however be slightly more constructive to give a little hint at what the problems that are so hard to overcome might be.

You keep telling us it is simple without proof.  For instance:

Many other languages support the eqivalent of a virtual copy ctor, and it would be simple to add to C++. 

If you think it is so simple, describe its behavior.  Does it work like regular virtual functions?  If so, how do you guarantee that base objects are constructed?  Can it be abstract?  Is there a compiler generated one, and if so, what does it look like?  Does it break anything just to put the word "virtual" in front of a copy constructor?

Again, if this is so simple, you should be able to answer these questions with ease.

Or we could take your "quite simple" virtual new.  Placement new is used to construct objects whose type and size are known at compile time.  virtual new makes that a run time decision.  That sounds like something which has far reaching consequences for the language, and not like something that is "quite simple".

Please describe the quite simple behavior of virtual new and the impact on people who currently use placement new and prove me wrong.
Reply all
Reply to author
Forward
0 new messages