In practice, many C++ projects disable exceptions so this case isn’t as big a deal to me, though the direct use of new is an immediate red flag. In general, you should never call new in modern C++ code, unless you’re doing something special like in-place new, or writing your own allocators. make_shared also has the benefit of allowing the implementation to do a single allocation, since otherwise you’re going to be doing one allocation for your widget, and then another allocation for the tracking block for tracking the shared pointer internals.
mercifully modern C[++] compilers have realized that unspecified argument evaluation order is bananas and all do left to right evaluation.
I recall a bug I was hitting back at uni that I could not debug until I discovered GCC was evaluating arguments in different order depending on optimization level. That was Not Fun (tm).
Are you sure it isn’t right-to-left? Because in general (except for the first few parameters often going in registers) most ABIs specify “additional arguments are pushed onto the stack (right to left).” That’s a quote from Wikipedia’s summary of the X86-64 ABI (both Windows and Unix agree on that part.) That’s mostly for historical reasons, because K&R C required that order so parameters would be at known stack offsets.
this might be why there is std::make_shared, with std::make_shared() being equivalent to std::shared_ptr(new Widget).
This is, indeed, the rationale behind std::make_shared. As a side effect, it also gives you a single allocation for both the underlying object and the reference count, which means that it’s always more efficient (or, in the worst case, no less efficient) to call std::make_shared than operator new followed by std::shared_ptr’s constructor.
C++14 added std::make_unique. This wasn’t in C++11 because there’s no efficiency gain, but it allows you to have a coding convention such as the C++ Core Guidelines that tell you not to have a bare operator new in any code. This is generally a good idea: if you’re calling operator new anywhere outside of a smart pointer class, that’s a bad code smell in modern C++.
it’s always more efficient (or, in the worst case, no less efficient) to call std::make_shared than operator new followed by std::shared_ptr’s constructor.
Using make_shared can be less efficient when you consider the fact that it’s impossible to free that memory until all instances of both shared_ptr and weak_ptr refer to it. You can end up in a situation when only weak pointers prevent deallocation and your applications memory usage is growing needlessly. Depending on the sizeof of type parameter of shared_ptr this could be a serious issue.
There is also the case that make_unique<uint8_t>zero-initializes the buffer. Which is most of the time nothing but a performance bug nobody wants (and to be honest, quite surprising).
So yes, we’re still not done with all valid uses of operator new :-(
That’s a good point. For those unfamiliar with how std::shared_ptr works:
It allocates a control structure that contains a strong and weak reference count and a pointer to the real object. Strong references are owned by std::shared_ptr instances, weak references by std::weak_ptr. Weak references can only be created if you hold a strong reference. You cannot use a weak_ptr directly, you must convert it to a shared_ptr first. When the last strong reference (shared_ptr) goes away, it can delete the underlying object but keep the control structure around. When a weak_ptr is then accessed, it decrements the weak reference count and nulls out its reference to the control structure. When all weak_ptrs have gone away, the control structure is deallocated.
When you use std::make_shared, the control structure and the object are in the same allocation. When the last shared_ptr goes away, the object’s destructor will be run, but the memory won’t be reclaimed.
I’d largely forgotten about that, because I rarely use weak_ptr, but if you do then it’s an interesting omission in the standard: there’s no way of doing exception-safe allocation of an object with a separate shared pointer control block without writing your own allocation wrapper.
this might be why there is std::make_shared, with std::make_shared() being equivalent to std::shared_ptr(new Widget).
These are not equivalent: make_shared will make a single allocation which holds both ref count and the widget, while the new route will result into two separate allocations for these two pieces of data (with a lot of finer differences in how is this achieved).
The first trick here is that function parameters evaluation order is unspecified, meaning that new Widget might be called, then priority(), then the value returned by new Widget is passed to std::shared_ptr(…)
I know the order of evaluation of function parameters is undefined, but I’ve never heard of the compiler being allowed to skip around between multiple function calls evaluating a parameter here, a parameter there… I don’t actually own EffC++; can someone verify this is true?
In other words, my understanding is that the compiler will first fully evaluate one parameter of processWidget, then the other. The order may be unspecified, but after the “new” operator we know the next call will be to the shared_ptr constructor. Thus there’s no chance of a leak … as I understand it.
Wild indeed. I still don’t want to admit this is true, 🙈 so I’m desperately seizing on the disclaimer at the top of the Sutter article:
This is the original GotW problem and solution substantially as posted to Usenet. See the book More Exceptional C++ (Addison-Wesley, 2002) for the most current solution to this GotW issue. The solutions in the book have been revised and expanded since their initial appearance in GotW. The book versions also incorporate corrections, new material, and conformance to the final ANSI/ISO C++ standard.
The article isn’t dated, but it must be from before 2002. I wonder if this part of the C++ spec has changed since then, considering how unintuitive this behavior is. 🤞🏻😬
Imagine an arithmetic expression like (a+b)*(c+d). The two additions are independent and can be computed in parallel. If the CPU has multiple ALUs for the parallel computation and there are enough registers to hold the data, the compiler should interleave the evaluation to enable instruction-level parallelism.
Such an optimization can result in this „skipping around“.
In practice, many C++ projects disable exceptions so this case isn’t as big a deal to me, though the direct use of
new
is an immediate red flag. In general, you should never callnew
in modern C++ code, unless you’re doing something special like in-place new, or writing your own allocators.make_shared
also has the benefit of allowing the implementation to do a single allocation, since otherwise you’re going to be doing one allocation for your widget, and then another allocation for the tracking block for tracking the shared pointer internals.mercifully modern C[++] compilers have realized that unspecified argument evaluation order is bananas and all do left to right evaluation.
I recall a bug I was hitting back at uni that I could not debug until I discovered GCC was evaluating arguments in different order depending on optimization level. That was Not Fun (tm).
gcc and clang use opposite orders. Here’s one reference I could find: https://dev.to/aditya612/argument-evaluation-order-in-c-31e5. It’s been a few years, but I’ve debugged some fun portability bugs because of that.
Are you sure it isn’t right-to-left? Because in general (except for the first few parameters often going in registers) most ABIs specify “additional arguments are pushed onto the stack (right to left).” That’s a quote from Wikipedia’s summary of the X86-64 ABI (both Windows and Unix agree on that part.) That’s mostly for historical reasons, because K&R C required that order so parameters would be at known stack offsets.
Order of evaluation is, in general, not specified by the standard, and to depend on it is just asking for bugs in my experience.
This is, indeed, the rationale behind
std::make_shared
. As a side effect, it also gives you a single allocation for both the underlying object and the reference count, which means that it’s always more efficient (or, in the worst case, no less efficient) to callstd::make_shared
thanoperator new
followed bystd::shared_ptr
’s constructor.C++14 added
std::make_unique
. This wasn’t in C++11 because there’s no efficiency gain, but it allows you to have a coding convention such as the C++ Core Guidelines that tell you not to have a bareoperator new
in any code. This is generally a good idea: if you’re callingoperator new
anywhere outside of a smart pointer class, that’s a bad code smell in modern C++.Using
make_shared
can be less efficient when you consider the fact that it’s impossible to free that memory until all instances of bothshared_ptr
andweak_ptr
refer to it. You can end up in a situation when only weak pointers prevent deallocation and your applications memory usage is growing needlessly. Depending on the sizeof of type parameter ofshared_ptr
this could be a serious issue.There is also the case that
make_unique<uint8_t>
zero-initializes the buffer. Which is most of the time nothing but a performance bug nobody wants (and to be honest, quite surprising).So yes, we’re still not done with all valid uses of
operator new
:-(That’s a good point. For those unfamiliar with how
std::shared_ptr
works:It allocates a control structure that contains a strong and weak reference count and a pointer to the real object. Strong references are owned by
std::shared_ptr
instances, weak references bystd::weak_ptr
. Weak references can only be created if you hold a strong reference. You cannot use aweak_ptr
directly, you must convert it to ashared_ptr
first. When the last strong reference (shared_ptr
) goes away, it can delete the underlying object but keep the control structure around. When aweak_ptr
is then accessed, it decrements the weak reference count and nulls out its reference to the control structure. When allweak_ptr
s have gone away, the control structure is deallocated.When you use
std::make_shared
, the control structure and the object are in the same allocation. When the lastshared_ptr
goes away, the object’s destructor will be run, but the memory won’t be reclaimed.I’d largely forgotten about that, because I rarely use
weak_ptr
, but if you do then it’s an interesting omission in the standard: there’s no way of doing exception-safe allocation of an object with a separate shared pointer control block without writing your own allocation wrapper.These are not equivalent:
make_shared
will make a single allocation which holds both ref count and the widget, while thenew
route will result into two separate allocations for these two pieces of data (with a lot of finer differences in how is this achieved).I know the order of evaluation of function parameters is undefined, but I’ve never heard of the compiler being allowed to skip around between multiple function calls evaluating a parameter here, a parameter there… I don’t actually own EffC++; can someone verify this is true?
In other words, my understanding is that the compiler will first fully evaluate one parameter of processWidget, then the other. The order may be unspecified, but after the “new” operator we know the next call will be to the shared_ptr constructor. Thus there’s no chance of a leak … as I understand it.
I was surprised at first, but after double-checking, the boost docs and Herb Sutter back it up. Wild.
Wild indeed. I still don’t want to admit this is true, 🙈 so I’m desperately seizing on the disclaimer at the top of the Sutter article:
The article isn’t dated, but it must be from before 2002. I wonder if this part of the C++ spec has changed since then, considering how unintuitive this behavior is. 🤞🏻😬
So C++ is non-strict?
Imagine an arithmetic expression like (a+b)*(c+d). The two additions are independent and can be computed in parallel. If the CPU has multiple ALUs for the parallel computation and there are enough registers to hold the data, the compiler should interleave the evaluation to enable instruction-level parallelism.
Such an optimization can result in this „skipping around“.