How can this be implemented? Can you explain this briefly? What would the limitations be?
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.
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
--
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).
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.
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...
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.
};
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
Nevin ":-)" Liber <mailto:ne...@eviloverlord.com> +1-312-623-5420
--
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.
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.
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.
--
This may be of interest for the proposal: http://mnmlstc.github.io/core/memory.html
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.
reinterpret_cast<const typename deep_ptr<U>::inner *>(&u.buffer_)->copy(&buffer_);
void copy(void *buffer) const override {
new (buffer) inner_impl(new U(*u_));
}
const T *get() const {
return reinterpret_cast<const inner *>(&buffer_)->get();
}
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.
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.
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).
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.
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.
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 overrideactually returns. shared_ptr works fine with the case you think is problematic, becausethe offset is adjusted at run-time. I don't know whether Jonathan's implementation gets itright with its reinterpret_cast dance, but shared_ptr and my implementation do.
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().
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*.
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().
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 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.
--
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?
template<typename U> deep_ptr<U> deep_ptr<T>::cast<U>()
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())); }
};
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);
}
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.
}
Base* slice(Base* src)
{
return new Base(*src);
}
Base* clone(Base* src)
{
return virtual new Base(*src);
}
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).
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.
One way of doing this is to offer a 'virtual new' like this:
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.
--
--
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.
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.
Many other languages support the eqivalent of a virtual copy ctor, and it would be simple to add to C++.