std::make_unique(和 emplace,emplace_back)对 initializer_list 参数的尴尬推论

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

假设我有这个结构:

struct position
{
  int x, y;
};

和另一个将其作为构造函数参数的 class:

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

我怎样才能得到简单的

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

上班?

目前,编译器试图通过 initializer_list<int> 匹配并调用 make_unique 的数组变体,这很愚蠢,因为 positioned 只有一个构造函数。 emplaceemplace_back 函数也会出现同样的问题。几乎任何将其可变参数模板参数转发给 class 的构造函数的函数似乎都表现出这种行为。

我知道我可以通过

解决这个问题
  1. positioned 两个 int 参数构造函数并在对 make_unique 的调用中删除 {},或者
  2. 明确指定 make_unique 的参数类型为 position{1,2}

两者似乎都过于冗长,在我看来(在 make_unique 实现中付出了一些努力),这可以在没有参数类型过度指定的情况下解决。

这是 make_unique 实现中可解决的缺陷,还是没有人应该关心的无法解决的、无趣的边缘案例?

问题是无法推导出像 {1, 2} 这样的初始化器:以下代码不起作用(有充分的理由:{1, 2} 的类型是什么?)。

template<typename U>
void foo(U args) { }

foo({1, 2});

make_unique 只是这个主题的一个更复杂的变体。

这里详细解释了根本原因:initializer_list and template type deduction

据我所知,最实用的方法可能是去掉大括号,并添加构造函数以离散地获取参数:

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);
}

如果 position 有多个构造函数,您可能想为 positioned 创建一个可变参数模板构造函数,以获取一些任意参数并将它们传递给 position' 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
}

通过这种方式,参数从 make_unique 转发到 positioned 再到 position。这确实至少在效率方面也提供了一些潜在的优势——而不是使用参数创建一个临时对象,然后将其传递给初始化底层对象,而是将原始对象直接传递(引用)给构造函数底层对象,所以我们只构建一次,就地。

请注意,这确实为我们提供了相当多的多功能性。例如,假设 positioned 本身是一个模板,而底层 position 是一个模板参数:

#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);
}

兼容性:我相信这需要 C++ 14 或更新版本,因为那时 make_unique 获得了它的 forwarding/variadic ctor。

函数模板参数推导在给定花括号初始化列表时不起作用;它仅基于实际表达式起作用。

还应注意,positioned 不能从 {1, 2} 列表初始化。这将尝试调用两个参数的构造函数,而 positioned 没有这样的构造函数。您需要使用 positioned({1, 2})positioned{{1, 2}}.

因此,一般的解决方案是 make_unique 以某种方式神奇地为它正在构造的类型重现每个可能的构造函数的签名。这显然不是此时在 C++ 中做的合理事情。

另一种方法是使用 lambda 来创建对象,并编写另一种 make 函数,使用 C++17 的保证省略规则将返回的纯右值应用于内部 new 表达式:

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});});

您甚至可以放弃 typename T 参数:

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});});