每周技巧 #134:`make_unique` 和私有构造函数
本节阅读量:本文翻译自 Abseil 官网的 Tip of the Week #134: make_unique and private Constructors。
原文最初作为 TotW #134 发布于 2017 年 5 月 10 日。
作者:Yitzhak Mandelbaum,Google 工程师
更新于 2020 年 4 月 6 日。
快捷链接:abseil.io/tips/134
所以,你读了 技巧 #126,准备把 new 抛在身后。一切都很顺利,直到你尝试用 std::make_unique 构造一个带私有构造函数的对象,然后编译失败。我们来看一个具体例子,理解哪里出了问题。然后再讨论一些解决方案。
示例:制造 Widget
你正在定义一个表示 widget 的类。每个 widget 都有一个标识符,而这些标识符受到某些约束。为了确保这些约束总是满足,你把 Widget 类的构造函数声明为私有,并向用户提供一个工厂函数 Make,用于生成带有合适标识符的 widget。(为什么工厂函数优于初始化方法,见 技巧 #42。)
|
|
当你尝试编译时,会得到类似这样的错误:
|
|
虽然 Make 可以访问这个私有构造函数,但 std::make_unique 不行!注意,这个问题也可能出现在 friend 上。例如,Widget 的某个友元在使用 std::make_unique 构造 Widget 时也会遇到同样的问题。
建议
我们推荐下面任一替代方案:
- 使用
new和absl::WrapUnique,但解释你的选择。例如:
|
|
- 考虑这个构造函数是否可以安全地公开暴露。如果可以,就把它设为 public,并记录何时适合直接构造。
在许多情况下,把构造函数标记为 private 是过度工程。这些情况下,最佳解决方案是把构造函数标记为 public,并记录其正确用法。不过,如果你的构造函数确实需要是 private(例如为了保证类不变量),那就使用 new 和 WrapUnique。
为什么不能直接把 std::make_unique(或 absl::make_unique)设为友元?
你可能会想把 std::make_unique(或 absl::make_unique)设为友元,让它访问你的私有构造函数。这是个坏主意,原因有几个。
首先,虽然完整讨论友元实践超出了本技巧范围,但一个很好的经验法则是“不要远距离交友”。否则,你就在创建一个由非所有者维护的竞争性友元声明。另见风格指南中的建议。
其次,注意你依赖了 make_unique 的一个实现细节,也就是它会直接调用 new。如果它被重构成间接调用 new,例如在使用 C++14 及更新版本的构建模式中,absl::make_unique 是 std::make_unique 的别名,那么这个友元声明就没用了。
最后,把 make_unique 设为友元后,你已经允许任何人用这种方式创建你的对象;那为什么不干脆把构造函数声明为 public,从根本上避免这个问题呢?
std::shared_ptr 怎么办?
对于 std::shared_ptr,情况有所不同。没有 absl::WrapShared,而类似写法 std::shared_ptr<T>(new T(...)) 会涉及两次分配,std::make_shared 则可以只做一次。如果这个差异很重要,可以考虑passkey idiom:让构造函数接收一个只有特定代码才能创建的特殊令牌。例如:
|
|
注意,我们提供了一个 explicit 默认化构造函数。C++17 及更早版本需要这样做;否则 Widget::Token 会是聚合,并且可以通过 {} 进行聚合初始化,从而实际绕过 private 访问。
关于 passkey idiom 的完整讨论,可参阅下面任一文章: