每周技巧 #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。)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Widget {
 public:
  static std::unique_ptr<Widget> Make() {
    return std::make_unique<Widget>(GenerateId());
  }

 private:
  Widget(int id) : id_(id) {}
  static int GenerateId();

  int id_;
}

当你尝试编译时,会得到类似这样的错误:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
error: calling a private constructor of class 'Widget'
    { return unique_ptr<_Tp>(new _Tp(std::forward<_Args>(__args)...)); }
                                 ^
note: in instantiation of function template specialization
'std::make_unique<Widget, int>' requested here
    return std::make_unique<Widget>(GenerateId());
                ^
note: declared private here
  Widget(int id) : id_(id) {}
  ^

虽然 Make 可以访问这个私有构造函数,但 std::make_unique 不行!注意,这个问题也可能出现在 friend 上。例如,Widget 的某个友元在使用 std::make_unique 构造 Widget 时也会遇到同样的问题。

建议

我们推荐下面任一替代方案:

  • 使用 newabsl::WrapUnique,但解释你的选择。例如:
1
2
    // 使用 `new` 来访问非公开构造函数。
    return absl::WrapUnique(new Widget(...));
  • 考虑这个构造函数是否可以安全地公开暴露。如果可以,就把它设为 public,并记录何时适合直接构造。

在许多情况下,把构造函数标记为 private 是过度工程。这些情况下,最佳解决方案是把构造函数标记为 public,并记录其正确用法。不过,如果你的构造函数确实需要是 private(例如为了保证类不变量),那就使用 newWrapUnique

为什么不能直接把 std::make_unique(或 absl::make_unique)设为友元?

你可能会想把 std::make_unique(或 absl::make_unique)设为友元,让它访问你的私有构造函数。这是个坏主意,原因有几个。

首先,虽然完整讨论友元实践超出了本技巧范围,但一个很好的经验法则是“不要远距离交友”。否则,你就在创建一个由非所有者维护的竞争性友元声明。另见风格指南中的建议

其次,注意你依赖了 make_unique 的一个实现细节,也就是它会直接调用 new。如果它被重构成间接调用 new,例如在使用 C++14 及更新版本的构建模式中,absl::make_uniquestd::make_unique 的别名,那么这个友元声明就没用了。

最后,把 make_unique 设为友元后,你已经允许任何人用这种方式创建你的对象;那为什么不干脆把构造函数声明为 public,从根本上避免这个问题呢?

std::shared_ptr 怎么办?

对于 std::shared_ptr,情况有所不同。没有 absl::WrapShared,而类似写法 std::shared_ptr<T>(new T(...)) 会涉及两次分配,std::make_shared 则可以只做一次。如果这个差异很重要,可以考虑passkey idiom:让构造函数接收一个只有特定代码才能创建的特殊令牌。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Widget {
  class Token {
   private:
    explicit Token() = default;
    friend Widget;
  };

 public:
  static std::shared_ptr<Widget> Make() {
    return std::make_shared<Widget>(Token{}, GenerateId());
  }

  Widget(Token, int id) : id_(id) {}

 private:
  static int GenerateId();

  int id_;
};

注意,我们提供了一个 explicit 默认化构造函数。C++17 及更早版本需要这样做;否则 Widget::Token 会是聚合,并且可以通过 {} 进行聚合初始化,从而实际绕过 private 访问。

关于 passkey idiom 的完整讨论,可参阅下面任一文章:

每周技巧 #132:避免冗余的 map 查找

上一节

每周技巧 #135:测试契约,而不是实现

下一节