Search code examples
c++c++14unique-ptrinitializer-listc++17

std::make_unique's (and emplace, emplace_back's) awkward deduction for initializer_list arguments


Say I have this struct:

struct position
{
  int x, y;
};

and another class that takes this as constructor argument:

class positioned
{
public:
  positioned(position p) : pos(p) {}
private:
  position pos;
};

How can I get the simple

auto bla = std::make_unique<positioned>({1,2});

to work?

Currently, the compiler tries to match through the initializer_list<int> and invoke the array variant of make_unique, which is silly, because positioned has only one constructor. The same issue arises for emplace and emplace_back functions. Pretty much any function that forwards its variadic template arguments to a class's constructor seems to exhibit this behaviour.

I understand I can resolve this by

  1. giving positioned a two int argument constructor and dropping the {} in the call to make_unique, or
  2. explicitly specifying the type of the argument to make_unique as position{1,2}.

Both seem overly verbose, as it seems to me (with some effort in the make_unique implementation), this can be resolved without this overspecification of the argument type.

Is this a resolvable defect in the make_unique implementation or is this an unresolvable, uninteresting edge-case no one should care about?


Solution

  • Function template argument deduction does not work when being given a braced-init-list; it only works based on actual expressions.

    It should also be noted that positioned cannot be list initialized from {1, 2} anyway. This will attempt to call a two argument constructor, and positioned has no such constructor. You would need to use positioned({1, 2}) or positioned{{1, 2}}.

    As such, the general solution would be to have make_unique somehow magically reproduce the signature of every possible constructor for the type it is constructing. This is obviously not a reasonable thing to do in C++ at this time.

    An alternative would be to use a lambda to create the object, and write an alternative make function use C++17's guaranteed elision rules to apply the returned prvalue to the internal new expression:

    template<typename T, typename Func, typename ...Args>
    std::unique_ptr<T> inject_unique(Func f, Args &&...args)
    {
      return std::unique_ptr<T>(new auto(f(std::forward<Args>(args)...)));
    }
    
    auto ptr = inject_unique<positioned>([]() {return positioned({1, 2});});
    

    You can even ditch the typename T parameter:

    template<typename Func, typename ...Args>
    auto inject_unique(Func f, Args &&...args)
    {
      using out_type = decltype(f(std::forward<Args>(args)...));
      return std::unique_ptr<out_type>(new auto(f(std::forward<Args>(args)...)));
    }
    
    auto ptr = inject_unique([]() {return positioned({1, 2});});