Perfect forwarding in std::make_unique<SomeWrapper<T>> not quite perfect

Viewed 91

Given the following types:

struct Point
{
    int x;
    int y;

    Point(int x, int y) : x{ x }, y{ y } { }
};

class Widget
{
public:
    std::string name;
    Widget(std::string name) : name{ name } { }
};

template <typename T>
struct DataHolder
{
    T value;

    DataHolder(T value) : value {value} { }
};

Why does this code compile:

auto compiles = std::make_unique<DataHolder<int>>(42);
auto alsoCompiles = std::make_unique<DataHolder<std::string>>("Hi");

and this code does not:

auto doesnt = std::make_unique<DataHolder<Point>>({20, 21});   // C2672 'std::make_unique': no matching overloaded function found    and    C7627   'initializer list': is not a valid template argument for '_Types'
auto alsoDoesnt = std::make_unique<DataHolder<Widget>>("Hello");   // C2664 'DataHolder<Widget>::DataHolder(T)': cannot convert argument 1 from 'const char [6]' to 'T'

How does std::make_unique<SomeWrapper<T>>() determines if T can be constructed within SomeWrapper? What makes int and std::string special in this case? How can I make my types work the same way?

2 Answers

Why does this code compile:

auto compiles = std::make_unique<DataHolder<int>>(42);
auto alsoCompiles = std::make_unique<DataHolder<std::string>>("Hi");

DataHolder<int> has a constructor taking int, and 42 is passed as the constructor argument to construct DataHolder<int>, which works fine (similarly as DataHolder<int>(42)).

DataHolder<std::string> has a constructor taking std::string, and "Hi" is passed as the constructor argument, it could convert to std::string implicitly, which works fine (similarly as DataHolder<std::string>("Hi")).

and this code does not:

auto doesnt = std::make_unique<DataHolder<Point>>({20, 21});
auto alsoDoesnt = std::make_unique<DataHolder<Widget>>("Hello");

std::make_unique<DataHolder<Point>>({20, 21}) doesn't work because braced-init-list like {20, 21} doesn't have type, template argument deduction for std::make_unique fails on it. This belongs to non deduced contexts.

  1. The parameter P, whose A is a braced-init-list, but P is not std::initializer_list, a reference to one (possibly cv-qualified), or a reference to an array:

DataHolder<Widget> has a constructor taking Widget, and "Hello" is a const char[6] which could decay to const char*, then two user-defined conversions are required, one from const char* to std::string, one from std::string to Widget, but only one user-defined conversion is allowed in one implicit conversion sequence. (Similarly DataHolder<Widget>("Hello") doesn't work either.)

If you pass Point and std::string directly, they would work fine.

auto doesnt = std::make_unique<DataHolder<Point>>(Point{20, 21});;
auto alsoDoesnt = std::make_unique<DataHolder<Widget>>(std::string{"Hello"});

As @YiFei suggested, you can make DataHolder's constructor template, and forward all the arguments by std::forward to the constructor of T to initialize the data member directly, i.e. work in the similar way as std::make_unique.

E.g.

template <typename... Args>
DataHolder(Args&&... value) : value {std::forward<Args>(value)...} {}

Note that std::make_unique<DataHolder<Point>>({20, 21}); still doesn't work for the reason above. But you can do std::make_unique<DataHolder<Point>>(20, 21); instead, 20 and 21 are forwared to the constructor of Point taking two ints and then works fine. Similarly for std::make_unique<DataHolder<Widget>>("Hello"), "Hello" is forwarded to the constructor of Widget taking std::string, "Hello" could convert to std::string implicitly and then works fine.

std::make_unique<DataHolder<Widget>>("Hello") requires two conversions, one from const char* to std::string and another to Widget. Only one implicit conversion is allowed.

std::make_unique<DataHolder<Point>>(20, 21) doesn't work because it's would be the same as calling DataHolder<Point>(20, 21) but DataHolder only takes a single argument.

Your first two examples only require a single or no implicit conversion so work.

To make these work you need DataHolder to forward its constructor arguments to value:

template <typename ...Args>
DataHolder(Args&& ... args) : value {std::forward<Args>(args)...} { }
Related