本周小贴士#177:可赋值性与数据成员类型

作为TotW#177最初发表于2020年4月6日

由Titus Winters创作

在实现类型时,应先确定类型设计。将API优先于实现细节。这种情况的一个常见例子是类型可赋值性和数据成员的限定符之间的权衡。

决定如何表示数据成员

假设你正在编写一个City类,并讨论如何表示其成员变量。你知道它只存在很短的时间,表示城市作为时间快照,因此像人口、名称和市长等东西可以被视为const - 我们不会在程序中多年多年地使用同一对象,因此我们不需要考虑人口的变化、新的人口普查结果或选举结果。

我们应该像这样定义成员吗?

 private:
  const std::string city_name_;
  const Person mayor_;
  const int64_t population_;

为什么或为什么不?

常见的建议是“是的,让它们成为const”,其中的关键在于“嗯,对于给定的城市,这些值不会改变,因此由于可以是const的所有内容都应该是const,使它们成为const将更容易使类的维护者避免意外修改这些字段。”

这忽略了一个极其重要的问题:City是什么类型?它是一个值还是一堆业务逻辑?它可以复制、移动或不可复制吗?对于City的整个操作集可能会受到单个成员变量是const的影响,而这往往是一个不好的权衡。

具体来说,如果你的类有const成员,它就不能被赋值(无论是通过复制赋值还是移动赋值)。语言理解这一点:如果你的类型有一个const成员,复制赋值和移动赋值操作符将不会被合成。你仍然可以复制(或移动)构造这样一个对象,但你在构造后不能以任何方式改变它(甚至“仅仅”从同一类型的另一个对象复制)。即使你编写自己的赋值运算符,你很快就会发现你(显然)无法覆盖这些const成员。

因此,问题可能变成“我们应该更喜欢const成员还是赋值操作?”然而,即使是这个问题也是具有误导性的,因为两者都由一个重要的问题“City是什么类型?”来回答。如果它的预期是一个值类型,那么就明确了API(包括赋值操作),而API在一般情况下优先于实现关注。

在API设计决策优先于实现细节选择方面非常重要:在一般情况下,受到类型API的影响的工程师比类型维护人员更多。也就是说,有更多的用户使用类型而不是维护该类型的人,因此优先考虑影响用户的设计选择。即使你认为这种类型永远不会被除维护它的团队以外的任何人使用,软件工程是关于接口设计和抽象的 - 我们应该优先考虑良好的接口设计。

引用成员

同样的推理也适用于将引用存储为数据成员。即使我们知道该成员必须为非空,通常仍然更倾向于为值类型存储T*,因为引用不可重新绑定。也就是说,我们无法重新指向T&,对这样的成员进行任何修改都会修改底层的T。

考虑std::vector的实现。在任何std::vector实现中,几乎肯定会有一个T*数据成员,指向分配的内存。我们从std::vector的规范中知道,这样的分配通常是有效的(除了可能为空的向量)。总是具有分配的实现可以将其作为T&,对吗?(是的,我在这里忽略了数组和偏移量。)

显然不行。std::vector是一个值类型,它是可复制和可赋值的。如果将分配与引用第一个成员而不是指向第一个成员的指针一起存储,我们将无法移动分配的存储,并且不清楚如何在正常调整大小时更新数据。我们聪明地告诉其他维护者“这个值是非空的”的方法将妨碍提供用户所需的API。希望清楚地表明这是错误的权衡。

不可复制/可赋值类型

当然,如果您的类型设计选择表明City(或您正在考虑的任何类型)应该是不可复制的,那么对您的实现的约束就会少得多。一个类持有const或引用成员并不对错,只有当这些实现决策限制或破坏该类呈现的接口时才会引起关注。如果您已经经过深思熟虑地做出了您的类型不需要复制的决定,那么您可以合理地对类的数据成员做出不同的选择。(但是请参见提示#116,了解有关参数生命周期和引用存储的更多想法和陷阱。)

不寻常的情况:不可变类型

有一种有用但不寻常的设计可能需要强制使用const成员:有意不可变的类型。这种类型的实例在构造后是不可变的:没有改变方法,没有赋值运算符。这些类型相当罕见,但有时可能很有用。特别是,这种类型本质上是线程安全的,因为没有改变操作。这种类型的对象可以在线程之间自由共享,不必担心数据竞争或同步。然而,作为交换,这些对象可能具有显着的运行时开销,源于需要不断复制它们。甚至不可变性也防止这些对象被有效地移动。

通常最好将类型设计为可变的但仍然与线程兼容,而不是依赖于通过不可变性实现线程安全。您的类型的用户通常更能够根据情况逐个判断可变性的好处。不要强迫他们在没有非常强的证据表明您的用例不寻常的设计选择周围工作。

建议

  • 在考虑实现细节之前决定您的类型的设计。
  • 值类型很常见且推荐。业务逻辑类型通常是不可复制的。
  • 不可变类型有时很有用,但有正当理由的情况相当罕见。
  • 优先考虑API设计和用户需求,而不是维护者(通常较小的)问题。
  • 在构建值类型或仅移动类型时,避免使用const和引用数据成员。

你可能感兴趣的:(C++,Tips,of,the,Week,c++)