std::make_shared/std::make_unique 不使用列表初始化是有原因的吗?

Is there a reason why std::make_shared/std::make_unique don't use list initialization?

具体来说:直接列表初始化 (cppreference.com (3)).

std::make_shared 统一初始化 特性均在 C++11 中引入。所以我们可以在堆上分配对象时使用聚合初始化new Foo{1, "2", 3.0f}。这是一种直接初始化没有构造函数的对象的好方法,例如聚合,pods等

现实生活中的场景,例如在函数中声明 临时 结构,以有效地向 lambda 提供参数集变得非常普遍,根据我的经验:

void foo()
{
    struct LambdaArgs
    {
        std::string arg1;
        std::string arg2;
        std::string arg3;
    };

    auto args = std::make_shared<LambdaArgs>(LambdaArgs{"1", "2", "3"});

    auto lambda = [args] {
        /// ...
    };

    /// Use lambda
    /// ...
}

这里 auto args = std::make_shared<LambdaArgs>("1", "2", "3"); 应该不错,但行不通,因为 std::make_shared 通常实现为:

template<typename T, typename... Args>
std::shared_ptr<T> make_shared(Args && ...args)
{
    return std::shared_ptr<T>(new T(std::forward<Args>(args)...));
}

所以我们陷入了 auto args = std::make_shared<LambdaArgs>(LambdaArgs{"1", "2", "3"});

本应通过 std::make_shared 解决的问题对于没有构造函数的对象仍然存在。而且解决方法不仅不美观而且效率低下。

这是另一个疏忽还是有一些理由支持这个选择。具体来说,列表初始化解决方案中可以有哪些陷阱? std::make_unique 是后来在 C++14 中引入的,为什么它也遵循相同的模式?

The problem that was supposed to be solved with std::make_shared still persists for object without constructor.

否,问题不再存在。 make_shared 解决的主要问题是对象分配和智能指针取得所有权之间的潜在内存泄漏。它还能够为控制块删除一个额外的分配。

是的,不能使用直接初始化很不方便,但这从来不是make_shared声明的目标。

Specifically, what pitfalls can be in the list initialization solution?

使用列表初始化的所有典型陷阱。

例如non-initializer_list构造函数的隐藏。 make_shared<vector<int>>(5, 2) 是做什么的?如果您的答案是“构造 5 int 的数组”,那是绝对正确的...只要 make_shared 不使用列表初始化 。因为那会在你做的那一刻改变。

请注意,突然更改它会破坏现有代码,因为现在所有间接初始化函数都使用构造函数语法。所以你不能随意改变它并期望世界继续运转。

再加上这个案例的另一个独特之处:缩小问题:

struct Agg
{
  char c;
  int i;
};

您可以 Agg a{5, 1020}; 来初始化这个集合。但你永远做不到 make_shared<Agg>(5, 1020)。为什么?因为编译器可以保证文字 5 可以在不丢失数据的情况下转换为 char 。但是,当您像这样使用间接初始化时,文字 5 被模板推导为 int。并且编译器不能保证任何int都可以在不丢失数据的情况下转换为char。这称为“缩小转换”,在列表初始化中明确禁止。

您需要明确地将 5 转换为 char

标准库对此有一个问题:LWG 2089。虽然从技术上讲这个问题讨论的是 allocator::construct,但它应该同样适用于所有间接初始化函数,例如 make_X 和 C++17 的 any/optional/[ 的就地构造函数=28=].

why does it too follow same pattern?

它遵循相同的模式,因为拥有两个看起来几乎相同但具有完全不同的行为的不同功能并不是一件好事。


请注意,C++20 至少通过使构造函数样式的语法调用聚合初始化来解决此问题的聚合部分,如果初始化程序对于常规直接初始化而言是格式错误的。因此,如果 T 是某种聚合类型(没有用户声明的构造函数),并且 T(args) 不会调用 copy/move 构造函数(唯一采用没有用户类型的参数的构造函数-声明的构造函数可能有),那么参数将被用来尝试聚合初始化结构。

由于 allocator::construct 和其他形式的转发初始化默认为直接初始化,这将允许您通过转发初始化来初始化聚合。

如果不在调用站点明确使用 initializer_list,您仍然无法执行其他列表初始化操作。但这可能是最好的。