Why is std::make_unique not implemented using list initialization?
Asked Answered
B

3

32

https://en.cppreference.com/w/cpp/memory/unique_ptr/make_unique writes that std::make_unique can be implemented as

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

This does not work for plain structs with no constructors. Those can be brace-initialized but don't have a non-default constructor. Example:

#include <memory>
struct point { int x, z; };
int main() { std::make_unique<point>(1, 2); }

Compiling this will have the compiler complain about lack of a 2-argument constructor, and rightly so.

I wonder, is there any technical reason not to define the function in terms of brace initialization instead? As in

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

That works well enough for the scenario above. Are there any other legitimate use cases this would break?

Seeing how the general trend appears to prefer braces for initialization, I would assume making braces in that template would be the canonical choice, but the fact that the standard doesn't do it might be an indication of me missing something.

Butacaine answered 13/3, 2019 at 12:10 Comment(5)
Consider situation when you call make_unique<vector<int>>(10,20) what do you want to get ? vector with 10 items, all 20 value (when () is used in make_unique), or vector with 2 items 10,20 (when {} is used, vector has constructor which takes initializer_list).Benedetta
If it's any consolation, there's p0960 in the pipeline. Future versions of C++ may support aggregates without needing to change the implementation of how factory library functions work.Mccaffrey
Although conceivably, it's possible to support stuff like that today with the help of the std::is_aggregate trait. I guess people felt it's better to have a language solution to that, rather than a library one.Mccaffrey
"Seeing how the general trend appears to prefer braces for initialization" I am afraid due to problem with vector etc there is no such trend anymore.Colorado
From this proposal, it was done to match make_shared. I have yet to find why make_shared chose one way over the other. shared_ptr was in TR1 though, long before unified initialisation syntax existed - not certain if make_shared existed back then.Broeker
W
18

Some classes have different behavior with the 2 initialization styles. e.g.

std::vector<int> v1(1, 2); // 1 element with value 2
std::vector<int> v2{1, 2}; // 2 elements with value 1 & 2

There might not be enough reason to choose one prefer to another; I think the standard just choose one and state the decision explicitly.

As the workaround, you might want to implement your own make_unique version. As you have showed, it's not a hard work.

Whimper answered 13/3, 2019 at 12:17 Comment(2)
C++20 changes this answer I believe.Ameeameer
No, C++20 will not break anything from the answer. The 2 versions of std::vector constructors will still be different and make_shared will still call the round brace constructor (the committee will never support changes that break tons of code). However, C++20 will allow aggregate initialization with standard braces.Stratosphere
C
29

In C++20, this will compile:

std::make_unique<point>(1, 2);

due to the new rule allowing initializing aggregates from a parenthesized list of values.


In C++17, you can just do:

std::unique_ptr<point>(new point{1, 2});

That won't work with make_shared though. So you can also just create a factory (forwarding left as an exercise):

template <typename... Args>
struct braced_init {
    braced_init(Args... args) : args(args...) { }
    std::tuple<Args...> args;

    template <typename T>
    operator T() const {
        return std::apply([](Args... args){
            return T{args...};
        }, args);
    }
};

std::make_unique<point>(braced_init(1, 2));

In C++14, you'll have to implement apply and write a factory function for braced_init because there's no CTAD yet - but these are doable.


Seeing how the general trend appears to prefer braces for initialization

Citation needed. It's a charged topic - but I definitely disagree with the claim.

Capillarity answered 13/3, 2019 at 14:48 Comment(3)
Parenthesized initialization has its own daemon too. This initializer class would be more complex if it were doing non braced direct initialization.Pangaro
With C++20 this still won't compile: auto p = std::make_unique<std::array<int, 2>>(1, 2); C++ initialization is a riddle wrapped in a mystery inside an enigma.Thrall
@AmirKirsh we can see here that Parenthesized initialization of aggregate (P0960R3) is only available on gcc >= 10, Clang >= 16 and not available on Apple Clang - reason why I was unable to compile this. –. See also this answer that gives features tests code to figure out if you have access to this feature on your compiler version.Chauffeur
W
18

Some classes have different behavior with the 2 initialization styles. e.g.

std::vector<int> v1(1, 2); // 1 element with value 2
std::vector<int> v2{1, 2}; // 2 elements with value 1 & 2

There might not be enough reason to choose one prefer to another; I think the standard just choose one and state the decision explicitly.

As the workaround, you might want to implement your own make_unique version. As you have showed, it's not a hard work.

Whimper answered 13/3, 2019 at 12:17 Comment(2)
C++20 changes this answer I believe.Ameeameer
No, C++20 will not break anything from the answer. The 2 versions of std::vector constructors will still be different and make_shared will still call the round brace constructor (the committee will never support changes that break tons of code). However, C++20 will allow aggregate initialization with standard braces.Stratosphere
N
9

In addition to other answers, in his presentation on C++17, Alisdair Meredith gives the following implementation of make_unique:

template<typename T, typename... Args>
auto make_unique(Args&&... args) -> std::unique_ptr<T> {
    if constexpr (std::is_constructible<T, Args...>::value)
        return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
    else
        return std::unique_ptr<T>(new T{std::forward<Args>(args)...});
}

It uses C+17 if constexpr, but can easily be rewritten without it.

With this version you can do both

auto v = make_unique<std::vector<int>>(10, 20); // *v is a vector of 10 elements

and

auto p = make_unique<point>(10, 20); // *p is a point (10, 20)
Northwester answered 25/9, 2019 at 9:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.