C++类的常量正确性

3
class SomeClass {
  public:
    void Render() const;

  private:
     mutable Cache m_some_cache;
};

这个类是const-correct的吗?什么时候可以安全地说“这个操作不会改变实例的内部状态”?

在上面的示例中,SomeClass是一个在屏幕上渲染东西的东西。它使用缓存(例如OpenGL缓冲区对象)来允许更快的处理进一步的调用。因此,唯一会在内部发生变化的是缓存对象。我在问自己,缓存是否已经属于渲染器的内部状态。

这个示例非常简单,但在我的实际应用程序中,这会涉及到许多类,即涉及许多Render()调用,其中大部分只进行缓存。但有些还通过资源加载器加载资源--在这里做出的假设仍然正确吗,即如果查询资源管理器以加载资源,则方法仍可能是const的?


12
常量正确性与改变实例的内部状态无关,它与改变实例的“可观察”状态有关。 - ildjarn
@ildjarn,这可能是个好答案 :) - unkulunkulu
6个回答

7
当我们说“内部状态不改变”时,我们指的是一件纯粹的逻辑事情。改变m_some_cache是否会改变对象状态是一个逻辑决定。常量正确性是一个逻辑问题。因此,如果你认为从用户的角度来看,改变m_some_cache不会影响对象状态(在逻辑意义上),那么这段代码就是常量正确的。
在你的特定情况下,我认为这是可以的。

好的,这很清楚,但让我们再深入一点:在Render()中,调用了SomeResourceManager::GetImage(id)。如果该ID的图像尚未加载,则会加载它,存储在管理器中并返回。你认为它仍然可以是const吗?(因为如果没有它,我无法声明我的一些Render()方法为const;). - stschindler
@Tank:是的,事实上我会。 - Armen Tsirunyan
@Tank:在一个const成员函数中修改SomeResourceManager是可以的,只要资源管理器的状态不被视为该对象状态的一部分即可。 - Steve Jessop
@Steve Jessop:嗯,资源管理器的唯一状态只包括这些资源。但实际上,在加载资源时永远不会有任何副作用,因此可以假定我的资源管理器根本没有任何“真正”的内部状态。 - stschindler
1
@Tank:它可能具有状态——即它已加载哪些资源的事实。假设它从磁盘加载资源。进一步假设您从磁盘加载文件,然后稍后在磁盘上修改该文件。除非资源管理器检测到此并使其缓存记录无效,否则它将具有外部可见状态。 - Steve Jessop
@Steve Jessop:这种情况不会发生,它只是一次性的操作,但你的想法非常合理。就像在另一个评论中提到的那样,我应该考虑将加载和从管理器获取资源分开。 - stschindler

4

这样想。下面的代码是完全有效的:

void Type::print_self () const {
    std::cout << *this << std::endl;
}

您并没有修改对象本身,因此使用const限定符是完全有效的。该方法正在修改std::cout,但就Type::print_self()const性而言,这并不算数。
话虽如此,对我来说,mutable Cache似乎是一个自相矛盾的说法,除非您仅在Render内部使用该Cache元素进行本地存储。如果您真的将其用作缓存,那么将此元素标记为mutable似乎有点可疑。将其用作缓存(例如,跨Render调用而不是在Render调用中)并且您已经欺骗了编译器和类的用户。
根据OP发表的评论,Render方法确实是print_self()的图形等效物。对象的“真实”状态(可能为了构建最小工作示例而未显示)可能没有被渲染修改。将Render指定为const方法是正确的做法。如果Cache数据成员存在的原因是作为速度障碍以避免每次调用Render时都要构造和析构它的成本,那么将该Cache成员标记为mutable是没有问题的(这是需要的,以便Render保持const)。

Render 确实是唯一使用缓存的人。我喜欢你的例子,但让我扩展一下。想象一下你有一个 SnippetManager,它提供文本片段,具有 GetSnippet(id) 方法。如果在 print_self() 中提供了未知的片段ID,则管理器会加载它。所以 SnippetManagerstate 改变了(不会向外界改变,因为它总是返回相同的值);将 GetSnippet() 声明为常量是否可以? - stschindler
1
@Tank:如果我正在审查那个SnippetManager代码,我会对将GetSnippet限定为const表示怀疑。话虽如此,我故意使用了print_self,因为我假设你的Render方法是print_self的图形等效物。如果缓存仅用于渲染,并且您将其作为实例变量而不是Render中的局部变量出于性能原因,则您在这里使用constmutable可能是可以接受的。请准备好在代码审查期间(假设您进行了代码审查)为您在这里使用constmutable进行辩解。 - David Hammen
非常感谢。你的回答和评论指引我找到了正确的方向,因此我将其设为最佳答案。 - stschindler

3
什么时候可以安全地说“此操作不会改变实例的内部状态”?
这个问题是逻辑问题。通常,可变成员不被视为内部状态,而是作为实现工件。 因此,您类的文档通常应描述什么被视为内部状态,可以忽略可变成员。
const只涉及对象的内部状态。const方法可以合法地更改外部状态。这里想到指针类比:char * const p是一个常量指针,但它可以更改指向的值。因此,您关于资源管理器的示例也是正确的。

好的,我明白了。但是为了让它工作,资源管理器的方法也需要是const的,应该像这样:const Resource& GetResource( const string& id ) const。如果资源还没有被加载,管理器将会加载它。所以管理器中持有资源的容器需要声明为mutable。我想知道这是否仍然是一个好的设计。 :) - stschindler
1
@Tank 我不明白。再说一遍,const方法可以访问另一个(非const)对象的非const方法。因此,我们应该单独讨论资源管理器而不是SomeClass类,在这里您可以将加载的资源视为其可观察状态的一部分,并且不声明GetRc为const。 - unkulunkulu
因此,将加载和获取资源拆分为不同的方法会更好(这样Load()将不是const,而Get()是const)。我这样做不仅仅是为了方便,而且从设计上来看,这可能是更好的选择。 - stschindler
我还是不明白原因:只需将其设为非常量即可。如果ResourceManager本身是const,您将无法调用它。但在我看来,这似乎是合理的。任何仅持有ResourceManager的const引用的人为什么能够使用它加载资源呢?这个要求从哪里来的? - unkulunkulu
假设:您可以提供一个名为IsLoaded(id)的const函数,然后一个持有const引用的用户可以测试特定资源是否已加载,然后使用您在先前评论中提出的const Get方法,但是如果它没有被加载呢?对我来说看起来像是个死胡同。如果他在这种情况下能找到一些非const引用,为什么不一开始就用呢? - unkulunkulu
在审查我的代码时,我遇到了一个关键部分:资源管理器包括一个“基本路径”,它从中加载资源。理论上,该路径可以随时更改,因此Get()ters可能返回不同的值。 我通过将其拆分为三个方法来解决问题:Get() constLoad()IsLoaded() const。运行得非常顺畅。 当资源不存在时,会尝试在正确的位置加载它。如果无法加载资源,那么我就有一个严重的问题。;) 除此之外,我的所有Render()现在都是const,并且只查询管理器的内容。 - stschindler

2
正如@ildjarn所观察到的那样,const-correctness指的是对象的可观察状态,而非内部状态;这就是为什么mutable很有用的原因。
然而,如果你实际上正在渲染东西,那么代表屏幕的对象的可观察状态不可能合理地是const,在我看来,因为如果你稍后添加一个检查方法以找出屏幕上/帧缓冲区中的内容,它将会中断。
如果SomeClass不代表屏幕,那么我希望Render将一个可变引用作为参数传递给一个Screen对象。从逻辑上讲,即使它不是SomeClass实例,某些东西也必须改变。

2

常量正确性是关于从外部看对象的状态。

如果所有调用成员函数的结果都保持不变,对象的状态在逻辑上是相同的。


2
要问自己的问题是:“调用Render函数是否会改变对象的定义状态或未来行为,就用户而言?”假设您的缓存确实只是一个资源缓存,那么修改缓存不会改变功能行为,只会使其潜在地更快。由于您的类没有提供有关其速度慢的保证,因此对于调用方而言,这不会改变定义状态或行为。因此,它是可变成员的有效候选项,可以通过const成员函数进行修改。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接