成员函数什么时候应该有 const 限定符,什么时候不应该有?
大约六年前,一位名叫 Harri Porten 的软件工程师写了这篇文章,询问问题,“成员函数什么时候应该有 const 限定符,什么时候不应该有?”我发现这是我能找到的关于这个问题的最好的文章,我最近一直在努力解决这个问题,而且我认为我发现的关于 const 正确性的大多数讨论都没有很好地涵盖这个问题。由于当时还没有像SO这样强大的软件信息共享网站,所以我想在这里重新提出这个问题。
About six years ago, a software engineer named Harri Porten wrote this article, asking the question, "When should a member function have a const qualifier and when shouldn't it?" I found it to be the best write-up I could find of the issue, which I've been wrestling with more recently and which I think is not well covered in most discussions I've found on const correctness. Since a software information-sharing site as powerful as SO didn't exist back then, I'd like to resurrect the question here.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
这篇文章似乎涵盖了很多基础知识,但作者仍然对返回指针的函数的 const 和非常量重载有疑问。文章的最后一行是:
许多人可能会回答“这取决于情况”。但我想问“这取决于什么?”
绝对准确地说,这取决于 A 对象指针对象的状态在逻辑上是否是
this
对象状态的一部分。例如,
vector::operator[]
返回对 int 的引用。 int 引用数是向量的“一部分”,尽管它实际上不是数据成员。因此,常量重载惯用语适用:更改一个元素,您就更改了向量。对于不存在的示例,请考虑
shared_ptr
。它具有成员函数T * operator->() const;
,因为拥有一个指向非 const 对象的 const 智能指针 具有逻辑意义。引用对象不是智能指针的一部分:修改它不会更改智能指针。因此,是否可以“重新设置”智能指针以引用不同的对象的问题与引用对象是否为 const 无关。我认为我无法提供任何完整的指导方针来让您决定受指点在逻辑上是否是对象的一部分。但是,如果修改被指点会更改
this
的任何成员函数的返回值或其他行为,特别是如果被指点参与operator==
,那么很可能是逻辑上是this
对象的一部分。我宁愿假设它是一部分(并提供重载)。然后,如果出现编译器抱怨我正在尝试修改从 const 对象返回的 A 对象的情况,我会考虑我是否真的应该这样做,如果是这样,请更改设计,以便只有 < em>指向 A 的指针在概念上是对象状态的一部分,而不是 A 本身。这当然需要确保修改 A 不会做任何破坏
this
const 对象预期行为的事情。如果您要发布接口,您可能必须提前弄清楚这一点,但实际上,从 const 重载返回到 const 函数返回非 const 指针不太可能破坏客户端代码。不管怎样,当您发布一个接口时,您希望已经使用过它一点,并且可能对对象的状态真正包含的内容有所了解。
顺便说一句,我也尝试犯错误,不提供指针/引用访问器,尤其是可修改的访问器。这确实是一个单独的问题(德米特定律等),但是您可以将: 替换
为:
的次数越多,您需要担心该问题的次数就越少。当然后者也有其自身的局限性。
The article seems to cover a lot of basic ground, but the author still has a question about const and non-const overloads of functions returning pointers. Last line of the article is:
Many will probably answer "It depends." but I'd like to ask "It depends on what?"
To be absolutely precise, it depends whether the state of the A object pointee is logically part of the state of
this
object.For an example where it is,
vector<int>::operator[]
returns a reference to an int. The int referand is "part of" the vector, although it isn't actually a data member. So the const-overload idiom applies: change an element and you've changed the vector.For an example where it isn't, consider
shared_ptr
. This has the member functionT * operator->() const;
, because it makes logical sense to have a const smart pointer to a non-const object. The referand is not part of the smart pointer: modifying it does not change the smart pointer. So the question of whether you can "reseat" a smart pointer to refer to a different object is independent of whether or not the referand is const.I don't think I can provide any complete guidelines to let you decide whether the pointee is logically part of the object or not. However, if modifying the pointee changes the return values or other behaviour of any member functions of
this
, and especially if the pointee participates inoperator==
, then chances are it is logically part ofthis
object.I would err on the side of assuming it is part (and provide overloads). Then if a situation arose where the compiler complains that I'm trying to modify the A object returned from a const object, I'd consider whether I really should be doing that or not, and if so change the design so that only the pointer-to-A is conceptually part of the object's state, not the A itself. This of course requires ensuring that modifying the A doesn't do anything that breaks the expected behaviour of
this
const object.If you're publishing the interface you may have to figure this out in advance, but in practice going back from the const overloads to the const-function-returning-non-const-pointer is unlikely to break client code. Anyway, by the time you publish an interface you hopefully have used it a bit, and probably got a feel for what the state of your object really includes.
Btw, I also try to err on the side of not providing pointer/reference accessors, especially modifiable ones. That's really a separate issue (Law of Demeter and all that), but the more times you can replace:
with:
The less times you have to worry about the issue. Of course the latter has its own limitations.
我在研究这个问题时发现的一个有趣的经验法则来自这里:
一个很好的经验法则LogicalConst 如下: 如果一个操作保留 LogicalConstness,那么如果用 EqualityOperator 比较旧状态和新状态,结果应该为 true。换句话说,EqualityOperator 应该反映对象的逻辑状态。
One interesting rule of thumb I found while researching this came from here:
A good rule of thumb for LogicalConst is as follows: If an operation preserves LogicalConstness, then if the old state and the new state are compared with the EqualityOperator, the result should be true. In other words, the EqualityOperator should reflect the logical state of the object.
我个人使用一个非常简单的经验法则:
如果调用给定方法时对象的可观察状态没有改变,则该方法应该是
const
。总的来说,它类似于 SCFrench 提到的关于相等比较的规则,只是我的大多数类无法进行比较。
不过,我想将争论进一步推进:
当需要一个参数时,函数应该通过
const
处理(或复制)如果参数保持不变(对于外部观察者)它稍微更通用一些,因为毕竟类的方法只不过是一个接受类实例作为第一个参数的独立函数:
相当于:
I personally use a very simple Rule Of Thumb:
If the observable state of an object does not change when calling a given method, this method ought to be
const
.In general it is similar to the rule mentioned by
SCFrench
about Equality Comparison, except that most of my classes cannot be compared.I would like to push the debate one step further though:
When requiring an argument, a function ought to take it by
const
handle (or copy) if the argument is left unchanged (for an external observer)It is slightly more general, since after all the method of a class is nothing else than a free-standing function accepting an instance of the class as a first argument:
is equivalent to:
当它不修改对象时。
它只是使
this
具有类型const myclass*
。这保证了调用函数的对象不会改变。允许对编译器进行一些优化,并且程序员更容易知道他是否可以在没有副作用的情况下调用它(至少对对象有影响)。when it doesn't modify the object.
It simply makes
this
to have typeconst myclass*
. This guarantees a calling function that the object won't change. Allowing some optimizations to the compiler, and easier for the programmer to know if he can call it without side effects (at least effects to the object).一般规则:
例外:
const
的语言编写的代码接口的方法General rule:
Exceptions:
const
这里有一些不错的文章:
赫伯·萨特的 GotW #6
赫伯·萨特 &用于优化的 const
有关 const 正确性的更多建议
来自 Wikipedia
当方法执行时,我使用
const
方法限定符不改变类的数据成员或其共同意图是不修改数据成员。一个示例涉及 RAII 的 getter 方法,该方法可能必须初始化数据成员(例如从数据库检索)。在此示例中,该方法仅在初始化期间修改数据成员一次;所有其他时间它都是恒定的。我允许编译器在编译时捕获 const 错误,而不是在运行时(或用户)捕获它们。
Here are some good articles:
Herb Sutter's GotW #6
Herb Sutter & const for optimizations
More advice on const correctness
From Wikipedia
I use
const
method qualifiers when the method does not alter the class' data members or its common intent is not to modify the data members. One example involves RAII for a getter method that may have to initialize a data members (such as retrieve from a database). In this example, the method only modifies the data member(s) once during initialization; all other times it is constant.I'm allowing the compiler to catch const errors during compile time rather than me catching them during run-time (or a User).