[shared_ptr] Design question about make_shared

Hello everybody, I recently wondered about the choice to make make_shared a free function, and not a static member function: struct A{...}; struct B:A {...}; shared_ptr<A> ptr = make_shared<B>(arg1,arg2); vs shared_ptr<A> ptr = shared_ptr<B>::make(arg1, arg2); I was wondering if this is just a stylistic choice, or if there are some differences? Thank you for any information. -- Loïc

On 15.06.2011 4:16, Loïc Joly wrote:
Hello everybody,
I recently wondered about the choice to make make_shared a free function, and not a static member function:
struct A{...}; struct B:A {...};
shared_ptr<A> ptr = make_shared<B>(arg1,arg2); vs shared_ptr<A> ptr = shared_ptr<B>::make(arg1, arg2);
I was wondering if this is just a stylistic choice, or if there are some differences?
Thank you for any information.
free functions are preferable due to decreasing code coupling: static member have access to other static members, but (non-friendly) free function doesn't. think about boost::make_shared<> as yet another constructor for boost::shared_ptr<> with alternative name :)

[Loïc Joly]
I was wondering if this is just a stylistic choice, or if there are some differences?
Unless I'm missing something, it's purely stylistic: * make_shared<T>() is less typing (with less punctuation) than shared_ptr<T>::make(). * What would you call allocate_shared<T>()? * It is similar to make_pair()/make_tuple(), although different in effect. The notable thing is that C++0x lacks make_unique<T>(), which it should really have. [Max Sobolev]
free functions are preferable due to decreasing code coupling:
Actually, make_shared<T>() needs access to shared_ptr's guts, at least if the implementer takes advantage of the obvious and highly desirable optimization. They are tightly coupled to begin with. Stephan T. Lavavej Visual C++ Libraries Developer (I obviously didn't design make_shared<T>(), but I have implemented it.)

On Tue, Jun 14, 2011 at 11:56 PM, Stephan T. Lavavej <stl@exchange.microsoft.com> wrote:
[Max Sobolev]
free functions are preferable due to decreasing code coupling:
Actually, make_shared<T>() needs access to shared_ptr's guts, at least if the implementer takes advantage of the obvious and highly desirable optimization. They are tightly coupled to begin with.
However, the coupling between make_shared and shared_ptr is an implementation detail. They may or may not be coupled. For example make_shared and shared_ptr might be coupled with another internal type that facilitates the "highly desirable optimization", but not with each other. In general, a member function is always coupled with a type, whereas a free function may or may not be coupled with one or more types (formally by specifying it as a "friend"). Using free functions, you can choose to pay the price in terms of physical coupling to implement a desirable optimization, or you can choose a suboptimal (in run-time terms) but less coupled design; and that choice does not affect the user interface. Emil Dotchevski Reverge Studios, Inc. http://www.revergestudios.com/reblog/index.php?n=ReCode

[STL]
Actually, make_shared<T>() needs access to shared_ptr's guts, at least if the implementer takes advantage of the obvious and highly desirable optimization. They are tightly coupled to begin with.
[Emil Dotchevski]
However, the coupling between make_shared and shared_ptr is an implementation detail.
Right, that's what I was trying to say - internally, they are coupled, yet make_shared goes out of its way to be a free function. STL

On Wed, Jun 15, 2011 at 10:35:05AM +0000, Stephan T. Lavavej wrote:
[STL]
Actually, make_shared<T>() needs access to shared_ptr's guts, at least if the implementer takes advantage of the obvious and highly desirable optimization. They are tightly coupled to begin with.
[Emil Dotchevski]
However, the coupling between make_shared and shared_ptr is an implementation detail.
Right, that's what I was trying to say - internally, they are coupled, yet make_shared goes out of its way to be a free function.
Notable in general is that free functions can be forward declared on their own, but to declare and use a static member function, you need to have the class definition present. -- Lars Viklund | zao@acc.umu.se

Stephan T. Lavavej wrote:
Right, that's what I was trying to say - internally, they are coupled, yet make_shared goes out of its way to be a free function.
My initial implementation of boost::make_shared was a self-contained header that didn't require any changes to shared_ptr (it would have worked with std::tr1::shared_ptr as well as it worked with boost::shared_ptr). This can only be done with a free function. Now, this copied around the deleter a few times more than necessary and created problems with over-aligned types, so you're right that an optimized implementation needs support from shared_ptr. :-) (However, it can be argued that this support - the ability to ask for a deleter of a specific type to be default-constructed in place by shared_ptr - should be offered to users as well, not just to make_shared.)

On 15/6/2011 13:15, Peter Dimov wrote:
My initial implementation of boost::make_shared [...]
Now, this copied around the deleter a few times more than necessary and created problems with over-aligned types, so you're right that an optimized implementation [...]
speaking of which (i.e. deleter and make_shared) is there any way to get the optimized layout and a custom deleter? Because if I understand correctly neither make_shared nor allocate_shared can be used with custom deleters? regards Fabio

[Peter Dimov]
My initial implementation of boost::make_shared was a self-contained header that didn't require any changes to shared_ptr (it would have worked with std::tr1::shared_ptr as well as it worked with boost::shared_ptr). This can only be done with a free function.
Aha - that's an interesting bit of history!
(However, it can be argued that this support - the ability to ask for a deleter of a specific type to be default-constructed in place by shared_ptr - should be offered to users as well, not just to make_shared.)
I don't look at Boost's implementation, but this makes me curious as to whether you implement the "we know where you live" optimization. On x86, for a 4-byte Foobar, how big is your reference count control block for make_shared<Foobar>()? Mine is 16 bytes. [Fabio Fracassi]
speaking of which (i.e. deleter and make_shared) is there any way to get the optimized layout and a custom deleter? Because if I understand correctly neither make_shared nor allocate_shared can be used with custom deleters?
Ordinary shared_ptrs have separate objects and reference count control blocks. The object, allocated by the user, needs to be disposed of - that's what a custom deleter is for. Custom allocators are used to allocate and deallocate the control block. With make_shared<T>(), the object isn't constructed by the user, it's constructed by the shared_ptr, and it lives within the control block. Therefore, there's no need for a custom deleter. allocate_shared<T>()'s custom allocator performs the single dynamic memory allocation and deallocation. STL

Stephan T. Lavavej wrote:
I don't look at Boost's implementation, but this makes me curious as to whether you implement the "we know where you live" optimization. On x86, for a 4-byte Foobar, how big is your reference count control block for make_shared<Foobar>()? Mine is 16 bytes.
Yes, you're right, I'm not eliminating the pointer at the moment. (I wasn't sure to which optimization you refer.)

Stephan T. Lavavej wrote:
[Fabio Fracassi]
speaking of which (i.e. deleter and make_shared) is there any way to get the optimized layout and a custom deleter? Because if I understand correctly neither make_shared nor allocate_shared can be used with custom deleters?
Ordinary shared_ptrs have separate objects and reference count control blocks. The object, allocated by the user, needs to be disposed of - that's what a custom deleter is for. Custom allocators are used to allocate and deallocate the control block.
With make_shared<T>(), the object isn't constructed by the user, it's constructed by the shared_ptr, and it lives within the control block. Therefore, there's no need for a custom deleter. allocate_shared<T>()'s custom allocator performs the single dynamic memory allocation and deallocation.
It can still make sense to have a custom deleter, to be used by make_shared in place of the explicit destructor call it issues on the embedded object. I don't have a specific use case in mind now though, maybe Fabio can provide one. Something like make_shared<int> to hold a file descriptor, with close() needing to be called instead of ~int. One can implement something like that from the user side, via the equivalent of make_shared< pair<T,D> >, where the pair has a destructor that calls d_(t_), and then using the aliasing constructor on the result to get a shared_ptr<T>. On this same note, allocate_shared ought to use the construct and destroy members of the allocator, so it also provides a similar (but not quite the same) customization point.

On 16/06/2011 06:43, Stephan T. Lavavej wrote:
[Peter Dimov]
My initial implementation of boost::make_shared was a self-contained header that didn't require any changes to shared_ptr (it would have worked with std::tr1::shared_ptr as well as it worked with boost::shared_ptr). This can only be done with a free function.
Aha - that's an interesting bit of history!
(However, it can be argued that this support - the ability to ask for a deleter of a specific type to be default-constructed in place by shared_ptr - should be offered to users as well, not just to make_shared.)
I don't look at Boost's implementation, but this makes me curious as to whether you implement the "we know where you live" optimization. On x86, for a 4-byte Foobar, how big is your reference count control block for make_shared<Foobar>()? Mine is 16 bytes.
What's the point? It still need to be padded to 32 for good performance and to use DWCAS.

Le 15/06/2011 13:15, Peter Dimov a écrit :
Stephan T. Lavavej wrote:
Right, that's what I was trying to say - internally, they are coupled, yet make_shared goes out of its way to be a free function.
My initial implementation of boost::make_shared was a self-contained header that didn't require any changes to shared_ptr (it would have worked with std::tr1::shared_ptr as well as it worked with boost::shared_ptr). This can only be done with a free function.
Now, this copied around the deleter a few times more than necessary and created problems with over-aligned types, so you're right that an optimized implementation needs support from shared_ptr. :-) (However, it can be argued that this support - the ability to ask for a deleter of a specific type to be default-constructed in place by shared_ptr - should be offered to users as well, not just to make_shared.)
Thank you for all those answers. I was wondering if ADL had something to do in this decision (I could not see how, at least not in a positive way), but it looks like this was not a criterion. -- Loïc

[Loïc Joly]
Thank you for all those answers. I was wondering if ADL had something to do in this decision (I could not see how, at least not in a positive way), but it looks like this was not a criterion.
make_shared<T>() requires an explicit template argument, and explicit template arguments inhibit ADL. This is explained in C++ Templates: The Complete Guide, if I recall correctly (it is a fundamental limitation). STL

On Wed, Jun 15, 2011 at 3:35 AM, Stephan T. Lavavej <stl@exchange.microsoft.com> wrote:
[STL]
Actually, make_shared<T>() needs access to shared_ptr's guts, at least if the implementer takes advantage of the obvious and highly desirable optimization. They are tightly coupled to begin with.
[Emil Dotchevski]
However, the coupling between make_shared and shared_ptr is an implementation detail.
Right, that's what I was trying to say - internally, they are coupled, yet make_shared goes out of its way to be a free function.
I'm not sure if I read what you're saying correctly, but it seems to me that the default for any design should be less, not more coupling. If you make something a member, not only you're coupled, but also that fact is part of the interface, and therefore not an implementation detail. I wouldn't say that make_shared goes out of its way to be a free function -- it is a free function by default, so to speak. You need to have an upside to making something a member function, something that offsets the price you're paying in terms of coupling. Emil Dotchevski Reverge Studios, Inc. http://www.revergestudios.com/reblog/index.php?n=ReCode

[Emil Dotchevski]
I wouldn't say that make_shared goes out of its way to be a free function -- it is a free function by default, so to speak. You need to have an upside to making something a member function, something that offsets the price you're paying in terms of coupling.
That's reasonable. Or to put it another way, "don't be bloaty like std::string". STL
participants (8)
-
Emil Dotchevski
-
Fabio Fracassi
-
Lars Viklund
-
Loïc Joly
-
Mathias Gaunard
-
Max Sobolev
-
Peter Dimov
-
Stephan T. Lavavej