Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 211
rubenvb Avatar asked Dec 31 '17 15:12

rubenvb


2 Answers

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});});
like image 110
Nicol Bolas Avatar answered Nov 15 '22 19:11

Nicol Bolas


As far as I can see, the most practical way to do this is probably to get rid of the braces, and add constructors to take the arguments discretely:

struct position
{
  int x, y;

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

class positioned
{
public:
  positioned(int x, int y) : pos(x, y) {}
private:
  position pos;
};

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

If position had more than one ctor, you'd probably want to create a variadic template ctor for positioned to take some arbitrary parameters and pass them through to position's ctor(s).

struct position
{
  int x, y;

  position(int x, int y) : x(x), y(y) {}
  position(int b) : x(b), y(b) {} // useless--only to demo a different ctor
};

class positioned
{
public:
  template <class... Args>
  positioned(Args&&... a) : pos(std::forward<Args>(a)...) {}
private:
  position pos;
};

int main() { 
    auto bla = std::make_unique<positioned>(1,2); // use 1st ctor
    auto bla2 = std::make_unique<positioned>(1); // use 2nd ctor
}

This way the arguments get forwarded through from make_unique to positioned to position. This does give at least some potential advantage in efficiency as well--instead of using the arguments to create a temporary object, which is then passed to initialize the underlying object, it passes (references to) the original objects directly to the ctor for the underlying object, so we only construct it once, in-place.

Note that this does give us a fair amount of versatility. For example, let's assume positioned were itself a template, and the underlying position a template argument:

#include <memory>

struct position2
{
  int x, y;

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

struct position3 {
    int x, y, z;

    position3(int x, int y, int z) : x(x), y(y), z(z) {}
};

template <class Pos>
class positioned
{
public:
  template <class... Args>
  positioned(Args&&... a) : pos(std::forward<Args>(a)...) {}
private:
  Pos pos;
};

int main() { 
    auto bla = std::make_unique<positioned<position2>>(1,2);
    auto bla2 = std::make_unique<positioned<position3>>(1, 2, 3);
}

Compatibility: I believe this requires C++ 14 or newer, since that's when make_unique got its forwarding/variadic ctor.

like image 38
Jerry Coffin Avatar answered Nov 15 '22 18:11

Jerry Coffin