Why Does C++11 Have 'Make_Shared' But Not 'Make_Unique'

Why does C++11 have `make_shared` but not `make_unique`

According to Herb Sutter in this article it was "partly an oversight". The article contains a nice implementation, and makes a strong case for using it:

template<typename T, typename ...Args>
std::unique_ptr<T> make_unique( Args&& ...args )
{
return std::unique_ptr<T>( new T( std::forward<Args>(args)... ) );
}

Update: The original update has been updated and the emphasis has changed.

How does std::make_shared and std::make_unique work behind the scenes?

You can use variadic templates and forwarding to forward an arbitrary amount of arguments to another constructor or function. For example:

#include <memory>  // Include std::forward.

template <typename T>
class SmartPointer
{
public:
template <typename ... Args>
SmartPointer(Args&& ... args) : the_pointer{new T{std::forward<Args>(args)...}} {}

// Other methods ...

private:
T* the_pointer;
};

struct Test
{
int a;
float b;
};

int main()
{
SmartPointer<Test> pointer { 0, 1.0f };
}

This will forward the arguments 0 and 1.0f to the constructor of Test. The expanded class would look something like this:

class SmartPointer
{
public:
SmartPointer(int&& a, float&& b)
: the_pointer{new Test{std::forward<int&&>(a), std::forward<float&&>(b)}} {}

// Other methods ...

private:
Test* the_pointer;
};

Try it online!

Differences between std::make_unique and std::unique_ptr with new

The motivation behind make_unique is primarily two-fold:

  • make_unique is safe for creating temporaries, whereas with explicit use of new you have to remember the rule about not using unnamed temporaries.

    foo(make_unique<T>(), make_unique<U>()); // exception safe

    foo(unique_ptr<T>(new T()), unique_ptr<U>(new U())); // unsafe*
  • The addition of make_unique finally means we can tell people to 'never' use new rather than the previous rule to "'never' use new except when you make a unique_ptr".

There's also a third reason:

  • make_unique does not require redundant type usage. unique_ptr<T>(new T()) -> make_unique<T>()

None of the reasons involve improving runtime efficiency the way using make_shared does (due to avoiding a second allocation, at the cost of potentially higher peak memory usage).

* It is expected that C++17 will include a rule change that means that this is no longer unsafe. See C++ committee papers P0400R0 and P0145R3.

differences between make_unique and make_shared when handling arrays

What prevented the standard maker people to let make_shared support array types [...]?

Probably nothing, this case was simply not considered, similar to std::make_unique not being present in C++11 but added in C++14. And as pointed out in the comments, this missing piece will ship with C++20.

There is a difference between std::unique_ptr and std::shared_ptr that made neglecting raw arrays pointers easy, though: custom deleters are part of std::unique_ptr's type but not part of std::shared_ptr's type. Therefore, you can handle an array like this

std::shared_ptr<int> array = std::shared_ptr<int>(new int[10],
[](int *ptr){ delete []ptr; });

and hence delegate the correct memory cleanup to the point of object creation. This makes it easy to treat raw arrays as a special case of std::shared_ptr instances.

Is there a reason why std::make_shared/std::make_unique don't use list initialization?

Specifically, what pitfalls can be in the list initialization solution?

All of the typical pitfalls of using list-initialization.

For example, the hiding of non-initializer_list constructors. What does make_shared<vector<int>>(5, 2) do? If your answer is "constructs an array of 5 ints", that's absolute correct... so long as make_shared isn't using list-initialization. Because that changes the moment you do.

Note that suddenly changing this would break existing code, since right now all of the indirect initialization functions use constructor syntax. So you can't just change it willy-nilly and expect the world to keep working.

Plus one more unique to this case: the narrowing issue:

struct Agg
{
char c;
int i;
};

You can do Agg a{5, 1020}; to initialize this aggregate. But you could never do make_shared<Agg>(5, 1020). Why? Because the compiler can guarantee that the literal 5can be converted to a char with no loss of data. However, when you use indirect initialization like this, the literal 5 is template-deduced as int. And the compiler cannot guarantee that any int can be converted to a char with no loss of data. This is called a "narrowing conversion" and is expressly forbidden in list initialization.

You would need to explicitly convert that 5 to a char.

The standard library has an issue on this: LWG 2089. Though technically this issue talks about allocator::construct, it should equally apply to all indirect initialization functions like make_X and C++17's in-place constructors for any/optional/variant.

why does it too follow same pattern?

It follows the same pattern because having two different functions that look almost identical that have radically and unexpectedly different behaviors would not be a good thing.


Note that C++20 resolves the aggregate part of this issue at least by making constructor-style syntax invoke aggregate initialization if the initializers would have been ill-formed for regular direct initialization. So if T is some aggregate type (with no user-declared constructors), and T(args) wouldn't invoke a copy/move constructor (the only constructors that take arguments which a type with no user-declared constructors could have), then the arguments will instead be used to attempt to aggregate initialize the structure.

Since allocator::construct and other forms of forwarded initialization default to direct-initialization, this will let you initialize aggregates through forwarded initialization.

You still can't do other list-initialization stuff without explicitly using an initializer_list at the call site. But that's probably for the best.

Why does C++ need std::make_unique over forwarded unique_ptr constructor?

I want to summarize discussion with Some programmer dude, StoryTeller - Unslander Monica and Raymond Chen

So, there are 2 reasons:

  1. std::make_unique pairs well with std::make_shared which was introduced earlier so this was easier to learn than new constructor for unique_ptr.
  2. There is possible ambiguity between constructor of unique_ptr and constructor of inner value type (T) if this type have own constructor which takes a pointer to self type. E.g.
struct Inner{
Inner() = default;
Inner(Inner* parent_node): parent(parent_node){}

Inner* parent = nullptr;
};

Inner* parent = make_parent();
// It would be not clear for human which constructor must be called here
std::unique_ptr<Inner> child(parent);

Compiler can deduce which constructor should be called here but it is hard for human. So having function std::make_unique is beneficial because it is clear that all constructors of std::unique_ptr create only unique_ptr and never call inner value constructor while std::make_unique would always call constructor of inner value. This makes code much easier to reason about.

Thanks to everyone for discussion!



Related Topics



Leave a reply



Submit