每周小贴士#142:多参数构造函数和explicit

Posted -飞鹤-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了每周小贴士#142:多参数构造函数和explicit相关的知识,希望对你有一定的参考价值。

作为TotW#142最初发表于2018年1月29日

由James Dennett创作

“显式比隐式更好”——PEP 20

长话短说

多数构造函数应该是explicit

介绍

在 C++11 之前,explicit 关键字仅对可以使用单个参数调用的构造函数有意义,并且我们的风格指南要求将其用于此类构造函数,以免它们充当“转换构造函数”。 该要求不适用于多参数构造函数。 事实上,风格指南曾经不鼓励对多参数构造函数使用显式,因为它没有任何意义。 情况不再如此。

在 C++11 中,显式对于从花括号列表进行复制初始化变得有意义,例如在使用 f(1, 2) 调用函数 void f(std::pair<int, int>) 或初始化变量bad时, 使用 std::vector<char> bad = “hello”, “world”;。

等一下! 在最后一个示例中,类型不匹配。 那不能编译,可以吗? std::vector<std::string> good = “hello”, “world”; 这是合理的,但一个 std::vector<char> 不能容纳两个 std::strings。 然而它确实可以编译(或者至少它可以与所有当前的 C++ 编译器一起编译)。 那是怎么回事? 在我们详细讨论了显式之后,让我们稍后再谈。

改变类型但不改变值的构造函数

编译器可以调用未标记为显式的构造函数来创建值,而无需提及类型的名称。 当我们已经有了我们想要的值但类型不完全匹配时,那也很好了——我们有一个 const char[],我们可能需要一个 std::string,或者我们有两个 std::strings,我们想要一个 std::vector<std::string>,或者我们有一个 int 并且我们想要一个 BigNum。 简而言之,如果转换前后的值基本相同,则效果很好。

// The coordinates of a point in the Cartesian plane, w.r.t. some basis.
class Coordinate2D 
 public:
  Coordinate2D(double x, double y);
  // ...
;

// Computes the Euclidean norm of a given point `p`.
double EuclideanNorm(Coordinate2D p);

// Uses of the non-explicit constructor:
double norm = EuclideanNorm(3.0, 4.0);  // passing a function argument
Coordinate2D origin = 0.0, 0.0;         // initializing with `=`
Coordinate2D Translate(Coordinate2D p, Vector2D v) 
  return p.x() + v.x(), p.y() + v.y();  // returning a value from a function

通过未显式声明的构造函数Coordinate2D(double, double),我们允许将 3.0, 4.0 传递给接受 Coordinate2D 参数的函数。 鉴于 3.0, 4.0 对此类对象来说是一个完全合理的值,因此这种可供性不会引起混淆。

做更多事的构造函数

如果构造函数的输出与其输入的值不同,或者它可能具有先决条件,则隐式调用构造函数并不是一个好主意。

考虑一个带有构造函数 Request(Server*, Connection*) 的 Request 类。 请求对象的值“是”服务器和连接是没有意义的——只是我们可以创建一个使用它们的请求。 可以从 server, connection 构造许多语义不同的类型,例如响应。 这样的构造函数应该是显式的,这样我们就不能将 server, connection 传递给接受请求(或响应)参数的函数。 在这种情况下,将构造函数标记为显式可以通过要求在实例化目标类型时对其进行命名,从而使代码对读者更清晰,并有助于避免由意外转换引起的错误。

// A line is defined by two distinct points.
class Line 
 public:
  // Constructs the line passing through the given points.
  // REQUIRES: p1 != p2
  explicit Line(Coordinate2D p1, Coordinate2D p2);

  // Determines whether this line contain a given point `p`.
  bool ContainsPoint(Coordinate2D p) const;
;

Line line(0, 0, 42, 1729);

// Computes the gradient of `line`.  Returns an infinite value if `line` is
// vertical.
double Gradient(const Line& line);

通过将构造函数 Line(Coordinate2D, Coordinate2D) 声明为显式,我们可以防止代码将不相关的点传递给 Gradient,而无需先特意将它们转换为 Line 对象。 一条线的“值”不是两点,也不是任何两点定义一条线。

作为另一个例子,在 std::unique_ptr 中使用从原始指针显式进行所有权转移可防止此处出现双重删除错误:

std::vector<std::unique_ptr<int>> v;
int* p = new int(-1);
v.push_back(p);  // error: cannot convert int* to std::unique_ptr<int>
// ...
v.push_back(p);

要传递 std::unique_ptr ,程序员必须显式创建一个,这为读者留下了所有权正在转移的视觉线索。 显式构造函数有助于记录原始指针必须拥有其目标的约束。

推荐

  • 复制构造函数和移动构造函数永远不应该是显式的。
  • 使构造函数显式,除非它的参数“是”新创建的对象的值。 (注意:Google 风格指南目前要求所有单参数构造函数都是显式的)。
    -特别是,标识(地址)与值相关的类型的构造函数应该是显式的。
  • 对值施加额外约束(即具有先决条件)的构造函数应该是显式的。 有时这些更好地实现为工厂函数(参见贴士#42:首选工厂函数而不是初始化方法)。

结束语

这个技巧可以看作是贴士#88:初始化:=、() 和 的反面,它建议我们在从“预期的文字值”初始化时使用复制初始化语法(带 =)。 本技巧的建议是仅当贴士88 说要使用复制初始化时才省略显式(否则将被显式关键字禁止)。

最后,一句警告:C++ 标准库并不总能做到这一点。 回到我们的示例(错误地声明了一个 std::vector<char> 而不是一个字符串容器):

std::vector<char> bad = "hello", "world";

我们发现 std::vector 有一个带有一对迭代器的模板化“范围”构造函数,它们在这里匹配并将参数类型推导出为 const char*。 如果应用了本贴士中的建议,则该构造函数将是显式的,因为 std::vector<char> 的值不是两个迭代器(而是一个字符序列)。 事实上,explicit 被省略了,并且这个示例代码给出了未定义的行为,因为第二个迭代器无法从第一个迭代器访问。

以上是关于每周小贴士#142:多参数构造函数和explicit的主要内容,如果未能解决你的问题,请参考以下文章

本周小贴士#74:委托和继承构造函数

每周小贴士#158:Abseil关联窗口和contains()

每周小贴士#153:不要使用using指令

本周小贴士#134:make_unique与私有构造函数

每周小贴士#152:AbslHashValue和你

每周小贴士#65:就地安放