Interest in a "Heterogenous Container" system?

Hello, After a discussion on a different thread, I came up with an implementation allowing containers to store and retrieve heterogenous instances, and provide correct deep cloning of those containers. A working motivational example is here http://tinyurl.com/larqyy. I have yet to do a comparison with container<boost::any>. This is just an initial proof-of-concept, and there remains a lot of work to do if it were to be a complete proposal. Before I did that work (especially since I have another proposal in the wings), I wanted to ask if there would be any interest in a completed system? The system is based on a modified ptr_container branch (also in the sandbox - I've contacted Thorsten about this), and and allocator adaptor system to deal with cloning. It allows one to place object pointers into a container using syntax such as: /// a vector of heterogenous objects template <class Alloc = std::allocator<char>, class Base = common_base > struct vector; heterogenous::vector<> v; v.push_back<T0>(); // add a default-constructed object of type T0 v.push_back<T1>(a0,a1,...,an); // add an object of type T1, passig a0..an as ctor arguments T1 &obj = v.ref_at<T1>(1); // get a reference of type T1& to the object at index 1 or throw bad_cast heterogenous::vector<> v2 = v; // copy the container; concrete types are preserved assert(v2.is_type_at<T1>(1)); There are many outstanding issues; it may not fly in the end, it may not work for all container types and semantics, but I would like some early opinion on whether it is even worth my pursuing further. One initial issue that may break any interest is that instances put into the container must be of a type T that derives from heterogenous::base<T>. Regards, Christian.

On Sun, Jun 28, 2009 at 5:10 PM, Christian Schladetsch < christian.schladetsch@gmail.com> wrote:
One initial issue that may break any interest is that instances put into the container must be of a type T that derives from heterogenous::base<T>.
So, how is this different from using a ptr_container and dynamic_cast?

On Mon, Jun 29, 2009 at 11:36 AM, Ross Levine <ross.levine@uky.edu> wrote:
On Sun, Jun 28, 2009 at 5:10 PM, Christian Schladetsch < christian.schladetsch@gmail.com> wrote:
One initial issue that may break any interest is that instances put into the container must be of a type T that derives from heterogenous::base<T>.
So, how is this different from using a ptr_container and dynamic_cast?
The instances in a heterogenous::container are added to the container using the correct allocator, and are duplicated using the correct (and same) allocator. struct base {}; struct derived : base {}; typedef ptr_vector<base, heap_clone_alocator, custom_allocator<base> > vec; vec v0; v0.push_back(new T(args)); // doesnt use the correct allocator vec v1 = v0; // clones are not created using the containers allocator as opposed to: typedef heterogenous::vector<custom_allocator> vec; vec v0; v0.push_back<T>(args); // uses correct allocator vec v1 = v0; // clones created using correct allocator, no custom clone_allocator required Of course, I have missed here that for this to work, T must derive from base<T>. There is a good case to be made for using std::vector<any> to do a similar thing. It's much simpler for a start. However, boost::any uses the heap and doesn't know about or use allocators. I've made modifications to boost::any, giving it an allocator type. See http://tinyurl.com/mxh9gm. This allows, for instance: typedef any<custom_allocator<char> > any_type; typedef std::vector<any_type, custom_allocator<any_type> > vec; vec v; v.push_back(T0(42)); v.push_back(T1("foo")); vec v2 = v; And this works - and T0 and T1 don't have to derive from anything. However, it creates the instances to add on the stack, then copies them into the container. v.push_back<type>(..args..) doesn't make a copy - it is constructed in-place using the correct storage provided by the allocator. This may be a pretty weak argument against a 'heterogenous container' based on any rather than ptr_container. Perhaps just making any<> take an allocator type argument would suffice for this? Regards, Christian.

"Christian Schladetsch" <christian.schladetsch@gmail.com> wrote in message news:6442c4ae0906281655j6b6de8cdi93d8efda4f440947@mail.gmail.com...
vec v0; v0.push_back(new T(args)); // doesnt use the correct allocator
I like the idea of in-place construction -- no copy constructor being called.

How is this different from a container of <boost/variant.hpp> Robert Ramey Ross Levine wrote:
On Sun, Jun 28, 2009 at 5:10 PM, Christian Schladetsch < christian.schladetsch@gmail.com> wrote:
One initial issue that may break any interest is that instances put into the container must be of a type T that derives from heterogenous::base<T>.
So, how is this different from using a ptr_container and dynamic_cast? _______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost

2009/6/28 Christian Schladetsch <christian.schladetsch@gmail.com>:
On Mon, Jun 29, 2009 at 12:58 PM, Robert Ramey <ramey@rrsd.com> wrote:
How is this different from a container of <boost/variant.hpp>
boost::variant doesnt support custom allocators. it uses naked new and delete.
A quick test with geordi suggests that it uses placement new, not naked new: <> geordi, { unique_ptr<tracked::B> p(new tracked::B); } <geordi> new(B) B0* B0~ delete(B0) <> geordi, { boost::variant<tracked::B> v; v = tracked::B(); } <geordi> B0* B1* B0=B1 B1~ B0~

On Mon, Jun 29, 2009 at 12:13 PM, Scott McMurray <me22.ca+boost@gmail.com>wrote:
2009/6/28 Christian Schladetsch <christian.schladetsch@gmail.com>:
On Mon, Jun 29, 2009 at 12:58 PM, Robert Ramey <ramey@rrsd.com> wrote:
How is this different from a container of <boost/variant.hpp>
boost::variant doesnt support custom allocators. it uses naked new and delete.
A quick test with geordi suggests that it uses placement new, not naked new:
That's true quite often, but it does use naked new and delete to make backups in backup_assign_impl. (that's where I saw the delete and new in the first place). This may or may not be a problem. However, how would you use a std::vector<variant<...> >? struct T0; struct T1; ... struct Tn; typedef std::vector<boost::variant<T0, T1, ... , Tn>, custom_allocator<boost::variant<T0, T1, ... , Tn> > > vec; Does that make much sense? You have to know all the possible types that could go into the container at the point of declaration. This contends with the idea of a general heterogenous collection that can be passsed around and used. Plus the syntax for using such a beast might be awkward. Has anyone tried this in practise? I'll have a quick look... Cheers, Christian.

Christian Schladetsch wrote:
boost::variant doesnt support custom allocators. it uses naked new and delete.
variant stores objects within itself. If you declare a variant on the stack, your object lies on the stack. The temporary heap backup is only there in operator= and only when you don't have a nothrow move or nothrow default constructor on one of your types. And it's only used for a few instructions unless you throw anyway, so who cares about what allocator it uses. Alternatively, it could also store the backup in the object itself to avoid heap allocations, but that would double its size.

Hi Mathias, On Mon, Jun 29, 2009 at 11:51 PM, Mathias Gaunard < mathias.gaunard@ens-lyon.org> wrote:
Christian Schladetsch wrote:
boost::variant doesnt support custom allocators. it uses naked new and
delete.
variant stores objects within itself. If you declare a variant on the stack, your object lies on the stack.
The temporary heap backup is only there in operator= and only when you don't have a nothrow move or nothrow default constructor on one of your types. And it's only used for a few instructions unless you throw anyway, so who cares about what allocator it uses.
The issue of which allocator to use is quite important to some people, especially in embedded and/or high-performance situations. Sliding in a heap allocation somewhere in a container which has an allocator is a big no-no. ptr_container does it as well when making temporary arrays and for clones. In the case of variant<> using the heap to make a backup, it may not seem like such a big deal to some. But allocating and deallocating on the heap, ignoring the containers allocator, for every assignment [1], is a deal-breaker for others. In any case (no pun intended), I've compared heterogenous::vector<> with vector<any> and vector<variant> in http://tinyurl.com/larqyy. The differences are: * I had to modify any<> to add an allocator. See http://tinyurl.com/mxh9gm. * vector<any<alloc<char> >, alloc<any<alloc<char> > > > is effectively typeless. This means that anyone could add anything to such a vector, and as such I think this is too lenient and error prone. Adding an object to a vector<any> requires a copy. map<any, any> can't be implemented. * vector<variant<T0, .. Tn>, alloc<variant<T0,...Tn> > requires all types that could go into the container to be known at the point of declaration, which is contrary to the idea of having a generic heterogenous container. Adding an object to a vector<variant> also makes a copy and possibly an allocation that does not use the containers allocator [1]. map<variant<..>, variant<...> > could make sense but it would be a lot of work and difficult to maintain, as a variant<> with N types needs N*N comparison functions. * heterogenous::vector<> requires that a type T added to the container must derive at some point from heterogenous::common_base. This excludes adding builtin types, and means that you either have to change your existing class definitions to put them into such a container, or use a supplied heterogenous::adaptor<T> system. Allowing the addition of builtin types, and types not derived from common_base is certainly possible. However, I don't think that would be a benefit. In that case, you may's well use vector<any<>
. Adding an object to a heterogenous::container does not require a copy, uses the correct allocator, and copies are also made using the correct allocator. heterogenous::map<> can make sense with a single comparison operator and a common custom base.
At this point, I'll go away and flesh out the idea more. Thanks to those whom pointed out any and variant, but I think that vector<any> is too lenient and vector<variant> is too restrictive, and both require copies to insert. I don't see how associative containers can work with these, but I can do it with heterogenous::map as in typedef heterogenous:::map<my_less, my_base, my_alloc> map; map.key<K0>(...).value<V0>(...) ... .key<Kn>(...).value<Vn>(...) ; Cheers, Christian [1] for every assignment when using a variant<T0..Tn> where any of Tn does not have a nothrow move or nothrow default ctor.

On Mon, Jun 29, 2009 at 6:12 PM, Christian Schladetsch < christian.schladetsch@gmail.com> wrote:
The issue of which allocator to use is quite important to some people, especially in embedded and/or high-performance situations. Sliding in a heap allocation somewhere in a container which has an allocator is a big no-no. ptr_container does it as well when making temporary arrays and for clones.
High-performance/embedded situations are probably not going to need heterogeneous storage. Moreover, not only does deriving from common_base suddenly make a class polymorphic, which adds a vptr, and thus extra size, to every class instance, but your code uses dynamic_cast which requires RTTI, which is typically disabled in these environments to reduce overhead.

On Tue, Jun 30, 2009 at 10:40 AM, Ross Levine <ross.levine@uky.edu> wrote:
On Mon, Jun 29, 2009 at 6:12 PM, Christian Schladetsch < christian.schladetsch@gmail.com> wrote:
The issue of which allocator to use is quite important to some people, especially in embedded and/or high-performance situations. Sliding in a heap allocation somewhere in a container which has an allocator is a big no-no. ptr_container does it as well when making temporary arrays and for clones.
High-performance/embedded situations are probably not going to need heterogeneous storage. Moreover, not only does deriving from common_base suddenly make a class polymorphic, which adds a vptr, and thus extra size, to every class instance, but your code uses dynamic_cast which requires RTTI, which is typically disabled in these environments to reduce overhead.
I agree, although it is more common these days for folks to be using RTTI than it once was; just as it is more common now for them to be using virtual methods at all, over a few years ago. A similar argument was made against moving to C from ASM, and then against C++ over C, then against using STL at all in C++, and now against using boost with C++. But eventually, the benefits outweigh the costs. This usually happens when for example people find that they are making their own implemenation of RTTI just to avoid using the compiler-generated RTTI. People once eschewed C++ vtbls at all, but then over time found that their C-code closely resembled "manual C++", by making structures of function pointers, populating them on construction, and passing them around as the first arguments to their C functions. That said, I agree with you entirely. There are still many cases where RTTI is not welcome. But in these cases, you usually also disable exceptions, which IIUC means you can't use much of boost anyway. An alternative that I considered was to introduce a traits-based type system. I have one of these hanging around. It maps a C++ type to a static type-number and could be used instead of RTTI, when used with a common_base that stores that type-number. This seemed to me to be too much to swallow, so I stuck with RTTI. This may all render the entire idea useless, and that would be fine as well. I am not emotionally attached to it; I just saw a way to make it work and wanted to see what people thought. Even so, I am sure the high-performance guys would like to have heterogenous containers that clone correctly using a common allocator. That concept is generally needed, and either people will roll their own type-systems or hack around it. Regards, Christian.

Christian Schladetsch wrote:
* vector<variant<T0, .. Tn>, alloc<variant<T0,...Tn> > requires all types that could go into the container to be known at the point of declaration, which is contrary to the idea of having a generic heterogenous container. Adding an object to a vector<variant> also makes a copy and possibly an allocation that does not use the containers allocator [1]. map<variant<..>, variant<...> > could make sense but it would be a lot of work and difficult to maintain, as a variant<> with N types needs N*N comparison functions.
* heterogenous::vector<> requires that a type T added to the container must derive at some point from heterogenous::common_base. This excludes adding builtin types, and means that you either have to change your existing class definitions to put them into such a container, or use a supplied heterogenous::adaptor<T> system. Allowing the addition of builtin types, and types not derived from common_base is certainly possible. However, I don't think that would be a benefit. In that case, you may's well use vector<any<>
Deriving from a common ancestor base class is contrary to generic programming. Knowing all your types ahead of time (at compile time) is not unreasonable, it is the common case for generic programming. Saying it is contrary to your idea is like saying it is different from what you implemented. That isn't exactly a strong argument for why people should use heterogenous::vector instead of vector<variant<> >. I can tell you people will not be happy to derive from your common base class. This kind of design relegates it to being useful only to new classes that people write, and not to legacy classes that people use. That hugely narrows the scope of where it can be useful. It may be pretty particular to what you want to do and not generally useful in a context where people aren't so picky about which allocator they use and all types in an application tend to use the same one. Regards, Luke

On Tue, Jun 30, 2009 at 11:14 AM, Simonson, Lucanus J < lucanus.j.simonson@intel.com> wrote:
[snip] Deriving from a common ancestor base class is contrary to generic programming. Knowing all your types ahead of time (at compile time) is not unreasonable, it is the common case for generic programming.
Sure, but as much as we'd perhaps like to forget about it, it is true that C++ supports OO style as well as generic programming, especially in GUIs and other object hierarchies. In these cases, you either cannot know or don't want to know all the types that may be used ahead of time. This is the basic premise of loose coupling.
Saying it is contrary to your idea is like saying it is different from what you implemented. That isn't exactly a strong argument for why people should use heterogenous::vector instead of vector<variant<> >.
What I implemented came from discussions with Thorsten about how to deal with cloning in ptr_containers. To solve that, I ended up with a system that I saw may be of general interest and could possibly stand orthogonally to ptr_container. Using variant<> instead is certainly possible, but you could then also argue that ptr_container is pointless over a vector<variant>. But the truth is that a ptr_container is much more flexible than a vector<variant>. Given that heterogenous is based largely on ptr_container, the same is true there. Even so, I can't see how associative containers can work with variant<>, and it also doesn't provide a means to have a custom clone operation.
I can tell you people will not be happy to derive from your common base class.
I understand and empathise with that position. That is why, in my initial email, I stated: "One initial issue that may break any interest is that instances put into the container must be of a type T that derives from heterogenous::base<T>." However, I do not see any possible way to provide correct cloning of types, given just a base pointer, without having this constraint. That said, the system allows you to your own base, rather than heterogenous::common_base. template <class Derived, class Base = common_base> struct base But even here, currently common_base is needed at the bottom somewhere. Perhaps this too could be conceptualised, rather than using a fixed abstract base class supplied by the library.
This kind of design relegates it to being useful only to new classes that people write, and not to legacy classes that people use. That hugely narrows the scope of where it can be useful.
Christian> This excludes adding builtin types, and means that you either have to change your existing class definitions to put them into such a container, or use a supplied heterogenous::adaptor<T> system. The idea of heterogenous::adaptor<T> is to make a 'heterogenous type' from an existing type T which you cannot modify.
It may be pretty particular to what you want to do and not generally useful in a context where people aren't so picky about which allocator they use and all types in an application tend to use the same one.
Perhaps you are right and this is too specific. Even so, isn't there a general need for a heterogenous containers (both sequence- and associative-types) that have proper value semantics, can be cloned correctly, use a single allocator, and uses emplace construction for object insertion? There are some interesting properties of real heterogenous containers that I think you may find surprising.
Regards, Luke
Cheers, Christian

[It looks like I forgot to send this message.] Christian Schladetsch wrote:
On Tue, Jun 30, 2009 at 11:14 AM, Simonson, Lucanus J < lucanus.j.simonson@intel.com> wrote:
[snip] Deriving from a common ancestor base class is contrary to generic programming. Knowing all your types ahead of time (at compile time) is not unreasonable, it is the common case for generic programming.
Sure, but as much as we'd perhaps like to forget about it, it is true that C++ supports OO style as well as generic programming, especially in GUIs and other object hierarchies. In these cases, you either cannot know or don't want to know all the types that may be used ahead of time. This is the basic premise of loose coupling.
ptr_containers and the heterogeneous containers should account for polymorphic type use cases. Somewhere in that space, however, should be support for cases of homogenous containers of pointers to a single type such that the purpose of the container, aside from holding the pointers, is to assume ownership of them and delete them when the container goes out of scope (unless told to avoid taking ownership). The latter use case makes tracking the lifetime of allocated memory easier. Yes, containers of shared_ptrs would work, but they add overhead that isn't always wanted. Yes, it is possible to leave pointers to former elements dangling when the container is destroyed, but C++ is replete with such opportunities to cause problems. Sometimes, performance wins. The heterogeneous containers, being implemented as class templates, need only be defined in terms of operations supported by its parameterizing types. Require that the type T be a common base of all objects in the container and that it provide specific polymorphic operations and you have all that's needed. There's no need to impose that by requiring derivation from any particular base class, just whatever is sensible to the user. If that can be captured via TMP to support types that aren't related with a default that assumes types that are, all the better. _____ Rob Stewart robert.stewart@sig.com Software Engineer, Core Software using std::disclaimer; Susquehanna International Group, LLP http://www.sig.com IMPORTANT: The information contained in this email and/or its attachments is confidential. If you are not the intended recipient, please notify the sender immediately by reply and immediately delete this message and all its attachments. Any review, use, reproduction, disclosure or dissemination of this message or any attachment by an unintended recipient is strictly prohibited. Neither this message nor any attachment is intended as or should be construed as an offer, solicitation or recommendation to buy or sell any security or other financial instrument. Neither the sender, his or her employer nor any of their respective affiliates makes any warranties as to the completeness or accuracy of any of the information contained herein or that this message or any of its attachments is free of viruses.

Stewart, Robert wrote:
Yes, containers of shared_ptrs would work, but they add overhead that isn't always wanted.
Then use containers of unique_ptr, like you should (shared_ptr is meant for shared ownership or making it easy to fix broken code), or use the boost pointer containers if you can't (which move semantics mostly render obsolete). You could alternatively use boost.any, adobe.poly, clone_ptr, or whatever is relevant.
The heterogeneous containers, being implemented as class templates, need only be defined in terms of operations supported by its parameterizing types.
This merely looks like a container of Adobe.Poly. As far as I can see, the only advantage of heterogeneous containers is when dealing with node-based containers. A heterogeneous node-based container would have the opportunity to store the object directly into the node, while a container of boost.any or anything like that would only have a pointer to the object in the node. I believe however a better approach to benefit from that would be to define a dynamically-sized object protocol and have containers extended to support it. (obviously, data structures that rely on the fact the size of all instances of the same type is the same couldn't do that)

Mathias Gaunard wrote:
Stewart, Robert wrote:
Yes, containers of shared_ptrs would work, but they add overhead that isn't always wanted.
Then use containers of unique_ptr, like you should (shared_ptr
"like you should" is a rather strong statement.
is meant for shared ownership or making it easy to fix broken code), or use the boost pointer containers if you can't (which move semantics mostly render obsolete). You could alternatively use boost.any, adobe.poly, clone_ptr, or whatever is relevant.
All of those schemes mean that the container interface is in terms of those wrapper types rather than raw pointers. They will work, but they complicate the code using the elements. I'm not suggesting that is onerous, but the interface isn't as simple.
The heterogeneous containers, being implemented as class templates, need only be defined in terms of operations supported by its parameterizing types.
This merely looks like a container of Adobe.Poly.
I've never used anything from the Adobe library. You may be right, but why shouldn't Boost provide a clean solution for itself? _____ Rob Stewart robert.stewart@sig.com Software Engineer, Core Software using std::disclaimer; Susquehanna International Group, LLP http://www.sig.com IMPORTANT: The information contained in this email and/or its attachments is confidential. If you are not the intended recipient, please notify the sender immediately by reply and immediately delete this message and all its attachments. Any review, use, reproduction, disclosure or dissemination of this message or any attachment by an unintended recipient is strictly prohibited. Neither this message nor any attachment is intended as or should be construed as an offer, solicitation or recommendation to buy or sell any security or other financial instrument. Neither the sender, his or her employer nor any of their respective affiliates makes any warranties as to the completeness or accuracy of any of the information contained herein or that this message or any of its attachments is free of viruses.

Stewart, Robert wrote
Yes, containers of shared_ptrs would work, but they add overhead that isn't always wanted. Then use containers of unique_ptr, like you should (shared_ptr
"like you should" is a rather strong statement.
If you want a container of pointers with the container owning the pointees, in order to use inclusion polymorphism for example, then a container of unique_ptr is the standard solution, while a container of shared_ptr is not only overkill since it adds a lot of useless overhead, it also gives the wrong semantics. A container of clone_ptr is a more elegant solution if you can't use move semantics, since it actually has the right ones. You may want to implement it using COW however since non move-aware containers can do a lot of spurious copies.
All of those schemes mean that the container interface is in terms of those wrapper types rather than raw pointers. They will work, but they complicate the code using the elements. I'm not suggesting that is onerous, but the interface isn't as simple.
How are smart pointers, smart references or smart objects (whatever you want to call them) any more difficult to use than pointers or objects?
The heterogeneous containers, being implemented as class templates, need only be defined in terms of operations supported by its parameterizing types. This merely looks like a container of Adobe.Poly.
I've never used anything from the Adobe library. You may be right, but why shouldn't Boost provide a clean solution for itself?
Adobe.Poly is a generic type erasure tool, that can implement any type erasure for a given interface given as a parameter. boost::function, boost::any, and a few others from boost are simply special cases of Adobe.Poly (function with a callable interface, any with an empty interface). Boost could have something similar to Adobe.Poly (and I believe it eventually will -- the real issue is how to specify the interface), but this should be completely separate from the very idea of a container, since coupling the two has less advantages than making them separate and is actually harder to design, write and maintain.

Mathias Gaunard wrote:
Stewart, Robert wrote
Yes, containers of shared_ptrs would work, but they add overhead that isn't always wanted. Then use containers of unique_ptr, like you should (shared_ptr
"like you should" is a rather strong statement.
If you want a container of pointers with the container owning the pointees, in order to use inclusion polymorphism for example, then a container of unique_ptr is the standard solution, while a container of shared_ptr is not only overkill since it adds a lot of useless overhead, it also gives the wrong semantics.
A container of clone_ptr is a more elegant solution if you can't use move semantics, since it actually has the right ones. You may want to implement it using COW however since non move-aware containers can do a lot of spurious copies.
It seems you just made my point for me. There are numerous approaches to take, each with tradeoffs.
All of those schemes mean that the container interface is in terms of those wrapper types rather than raw pointers. They will work, but they complicate the code using the elements. I'm not suggesting that is onerous, but the interface isn't as simple.
How are smart pointers, smart references or smart objects (whatever you want to call them) any more difficult to use than pointers or objects?
The client code must be written in terms of wrapper_type<T> or container::value_type rather than T * or it must ask the wrapper for its raw pointer, an extra step. The point I'm making is that there are times when a container that does nothing more than own pointers is useful. Other code can use the pointers at will until the container goes out of scope. There's no need to wrap the pointers in anything in that case. Maybe that's too uncommon to warrant support in this context, but that's the use case I was proposing. _____ Rob Stewart robert.stewart@sig.com Software Engineer, Core Software using std::disclaimer; Susquehanna International Group, LLP http://www.sig.com IMPORTANT: The information contained in this email and/or its attachments is confidential. If you are not the intended recipient, please notify the sender immediately by reply and immediately delete this message and all its attachments. Any review, use, reproduction, disclosure or dissemination of this message or any attachment by an unintended recipient is strictly prohibited. Neither this message nor any attachment is intended as or should be construed as an offer, solicitation or recommendation to buy or sell any security or other financial instrument. Neither the sender, his or her employer nor any of their respective affiliates makes any warranties as to the completeness or accuracy of any of the information contained herein or that this message or any of its attachments is free of viruses.

-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1 On Friday 31 July 2009, Mathias Gaunard wrote:
A container of clone_ptr is a more elegant solution if you can't use move semantics, since it actually has the right ones. You may want to implement it using COW however since non move-aware containers can do a lot of spurious copies.
By the way, there is a cloning pointer included in the generic_ptr library I've been working on recently: https://svn.boost.org/trac/boost/browser/sandbox/fmhess/boost/generic_ptr Still no docs, but at least I'm starting to add some tests. It should also be possible (haven't actually tried it yet) to combine the cloning pointer it with a shared pointer, like generic_ptr::cloning<generic_ptr::shared<T*> > to support a combination of deep and shallow copy handles, although you'd have to pass the cloning a null deleter to prevent it from double-deleting the owned object. -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.9 (GNU/Linux) iEYEARECAAYFAkpzSW0ACgkQ5vihyNWuA4U8rgCdHtMpaV/kEl9Z58iVwrw+c42y hwUAoJwb40mJHfAobIUV5MD/nARe8DWA =OF/7 -----END PGP SIGNATURE-----

The motivation for my original proposal was to provide a framework for heterogenous containers, and came from discussions about how cloning works in ptr_container. I wanted a way to make the cloning operation for ptr_container to use the same allocator that the container uses (and indeed, have the container use the correct allocator at all!). This ended up being quite a large task, and spawned a whole separate 'Cloneable' library at http://tinyurl.com/ndy5n8. A motivational example of Cloneable with Heterogenous containers is something like: struct B { int n; B(int x = 0) : n(x) { } bool operator<(B q) { return n < q.n; } }; // provide a common base struct A : cloneable::base<A, B> { A(int n = 1) : B(n) { } }; // derived1 struct C : cloneable::base<C, B> { C(int n = 2) : B(n) { } }; // derived2 void test() { heterogenous::list<B, my_allocator<B> > list; // parameterised over the base, give an allocator list.push_back<A>(12); // emplace list.push_back<C>(-4); // emplace heterogenous::list<B> list2 = list; // deep-copy, using rebound my_allocator<T> } You simply have to use emplace semantics for these containers; otherwise you aren't respecting the allocator. Similarly for the clone operation: it must use the same allocator as the original container to make the clones, otherwise there is no point in even providing a parameterised allocator type. Associative containers get a little more interesting: void assoc() { typedef heterogenous::set<B> Set; Set set; set.insert<A>(1); set.insert<C>(2); Set::iterator iter = set.find<A>(1); // find an instance of type A with value 1 iter = set.find<B>(2); // find any type that has a `value` of 2 } And for maps, my current design is split between a mapping of value-type to pointer-type, as well as an "isomorphic" mapping of pointer-to-pointer: void maps() { heterogenous::map<string, B> map; // map of value to pointer map.key("foo").value<A>(12); map.key("bar").value<C>(6); heterogenous:iso_map<B> iso; // map of pointer to pointer iso.key<A>(1).value<C>(2); assert(iso.find<B>(1) != iso.end()); } The key thing is that all such containers provide value semantics for comparison and copying. Cloning these containers does a deep copy using the same allocator as the parent container. It also allows for objects that do not have default constructors, and provides a mechanism to provide custom overrides of the clone operation. So that sums up what I was trying to do originally. It currently has problems with multiple inheritance: It seems that the only way to write such a system that works with MI is to give over the type information to Cloneable entirely: struct Base { }; struct Derived1 : cloneable::base<Derived1, Base> { }; struct Derived2 : cloneable::base<Derived1, Derived2, Base> { }; // inheritance struct Derived3 : cloneable::base<Derived1, Derived2, Derived3, Base> { }; // inheritance (?) To avoid having to derive from Base using virtual inheritance, it seems that having to provide the base list to the system is unavoidable. Going down this track is to create a reflected type-system adjacent to the C++ type system, with all the compexity of casts and so on moved to compile-time. For example, to deal with Derived3 above you'll need to provide a means to virtually inherit: struct Derived2 : clone::base<clone::virtual_derive<Derived1>, Derived2, Base> { }; // inheritance struct Derived3 : clone::base<clone::virtual_derive<Derived1>, Derived2, Derived3, Base> { }; // inheritance (?) An alternative is to remove cloneable::base<..> entirely, but that means much more housekeeping on the behalf of the client to the point where the system is of little practical benefit. Or of course, just don't support derivation at all and leave it up to the user. Anyway, all of this is to say that the problem is quite deep and I think I'll be going back to the drawing board on this. I'll also be looking hard at the Adobe library for inspiration. I'd appreciate any comments; at the moment I think the work I've done so far on this is promising but it may need a complete re-think and perhaps re-work to support multiple inheritance correctly. Or, I could wrap it all up, strip out the fledgling support for MI, and run with that? That isn't very attractive. It seems that there is some low-hanging fruit here, in terms of good support for OO-style programming with containers. I am still finding the balance and the best questions to ask. Comments very welcome. Cheers, Christian. On Sat, Aug 1, 2009 at 7:43 AM, Frank Mori Hess <frank.hess@nist.gov> wrote:
-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1
On Friday 31 July 2009, Mathias Gaunard wrote:
A container of clone_ptr is a more elegant solution if you can't use move semantics, since it actually has the right ones. You may want to implement it using COW however since non move-aware containers can do a lot of spurious copies.
By the way, there is a cloning pointer included in the generic_ptr library I've been working on recently:
https://svn.boost.org/trac/boost/browser/sandbox/fmhess/boost/generic_ptr
Still no docs, but at least I'm starting to add some tests. It should also be possible (haven't actually tried it yet) to combine the cloning pointer it with a shared pointer, like
generic_ptr::cloning<generic_ptr::shared<T*> >
to support a combination of deep and shallow copy handles, although you'd have to pass the cloning a null deleter to prevent it from double-deleting the owned object.
-----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.9 (GNU/Linux)
iEYEARECAAYFAkpzSW0ACgkQ5vihyNWuA4U8rgCdHtMpaV/kEl9Z58iVwrw+c42y hwUAoJwb40mJHfAobIUV5MD/nARe8DWA =OF/7 -----END PGP SIGNATURE----- _______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost

Christian Schladetsch wrote:
The motivation for my original proposal was to provide a framework for heterogenous containers, and came from discussions about how cloning works in ptr_container.
I wanted a way to make the cloning operation for ptr_container to use the same allocator that the container uses (and indeed, have the container use the correct allocator at all!).
Standard allocators are inherently incompatible with the very idea of cloneable objects, since the type information is supposed to be lost. The only sensible thing you can do (IMHO) is use a standard allocator of char or use an allocator interface similar to that of malloc or operator new. I personally never understood why they tied the type into the allocator. Having just the size and alignment would be so much better.
You simply have to use emplace semantics for these containers; otherwise you aren't respecting the allocator
For when you copy the container later, do you maybe add a pointer overhead per object on your heterogeneous list to reference a rightly rebound function for cloning with the given container allocator? Not something very nice, since it could be avoided.

On Saturday 01 August 2009, Mathias Gaunard wrote:
I personally never understood why they tied the type into the allocator. Having just the size and alignment would be so much better.
It seems to me any benefits of such a scheme would be non-portable, since the sizes and alignments of two types may be the same with one compiler (setting) and different with others.

On Mon, Aug 3, 2009 at 5:44 AM, Frank Mori Hess <fmhess@speakeasy.net>wrote:
On Saturday 01 August 2009, Mathias Gaunard wrote:
I personally never understood why they tied the type into the allocator. Having just the size and alignment would be so much better.
It seems to me any benefits of such a scheme would be non-portable, since the sizes and alignments of two types may be the same with one compiler (setting) and different with others.
I don't see a problem. As long as the objects are created with correct alignment for a given compiler (setting), what that alignment is, is not important.

AMDG Christian Schladetsch wrote:
On Mon, Aug 3, 2009 at 5:44 AM, Frank Mori Hess <fmhess@speakeasy.net>wrote:
On Saturday 01 August 2009, Mathias Gaunard wrote:
I personally never understood why they tied the type into the allocator. Having just the size and alignment would be so much better.
It seems to me any benefits of such a scheme would be non-portable, since the sizes and alignments of two types may be the same with one compiler (setting) and different with others.
I don't see a problem. As long as the objects are created with correct alignment for a given compiler (setting), what that alignment is, is not important.
Unfortunately, this requires the ability to find the alignment of an arbitrary type. boost::alignment_of only guarantees that it will return a positive multiple of the type's alignment. In Christ, Steven Watanabe

-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1 On Sunday 02 August 2009, Christian Schladetsch wrote:
On Mon, Aug 3, 2009 at 5:44 AM, Frank Mori Hess <fmhess@speakeasy.net>wrote:
On Saturday 01 August 2009, Mathias Gaunard wrote:
I personally never understood why they tied the type into the allocator. Having just the size and alignment would be so much better.
It seems to me any benefits of such a scheme would be non-portable, since the sizes and alignments of two types may be the same with one compiler (setting) and different with others.
I don't see a problem. As long as the objects are created with correct alignment for a given compiler (setting), what that alignment is, is not important.
I wasn't following the thread too closely, but I assumed the benefit of using size+alignment instead of type would be that you could reuse the same allocator with different types (as long as they had the same size and alignment). But since a type's size+alignment varies across platforms, you wouldn't be able to portably reuse such an allocator with different types (hence no portable benefit). -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.9 (GNU/Linux) iEYEARECAAYFAkp3C30ACgkQ5vihyNWuA4Ua8wCglhRtt1cSITwrVJ/9dP61dOF5 700AniWhvKV/l2Yq+VIEnsSODZjNWr+H =dl4g -----END PGP SIGNATURE-----

Frank Mori Hess wrote:
I wasn't following the thread too closely, but I assumed the benefit of using size+alignment instead of type would be that you could reuse the same allocator with different types (as long as they had the same size and alignment). But since a type's size+alignment varies across platforms, you wouldn't be able to portably reuse such an allocator with different types (hence no portable benefit).
The size + alignment wouldn't be part of the type of the allocator, it would be part of the arguments it receives to perform a given allocation. (i.e., like posix_memalign). But strictly speaking, the language doesn't even require to take into account arbitrary alignments, you can just align to anything big enough, like malloc and operator new do.

On Wed, Aug 5, 2009 at 4:30 AM, Mathias Gaunard < mathias.gaunard@ens-lyon.org> wrote:
Frank Mori Hess wrote:
I wasn't following the thread too closely, but I assumed the benefit of
using size+alignment instead of type would be that you could reuse the same allocator with different types (as long as they had the same size and alignment). But since a type's size+alignment varies across platforms, you wouldn't be able to portably reuse such an allocator with different types (hence no portable benefit).
The size + alignment wouldn't be part of the type of the allocator, it would be part of the arguments it receives to perform a given allocation. (i.e., like posix_memalign).
This is basically what Cloneable does. I'll try to make this as short as possible: basically, there are four levels. At the bottom (most general), there is abstract_base<Base>, where Base is the user-supplied base class for a type hierarchy. abstract_base<> provides an abstract interface. Using that is mixin<Derived, Base> : abstract_base<Base>, which implements much of the interface required by abstract_base, excluding what is implemented by base<Derived,Base,Construction> which deals with types that are not default constructable. At the top is YourType : base<YourType,YourBase>. Allocation is performed by an abstract_allocator, which is adapted from a type that models a std::allocator. This is necessary because the system uses the one allocator to make objects of different types. /// /// required interface for typeless allocation /// struct abstract_allocator { typedef char *pointer; virtual pointer allocate_bytes(size_t num_bytes, size_t alignment) = 0; virtual void deallocate_bytes(pointer, size_t alignment) = 0; static size_t calc_padding(pointer ptr, size_t alignment); }; /// /// adapt a std::allocator to provide abstract_allocator interface /// template <class Allocator> struct make_clone_allocator { typedef ... type; } /// /// base for cloneable hierarchy given user-supplied base /// same for all objects that share the same base /// template <class Base> struct abstract_base : Base { virtual this_type *allocate(abstract_allocator &alloc) const = 0; // and deallocate(), clone(), create(), etc template <class Alloc> this_type *allocate(Alloc &al) const { make_clone_allocator<Alloc>::type al; return allocate(al); } }; /// /// mixin for a specific derived/base pair. note that derived could have different bases /// template <class Derived, class Base> struct mixin : abstract_base<Base> { static const size_t alignment = boost::aligned_storage<sizeof(Derived)>::alignment; virtual this_type *allocate(abstract_allocator &alloc) const // covariant virtual method { return reinterpret_cast<Derived *>(alloc.allocate_bytes(sizeof(derived_type), alignment)); } // clone(), clone_as<T>(), etc elided... }; /// /// interface to the system for clients; they derive from this /// template <class Derived, class Base, class HasDefaultCtor = has_default_ctor> struct base : mixin<Derived, Base> { /// ... details }; /////////////////////////////////////// struct MyBase { }; struct MyDerived : cloneable::base<MyDerived, MyBase> { }; int main() { // clone using default allocator MyBase *base = new MyDerived; MyDerived *derived = base->clone(); // create and clone using a custom allocator std::allocator<myDerived> alloc; MyDerived *d2 = alloc.allocate(1); new (d2) MyDerived; MyDerived *clone = d2->clone(alloc); } ///////////////////////////////////// This scheme, with some slight mods to ptr_container to pass the containers' allocator to the clone site, allows for correct deep cloning of containers of pointers. The alignment is not incorrect. It works for all types, and it is portable. Free functions are provided so non-cloneable types can be used as well. The default clone operation, implemented using copy-construction, can be overridden by simply providing a `Derived *clone(abstract_allocator &) const` method in your derived class. There are some outstanding issues, mostly to do with the containers that are built using this system and based on ptr_container. These 'heterogenous' containers entirely use emplace semantics (the containers create the objects). The main issues are method names, some semantic issues, and dealing correctly with multiple inheritance. Also, what to do with various almost-pointers like heterogenous::vector<shared_ptr<Base>> is unresolved. That said, it is the closest I've seen yet to a general way to make cloneable objects, and thereby providing true heterogenous containers with real value semantics. For example: // the user-supplied base, with comparison operator struct B { int n; B(int N = 0) : n(N) { } bool operator<(B q) { return n < q.n; } }; // derived types struct A : cloneable::base<A,B> { A(int N = 0) : B(N) { } }; struct C : cloneable::base<C,B,cloneable::no_default_construct> { C(int N) : B(N) { } }; typedef heterogenous::list<B> List; List list; list.push_back<A>(1); // emplace list.push_back<C>(2); List list2 = list; assert(typeid(list2.back()) == typeid(C)); typedef my_magic_allocator<B> Alloc; typedef heterogenous::set<B, Alloc> Set; Set set; set.insert<A>(1); // emplace set.insert<C>(2); Set set2 = set; // deep copy using correct allocator assert(set2.find<C>(2) != set2.end()); // find an instance assert(set2.find<A>(2) == set2.end()); assert(set2.find<B>(1) != set2.end()); // search for any type that matches assert(set2.find<B>(2) != set2.end()); Similarly for map, vector etc. Map has the interesting case of: typedef heterogenous::iso_map<B> Map; Map map; map.key<A>(1).value<C>(2); However, so far I have only implemented map<Key, Base> (map of value-type to pointer), not iso_map<Base> (map of pointer to pointer). This is due to how ptr_map<> is implemented, but the implementation of iso_map<Base> is obvious if technically tedious. Regards, Christian.

On Sun, Aug 2, 2009 at 1:31 PM, Mathias Gaunard < mathias.gaunard@ens-lyon.org> wrote:
Christian Schladetsch wrote:
[snip] I wanted a way to make the cloning operation for ptr_container to use the same allocator that the container uses (and indeed, have the container use the correct allocator at all!).
Standard allocators are inherently incompatible with the very idea of cloneable objects, since the type information is supposed to be lost.
The only sensible thing you can do (IMHO) is use a standard allocator of char or use an allocator interface similar to that of malloc or operator new.
I personally never understood why they tied the type into the allocator. Having just the size and alignment would be so much better.
This is largely what I did. The allocation model used by Cloneable is based on covariant virtual methods that are passed an 'abstract allocator' which takes the alignment as an argument. struct abstract_allocator { typedef char *pointer; virtual pointer allocate_bytes(size_t num_bytes, size_t alignment) = 0; virtual void deallocate_bytes(pointer, size_t alignment) = 0; static size_t calc_padding(pointer ptr, size_t alignment); }; An abstract allocator is then adapted from a type that models std::allocator and is passed to methods used to create objects used by the system. Yes, it means that 'type info' is lost, as the allocations are really just sequences of correctly-aligned bytes - but I can't see this as a problem (and anyway, it is exactly what is happening). As you say, there doesn't seem to be anything to gain from having the type information in the signature of an allocator type - and much to lose. So I just inverted it, removed the type from the allocator signature, and now pass the alignment down as needed from where the type information is.
You simply have to use emplace semantics for these containers;
otherwise you aren't respecting the allocator
For when you copy the container later, do you maybe add a pointer overhead per object on your heterogeneous list to reference a rightly rebound function for cloning with the given container allocator? Not something very nice, since it could be avoided.
No, there is no per-object overhead. Each object in the container is `cloneable`, via the mixin that is added in the declaration: struct MyDerived : cloneable::base<MyDerived, SomeBase> { ... }; This of couse adds a vtbl to all your types used by the library, but that is unavoidable. The main costs are the dynamic_cast's used by the system, both internally and to answer queries for the associative containers (which answer based on type and value). Dynamic_cast<> can be very slow, but the effect is mitigated because in general the casts succeed (dynamic_cast is slowest for classes with many derived types when the cast eventually fails). The need for dynamic_cast<> over static_cast<> was introduced when I had to make the base virtual to allow rudimentary multiple inheritance support. That said, there is a structure in the libary called 'cloneable::instance<>' which stores an object pointer and a reference to the allocator that made it. This is used by the system when adding new objects to containers but is not stored anywhere. However, I found the idea of pairing objects with their allocators so appealing that I exposed it in the public interface for the library. It is one thing to be able to clone an object using clone(Q) or Q.clone(); but that is only half the story. To be complete, IMO, these methods should also be able to make a clone using the same allocator as the original. Hence the idea of a 'cloneable::instance<>'. How important this is in general may be questionable. But for high-performance system software, where lifetime management is often optimised for different timeframes (scope-local, per-frame, across milliseconds, across seconds, across hours etc), then knowledge about an instance including its allocator can be very handy. It is a way of adding semantics to an instance, not all of which are created equally. Anyway, I'm currently blocked on how to deal with multiple inheritance and will have to make the call soon about whether to drop explicit support for it, or to basically re-write it to pass the derived types to the cloneable system and have it synthesise everything itself. That approach may or may not even work. Regards, Christian.

Christian Schladetsch wrote:
This is largely what I did. The allocation model used by Cloneable is based on covariant virtual methods that are passed an 'abstract allocator' which takes the alignment as an argument.
struct abstract_allocator { typedef char *pointer; virtual pointer allocate_bytes(size_t num_bytes, size_t alignment) = 0; virtual void deallocate_bytes(pointer, size_t alignment) = 0; static size_t calc_padding(pointer ptr, size_t alignment); };
Ok, I see. But what gains does using a special container give?
participants (11)
-
Christian Schladetsch
-
Frank Mori Hess
-
Frank Mori Hess
-
Mathias Gaunard
-
Peter Foelsche
-
Robert Ramey
-
Ross Levine
-
Scott McMurray
-
Simonson, Lucanus J
-
Steven Watanabe
-
Stewart, Robert