make_unique 和完美转发

226
为什么标准的C++11库中没有std::make_unique函数模板?我发现这个问题非常困扰。
std::unique_ptr<SomeUserDefinedType> p(new SomeUserDefinedType(1, 2, 3));

有点啰嗦。以下的表述不是更好吗?
auto p = std::make_unique<SomeUserDefinedType>(1, 2, 3);

这样可以很好地隐藏new,并且只提到了类型一次。
无论如何,这是我对make_unique实现的尝试:
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

我花了相当长的时间才使std :: forward编译通过,但我不确定它是否正确。它是正确的吗?std::forward<Args>(args)... 究竟意味着什么?编译器对此做出了什么样的处理?


1
几乎可以肯定我们之前讨论过这个问题... 还要注意 unique_ptr 带有第二个模板参数,你应该以某种方式进行适配,这与 shared_ptr 不同。 - Kerrek SB
5
@Kerrek:我认为对make_unique进行参数化使用自定义删除器是没有意义的,因为它显然是通过普通的new进行分配的,因此必须使用普通的delete :) - fredoverflow
1
@Fred:没错。因此,提出的make_unique将局限于new 分配…… 如果你想写它,那很好,但我可以理解为什么这样的东西不是标准的一部分。 - Kerrek SB
4
实际上,我喜欢使用make_unique模板,因为std::unique_ptr的构造函数是显式的,所以从函数返回unique_ptr很冗长。此外,我更喜欢使用auto p = make_unique<foo>(bar, baz)而不是std::unique_ptr<foo> p(new foo(bar, baz)) - Alexandre C.
5
make_unique将在C++14中出现,详见http://isocpp.org/blog/2013/04/trip-report-iso-c-spring-2013-meeting。 - stefan
显示剩余2条评论
6个回答

161

C++ 标准化委员会主席 Herb Sutter 在他的博客上写道:

C++11 没有包括 make_unique 部分是疏忽,未来它几乎肯定会被添加。

他还给出了一个与 OP 给出的相同的实现。

编辑:std::make_unique现在是C++14的一部分。


2
一个make_unique函数模板本身并不能保证调用的安全性,它依赖于约定,即调用者使用它。相比之下,严格的静态类型检查(这是C++和C之间的主要区别)建立在通过类型来强制安全性的思想上。为此,make_unique可以简单地成为一个类,而不是函数。例如,请参阅我2010年5月的博客文章。它也链接到了Herb的博客讨论中。 - Cheers and hth. - Alf
1
@DavidRodríguez-dribeas:阅读Herb的博客以了解异常安全问题。忘记“不设计为派生”的说法,那只是废话。我现在认为,与其将make_unique作为一个类,不如将其作为一个生成make_unique_t的函数,原因是最恶劣的解析存在问题 :-)。 - Cheers and hth. - Alf
1
@Cheersandhth. - Alf:也许我们读的不是同一篇文章,因为我刚刚阅读的那篇文章明确表示make_unique提供了强异常保证。或者你混淆了工具和使用方法,在这种情况下,没有函数是异常安全的。考虑void f(int *, int *){},显然提供了no throw保证,但是按照你的推理,它不是异常安全的,因为它可能被误用。更糟糕的是,void f(int, int){}也不是异常安全的!:typedef unique_ptr<int> up; f(*up(new int(5)), *up(new int(10)))... - David Rodríguez - dribeas
2
@Cheersandhth.-Alf:我问了关于上述实现的make_unique中的异常问题,你给我指了一篇Sutter的文章。我的谷歌搜索结果显示,该文章指出make_unique提供了强异常保证,这与你上面的说法相矛盾。如果你有其他文章,我很感兴趣阅读。所以我的最初问题仍然存在:如何证明make_unique(如上定义)不是异常安全的?(顺便说一句:是的,我认为在可以应用的地方,make_unique可以提高异常安全性) - David Rodríguez - dribeas
3
我不打算使用一个不太安全的 make_unique 函数。首先,我不认为这个函数存在不安全的问题,而且我也不认为添加额外的类型会让它更加“安全”。我所知道的是,我有兴趣了解这个实现可能存在的问题(我并没有看到任何问题),以及替代实现如何解决这些问题。make_unique 依赖哪些“规则”?如何使用类型检查来确保安全性?这是我想要得到答案的两个问题。 - David Rodríguez - dribeas
显示剩余7条评论

80
很好,但是Stephan T. Lavavej(更为人所知的是STL)提供了一个更好的解决方案用于make_unique,它能正确处理数组版本。
#include <memory>
#include <type_traits>
#include <utility>

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

template <typename T, typename... Args>
std::unique_ptr<T> make_unique_helper(std::true_type, Args&&... args) {
   static_assert(std::extent<T>::value == 0,
       "make_unique<T[N]>() is forbidden, please use make_unique<T[]>().");

   typedef typename std::remove_extent<T>::type U;
   return std::unique_ptr<T>(new U[sizeof...(Args)]{std::forward<Args>(args)...});
}

template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
   return make_unique_helper<T>(std::is_array<T>(), std::forward<Args>(args)...);
}

你可以在他的Core C++ 6视频中看到这个。

STL的make_unique更新版本现在可作为N3656获得。这个版本被采纳进入了C++14草案。


make_unique被Stephen T Lavalej提议加入下一个std更新。 - tominator
这是他谈论添加它的链接。 http://channel9.msdn.com/Series/C9-Lectures-Stephan-T-Lavavej-Core-C-/STLCCSeries6#c634889372900373011 - tominator
为什么要对无关紧要的编辑进行修改,Xeo?它原本就很好了。那段代码正是Stephen T. Lavalej编写的,他在维护std库的dinkumware公司工作。你已经在那个墙上发表过评论了,所以你应该知道。 - tominator
3
make_unique 的实现应该放在头文件中。头文件不应该导入命名空间 (请参考 Sutter/Alexandrescu 的 "C++ 编程规范" 一书中的第59条)。Xeo 的更改有助于避免鼓励不良的编码习惯。 - Bret Kuhns
不幸的是,VC2010甚至我认为连VC2012都不支持,两者都不支持可变模板参数。 - zhaorufei
这个实现现在已经在VC2013中,该版本已经发布。 它也将包含在其他允许C++13的最新版本的C++ IDE中。 - tominator

19

虽然你可以编写自己的帮助程序,但我认为提供库中的make_shared<T>的主要原因是它实际上创建了与shared_ptr<T>(new T)不同的内部共享指针类型,这是不同分配的,并且没有其他方法可以实现这一点而不使用专用的帮助程序。

另一方面,您的make_unique包装器只是一个围绕new表达式的语法糖,因此虽然它可能看起来很舒服,但它并没有为表格带来任何新东西。 更正:事实上,这并不正确:在调用函数void f(std::unique_ptr<A> &&, std::unique_ptr<B> &&)的情况下,使用函数调用来包装new表达式提供异常安全性。如果两个未排序的new表达式之一由于异常而失败,则另一个可能会泄漏资源。至于为什么标准中没有make_unique:这只是被遗忘了。(偶尔会发生这种情况。标准中也没有全局的std::cbegin,即使应该有一个。)

还要注意,unique_ptr需要第二个模板参数,您应该以某种方式允许它;这与shared_ptr不同,后者使用类型擦除来存储自定义删除器,而不将其作为类型的一部分。


1
@FredOverflow:共享指针是一个比较复杂的类;它在内部保留了一个多态引用控制块,但有几种不同类型的控制块。shared_ptr < T > (new T)使用其中之一,而make_shared < T >()使用不同的控制块。允许这种情况是件好事,而make-shared版本在某种程度上是你可以得到的最轻量级的共享指针。 - Kerrek SB
15
shared_ptr 在创建时分配了一个动态内存块来管理计数和“释放器”操作。如果您明确传递指针,则需要创建一个新的内存块;如果使用 make_shared,它可以将您的对象和卫星数据捆绑在单个内存块中(一个 new),从而实现更快的分配/释放、更少的碎片化和(通常)更好的缓存行为。 - Matthieu M.
2
我认为我需要重新观看shared_ptr and friends... - fredoverflow
4
“Your make_unique包装器只是对new表达式的语法糖而已,虽然它看起来很好,但并没有带来什么新东西。”这种说法是不正确的。它提供了异常安全函数调用的可能性。但它并不能提供保证,为了达到这一点,它需要成为一个类,这样形式参数就可以声明为该类(Herb将其描述为Opt-in和Opt-out之间的区别)。 - Cheers and hth. - Alf
1
@Cheersandhth.-Alf:是的,你说得对。我现在意识到了这一点。我会编辑答案。 - Kerrek SB
显示剩余3条评论

19

std::make_shared并不仅仅是std::shared_ptr<Type> ptr(new Type(...));的简写。它还做了一件你没有它就无法做到的事情。

为了达到其目的,std::shared_ptr除了保存指针本身的存储空间外,还必须分配一个跟踪块。然而,由于std::make_shared分配了实际对象,因此可能会在同一块内存中分配对象和跟踪块。

因此,std::shared_ptr<Type> ptr = new Type(...);将需要两个内存分配(一个用于new,一个用于std::shared_ptr跟踪块),而std::make_shared<Type>(...)只需分配一个内存块。

对于许多潜在的std::shared_ptr用户来说,这是非常重要的。而std::make_unique唯一做的事情就是更加方便,除此之外没有其他。


2
不是必需的。提示了,但不是必须的。 - Puppy
3
不仅仅是为了方便起见,它在某些情况下也可以提高异常安全性。请参考Kerrek SB的回答。 - Piotr99

13

在C++11中,...也被用于模板代码中的“可变参数展开”。

要求您将其用作包含未展开的参数包的表达式的后缀,并且它将简单地将表达式应用于包中的每个元素。

例如,根据您的示例构建:

std::forward<Args>(args)... -> std::forward<int>(1), std::forward<int>(2),
                                                     std::forward<int>(3)

std::forward<Args...>(args...) -> std::forward<int, int, int>(1,2,3)

我认为后者是不正确的。

此外,未展开的参数包可能无法传递给函数。对于模板参数的参数包,我不确定。


1
很高兴看到这个被明确说明了。为什么不也包括可变参数模板参数呢? - Kerrek SB
1
我认为 forward 实际上总是需要一个模板参数,不是吗? - Kerrek SB
嗯,我不是100%确定,但如果您没有指定参数,可能会导致您无法得到您想要的结果--即参数可能被错误地推导。它仍然可以编译,但例如,您可能会强制rvalue变为lvalue。我不确定。 - Kerrek SB
@Matthieu:如果您在std::forward中不指定模板参数,它将无法编译。这是有意设计的错误方式。 - Howard Hinnant
1
@Howard:没错!强制实例化会触发警告(http://ideone.com/GDNHb)。 - Matthieu M.
显示剩余6条评论

5

受Stephan T. Lavavej实现的启发,我认为有一个支持数组范围的make_unique可能很好,它在github上,我希望能得到您的评论。它允许您这样做:

// create unique_ptr to an array of 100 integers
auto a = make_unique<int[100]>();

// create a unique_ptr to an array of 100 integers and
// set the first three elements to 1,2,3
auto b = make_unique<int[100]>(1,2,3); 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接