29

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.

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
MvG
  • 57,380
  • 22
  • 148
  • 276
  • 8
    Consider situation when you call `make_unique>(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). – rafix07 Mar 13 '19 at 12:17
  • 4
    If it's any consolation, there's [p0960](https://wg21.link/p0960) in the pipeline. Future versions of C++ may support aggregates without needing to change the implementation of how factory library functions work. – StoryTeller - Unslander Monica Mar 13 '19 at 12:19
  • 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. – StoryTeller - Unslander Monica Mar 13 '19 at 12:21
  • "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. – Slava Mar 13 '19 at 12:24
  • 2
    From [this proposal](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3588.txt), 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. – BoBTFish Mar 13 '19 at 14:03

3 Answers3

27

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.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • Parenthesized initialization has its own daemon too. This initializer class would be more complex if it were doing non braced direct initialization. – Oliv May 26 '20 at 22:29
  • 2
    With C++20 this still won't compile: `auto p = std::make_unique>(1, 2);` C++ initialization is a riddle wrapped in a mystery inside an enigma. – Amir Kirsh May 27 '20 at 11:26
  • @AmirKirsh we can see [here](https://en.cppreference.com/w/cpp/compiler_support/20) 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](https://stackoverflow.com/a/74885772/10360134) that gives features tests code to figure out if you have access to this feature on your compiler version. – WaterFox Dec 24 '22 at 02:23
17

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.

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
  • C++20 changes this answer I believe. – Yakk - Adam Nevraumont Mar 13 '19 at 13:02
  • 4
    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. – Handy999 Mar 14 '19 at 08:39
8

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)
Evg
  • 25,259
  • 5
  • 41
  • 83