Google C++每周贴士 #177: 可赋值性与数据成员类型

(原文链接:https://abseil.io/tips/177 译者:clangpp@gmail.com)

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

做类型的实现,先要决定类型的设计。先考虑API,后考虑实现细节。这件事的一个常见例子是,类型的可赋值性与数据成员限定符之间的权衡。

决定如何呈现数据成员

设想你正在写一个City类,然后正在探讨如何呈现它的数据成员。你知道它生存期很短,表示城市某个时刻的快照,所以诸如人口、名称和市长都可以被当做const——我们不会在某个程序里把同一个对象用上好几年,所以我们不用考虑人口、新的普查结果或者选举结果的变化。

我们是否应该有这样的成员?

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

为什么,或者为什么不?

通常的建议“对,给它搞成const”背后的思路是“你看,一个给定的City的这些属性都不会改变,又因为所有可以是const的东西都应该是const,搞成const吧。”这样可以让维护者的日子更好过,不至于不小心修改了这些属性。

这里面漏掉了一个至关重要的考量:City是什么样的类型?它是个值吗?还是一整套业务逻辑?它被期待是可复制的,只能移动的,还是不可复制的?某个成员是否被搞成const可能会直接影响City(作为整体)可以高效写出的操作集合,而这通常是桩很差的买卖。

特别地说,如果你的类里有const成员,那它就不能被赋值(不论是拷贝赋值还是移动赋值)。语言认定:如果你的类型有const成员,拷贝赋值和移动赋值操作符不会被生成。你仍然可以拷贝(或移动) 构造 这样的对象,但在构造(即使“仅仅”是通过复制同类型的其他对象)完成后你就不能修改它了。即使你自己实现赋值运算符,你也会马上发现你(显然)不能改写这些const成员。

那么有没有可能把问题改为“const成员或赋值操作,哪个更好?”遗憾的是,就算这样还是容易被误导,因为二者都可以被一个 重要 的问题来回答,“City是什么样的类型?”如果它应该是一个 值类型 ,那就限定了(包括赋值操作在内的)API,而API通常优先于实现考量。

考虑API设计决策优先于考虑实现细节的选择是很重要的:通常情况下,API影响到的工程师要比类型实现影响到的工程师要多。也就是说,一个类型的用户要比其维护者更多,所以影响用户的设计选择应该比影响实现者的设计选择优先级更高。即使你认为某个类型不会被维护它的团队以外的人使用,可软件工程是关于接口设计和抽象的活儿——我们应该优先考虑好的接口。

引用类型成员

存储引用类型数据成员时同理。就算我们知道一个成员必须是非空的,通常还是倾向于将值类型存储为T*,因为引用不能被重新绑定。也就是说,我们不能将T&指向别处——任何对该成员的修改都是在修改其底层的T

考虑std::vector<T>的实现。几乎可以肯定任何std::vector的实现都要有一个T* data成员,指向申请的空间。从std::vector的规格说明中可知,申请的空间通常必须是合法的(空向量情况除外)。如果一种实现下总是有申请的空间,该实现就可以使用T&了,对不对?(是的,我这里忽略了数组和偏移。)

当然不对。std::vector是个值类型,既可以复制又可以赋值。如果申请空间被存储为“指向首元素的引用”而不是“指向首元素的指针”,我们就不可能对存储空间进行移动赋值,在正常调整容量的时候也不清楚该如何更新data。我们以机智的方式告诉其他维护者“这个值不可能为空”,却成了给用户提供期待的API的绊脚石。以此说明这是桩错误的买卖应该够清楚了吧。

不可复制/赋值的类型

当然了,如果在你的类型设计选项里,City(或任何你想到的类型)应该是不可复制的,那就没那么多实现的桎梏了。一个类是否包含const或引用类型,不是个 的问题,而只是要考虑到这些实现决策会限制或腐蚀这个类所展现的接口。如果你想明白了你的类型不需要复制,那关于如何呈现类的数据成员你当然可以合理地做出不同的决策。(但请参阅贴士 #116以了解更多关于参数生存期和引用存储的思考和陷阱。)

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

有一种“有用但不常见”的设计可能 强制要求 const成员:故意的不可变的类型。这种类型的实例在构造之后就是不可变的:没有可修改的方法,没有赋值运算符。这种用法一般不常见,但有时候挺有用。尤其是,这样的类型天然是 线程安全 的,因为没有可修改操作。这种类型的对象可以自由地跨线程共享,而不必担心数据冲突或同步。然而,代价是这些对象因为总是需要拷贝而可能带来可观的运行时代价。其不可变性甚至会导致这些对象不能被高效地移动。

相比于依赖“不可变性带来的线程安全”,通常更推荐将类型设计为可修改但是线程兼容(thread-compatible,译者注:不修改对象状态的成员函数并发调用不用加锁)。你类型的用户通常会处在更好的位置,来根据情况判定可变性带来的好处。在没有很强的证据说明为什么你的用例不寻常之前,不要强迫你的用户来对付不寻常的设计决策。

推荐

  • 在考虑实现细节之前,先决定如何设计你的类型。
  • 值类型是常见的也是推荐的。业务逻辑类型同理,虽然它们通常是不可复制的。
  • 不可变类型有时候有用,但适用的情况不多。
  • 优先考虑API设计和用户的需求,而不是(通常人数更少的)维护者的考量。
  • 在构建值类型或只能移动的(move-only)类型时,避免const和引用类型数据成员。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值