成员函数啥时候应该有一个 const 限定符,啥时候不应该?
Posted
技术标签:
【中文标题】成员函数啥时候应该有一个 const 限定符,啥时候不应该?【英文标题】:When should a member function have a const qualifier and when shouldn't it?成员函数什么时候应该有一个 const 限定符,什么时候不应该? 【发布时间】:2010-03-17 14:21:55 【问题描述】:大约六年前,一位名叫 Harri Porten 的软件工程师写信给this article,提出了一个问题:“成员函数什么时候应该有一个 const 限定符,什么时候不应该?”我发现这是我能找到的关于这个问题的最好的文章,我最近一直在努力解决这个问题,我认为在我发现的关于 const 正确性的大多数讨论中都没有很好地涵盖这个问题。由于当时还没有像 SO 这样强大的软件信息共享网站,所以我想在这里重新提出这个问题。
【问题讨论】:
***.com/questions/136880/… 那篇文章回答“我为什么要使用 const”。我被卖了。我的问题是,“我应该如何有效地使用 const ”。如果您还没有,请阅读链接,它清楚地描述了我正在努力解决的设计问题。 鉴于您正在征求讨论,这应该是一个社区维基吗? 当我第一次浏览你的问题时,我的眼睛读到“..一个名叫哈利波特的软件工程师”。 链接已失效 - 用谷歌搜索更新的链接,但找不到。有人有链接吗? 【参考方案1】:这篇文章似乎涵盖了很多基础知识,但作者仍然对返回指针的函数的 const 和非 const 重载有疑问。文章最后一行是:
很多人可能会回答“视情况而定”。但我想问“这取决于什么?”
绝对准确地说,这取决于 A 对象指针的状态在逻辑上是否是 this
对象状态的一部分。
例如,vector<int>::operator[]
返回一个对 int 的引用。 int 引用是向量的“一部分”,尽管它实际上不是数据成员。所以 const-overload 习语适用:改变一个元素,你就改变了向量。
如果不是这样,请考虑shared_ptr
。这有成员函数T * operator->() const;
,因为有一个指向非const 对象 的const 智能指针 是合乎逻辑的。引用不是智能指针的一部分:修改它不会更改智能指针。因此,是否可以“重置”智能指针以引用不同对象的问题与引用对象是否为 const 无关。
我认为我无法提供任何完整的指导方针来让您决定指针对象在逻辑上是否是对象的一部分。但是,如果修改指针对象会更改this
的任何成员函数的返回值或其他行为,特别是如果指针对象参与了operator==
,那么它很可能在逻辑上是this
对象的一部分。
我会错误地假设它 是 的一部分(并提供重载)。然后,如果出现编译器抱怨我试图修改从 const 对象返回的 A 对象的情况,我会考虑我是否真的应该这样做,如果是这样,请更改设计,以便只有 pointer-to-A 在概念上是对象状态的一部分,而不是 A 本身。这当然需要确保修改 A 不会破坏this
const 对象的预期行为。
如果您要发布接口,您可能必须提前弄清楚这一点,但实际上从 const 重载返回到 const-function-returning-non-const-pointer 不太可能破坏客户端代码。不管怎样,当你发布一个接口时,你希望你已经使用了一点,并且可能对你的对象的状态真正包括什么有所感觉。
顺便说一句,我也尝试在不提供指针/引用访问器方面犯错,尤其是可修改的访问器。这确实是一个单独的问题(得墨忒耳法则等等),但您可以更换的次数越多:
A *getA();
const A *getA() const;
与:
A getA() const; // or const A &getA() const; to avoid a copy
void setA(const A &a);
您不必担心这个问题的次数越少。当然后者有其自身的局限性。
【讨论】:
Accessor vs Setter 是一个长期存在的问题。访问器的优点是语法可能更吸引人并且它避免了复制(这对于无法复制的对象非常有用)。 当然,这就是为什么它是“顺便说一句”。在此特定示例中,返回非常量指针不会保存复制。*(thing.getA()) = whatever;
(第一个定义)与void setA(const A &whatever) *(this->myAptr) = whatever;
(第二个定义)具有相同的复制量。为了减少复制,您可以执行void setA(A *whatever) myAptr = whatever;
之类的操作并大量管理资源。显然,当元素对象显式存在并且您可能合理地想要指向它的指针时,某些访问器(例如向量)非常有意义。如果它只是拥有对象的一个抽象属性,那就没有那么多了。
... 例如,将 vector 的 size()
和 resize()
函数合并到一个返回可以读取或写入的代理对象的单个 size()
函数中,这将是纯粹的疯狂到,尽管vec.size() = 12;
的语法很吸引人。然而有时你会看到对象的访问器相当于一个美化的数据成员,大概是由那些注意到 Properties 比 C# 中的公共字段更好的人编写的,但认为这仅仅是因为它们提供了一个方便的地方来挂钩之前的一些验证代码设置“真实”字段;-)【参考方案2】:
我在研究这个时发现的一个有趣的经验法则来自here:
LogicalConst 的一个好的经验法则如下:如果一个操作保留了 LogicalConstness,那么如果将旧状态和新状态与 EqualityOperator 进行比较,结果应该为真。换句话说,EqualityOperator 应该反映对象的逻辑状态。
【讨论】:
【参考方案3】:我个人使用一个非常简单的经验法则:
如果调用给定方法时对象的可观察状态没有改变,则该方法应该是const
。
总的来说,它类似于SCFrench
提到的关于平等比较的规则,除了我的大多数课程无法比较。
不过,我想进一步推动辩论:
当需要一个参数时,函数应该通过 const
如果参数保持不变(对于外部观察者)处理(或复制)
它稍微更通用一些,因为毕竟类的方法只不过是一个接受类实例作为第一个参数的独立函数:
class Foo void bar() const; ;
相当于:
class Foo friend void bar(const Foo& self); ; // ALA Python
【讨论】:
【参考方案4】:当它不修改对象时。
它只是让this
具有const myclass*
类型。这保证了对象不会改变的调用函数。允许对编译器进行一些优化,让程序员更容易知道他是否可以在没有副作用的情况下调用它(至少对对象有影响)。
【讨论】:
但是概念上的恒定性呢?如果它不修改对象本身,但修改了对象的指针成员之一指向的东西怎么办? 这个“当它不修改对象时”是指不修改类的字段(对象是字段和方法) 这不是逻辑常数,而是硬件常数。【参考方案5】:一般规则:
一个成员函数应该是
const
,如果它在标记为const
时都可以编译并且如果const
是可传递的w.r.t指针,它仍然可以编译。
例外情况:
逻辑上的 const 操作;改变内部状态的方法但是使用类的接口无法检测到这种改变。例如展开树查询。 const/non-const 实现仅因返回类型不同而不同的方法(与返回迭代器/const_iterator 方法相同)。可以通过 const_cast 在 const 版本中调用非 const 版本以避免重复。 与 const 不正确的第 3 方 C++ 或以不支持const
的语言编写的代码接口的方法
【讨论】:
如果你的内部状态改变了,即使在逻辑上它应该是可变的【参考方案6】:这里有一些不错的文章:Herb Sutter's GotW #6Herb Sutter & const for optimizationsMore advice on const correctnessFrom Wikipedia
当方法不改变类的数据成员或其共同意图不是修改数据成员时,我使用const
方法限定符。一个示例涉及可能必须初始化数据成员(例如从数据库中检索)的 getter 方法的 RAII。在这个例子中,该方法在初始化期间只修改了一次数据成员;其他时间都是常数。
我允许编译器在编译时捕获 const 错误,而不是我在运行时(或用户)捕获它们。
【讨论】:
以上是关于成员函数啥时候应该有一个 const 限定符,啥时候不应该?的主要内容,如果未能解决你的问题,请参考以下文章