我应该将所有可以被声明为const的方法声明为const方法吗?

6

一个简单的问题。 所有可以被声明为const的方法都应该被声明为const方法吗?这包括不返回任何成员变量或返回成员变量的const引用的方法。除了编译器会指出的明显原因之外,有没有其他理由不这样做?


有什么危害吗?你有一些保留吗? - Gabe
@Gabe:不,我只是想要确认(你永远不知道有人会回答“是的,但在某种情况下,你可能不想这样做”)...我正在逐渐习惯const-correctness,并且想澄清这一点。 - Samaursa
2
可能是成员函数应该何时具有const限定符,何时不应该?的重复问题。 - Kirill V. Lyadvinsky
1
有一个警告: “可以是const”应该意味着“可以通过设计成为const”,而不是“如果在当前实现中标记为const仍将编译”。极端的例子-您可以想象具有某些虚拟函数的无操作实现的基类,该函数目前可以被标记为const。但是,如果预计未来的子类将具有非const实现,则当然不会将其标记为const。因此,这不是机械过程,“尝试使其成为const,如果可以则保留其为const”,而是接口设计的一部分,因此是调用方和被调用方之间的协商。 - Steve Jessop
5个回答

13

一个非const方法不能通过一个指向常量对象的指针进行调用。 因此,如果方法可以是const,那么不声明它为const将对其使用施加人为限制。

除此之外,将方法声明为const是一个重要的语义细节,可以让用户感受到从调用它中可以期望得到的影响。


4
请注意物理常量和逻辑常量之间的(重要)区别。有关更多详细信息,请参阅此文章:http://drdobbs.com/cpp/184403892。 - AVH
@unapersson:const方法的语义不允许您修改包含的指针,但它们不会阻止您更改所指向的数据,因此如果类包含指针,则const方法可以以编译器不会标记的方式修改对象的某些状态。 struct test { int *p; test() : p( new int() ) {} ~test() { delete p; } int* getP() const { return p; } };是正确的,并允许int main() { test t; *t.getP() = 10; }更改存储在类中的值。这就是const性是一级属性的原因,用户需要解决更深层次的问题。 - David Rodríguez - dribeas
在某些情况下可以这样做:int const * getP() const { return p; } 将不会返回指针的副本,而是一个指向常量整数的指针,这样调用者就无法修改您的对象。对于引用也是如此,如果您存储了对可变对象的引用,则该对象(语法上)不属于您的对象,因此您可以从常量成员函数返回引用,这些引用引用该对象,从而可能允许用户代码操作您的内部。 - David Rodríguez - dribeas

6

看起来我在这里是一个不和谐的音符:

我应该将可以成为const的任何方法声明为const方法吗?

不,决策应在设计过程中的不同层次上进行。您应标记所有语义上不修改对象的方法为const。这可能包括一些实际上修改了某些内部细节但不是对象可感知状态的一部分的方法(这些属性应为mutable),而可能不包括一些根本不改变任何东西的方法。

enum impl {                 // different implementations of the algorithm
   one_implementation,
   another_implementation
};
class example {
   mutable std::pair<bool, int> cache;
protected:
   int a, b;
public:
   example( int a, int b ) : cache(), a(a), b(b) {}
   virtual ~example() {}
   void set( int _a, int _b ) { 
      cache.first = false;      // invalidate previous result 
      a = _a;
      b= _b;
   }
   int expensive_calculation() const {
      if ( !cache.first ) {
         cache.second = calculate();
         cache.first = true;
      }
      return cache.second;
   }
   virtual void change_impl( impl x ) {}
private:
   virtual int calculate() const = 0;
};

当前形式下,你不能改变实现方式,即使change_impl没有修改任何成员属性也不被标记为const,因为从语义上讲,它确实发生了更改。

另一方面,expensive_calculation()方法并未从语义上修改对象的状态,调用操作前后可感知的状态将保持不变,但是它确实修改了一个cache属性,以加快以后的调用(如果状态未改变)。因此,该方法是const的,而缓存是mutable的。


1
在我看来,这是正确的。在当前实现中,有些函数可能不会改变类数据,但从逻辑上讲它们可能会。我刚刚编写了一个套接字清理函数,它只关闭套接字而已,所以它可以是const的,但也许将来我会添加一个“bool isSocketOpen”来进行更改。 - rotanimod
(+1) 在设计阶段做出决策的观点是非常好的,但我仍然认为它并没有回答确切的问题 - OP问到了哪些方法可以被声明为const,因此使用mutable来声明否则不是const的方法为const根本不相关。 - Alexander Gessler
关于派生类实现虚方法的相同问题,可能会修改对象。如果您知道将需要使方法为非const的实现,则基类中的虚原型在第一次声明时不能被声明为“const”,因此这里提出的问题就不会出现。 - Alexander Gessler
@Alexander Gessler:我认为这非常相关。在mutable成员的情况下,mutable是方法被声明为const结果,而不是反过来。也就是说,没有人会吝啬地写出mutable,而是由于设计决策使方法保持不变,但需要修改该属性。如果你从class test { std::pair<bool,int> cache; int a,b; ...开始,操作不能被设置为const,因为它需要修改cache。然而,从语义上讲,它并没有修改,所以你将其标记为const,编译器会报错,然后你将cache标记为mutable - David Rodríguez - dribeas
在虚函数的情况下,从语言角度来看,你可以将其设置为const,而不需要其他类,但从设计角度来看则不然:是的,目前它确实没有修改任何成员,但如果你将其标记为const,那么以后就会遇到问题。现在,如果你说*在仔细设计后,应该将所有在你的设计中都是常量的东西标记为const*,那么你是完全正确的。但我见过很多人在编译器报错时删除缓存案例中的const,所以我认为这很重要。 - David Rodríguez - dribeas

5

是的。根据《Effective C++》的说法,“尽可能使用const”。


1
有冲动点赞,但因为“某人这么说”不是一个理由而退缩,无论这个人多么可靠和受尊敬。 - user180247
如果没有对“尽可能”的进一步定义,我无法点赞。这就像用完全相同的问题回答问题:“什么时候不应该使用const?” - David Rodríguez - dribeas
那不是他的问题。他的问题是“即使可以,是否有任何特定情况下你不应该将某些东西设为const”。 - Andrew Rasmussen
我在我的回答中提供了一个例子:一个虚方法,可以被重写以修改部分状态,但在当前实现中并没有这样做。在基类中它可以是“const”,但这将限制类的可扩展性,或者让你陷入“mutable”成员的噩梦中,这些成员不应该是可变的,并且突然在所有其他“const”方法中都可以进行修改... - David Rodríguez - dribeas

2

如果一个方法不修改对象的逻辑状态,那么你应该将这个方法标记为const。这对于你对象的客户端来说是一个很好的服务,因为它们可以使用const引用/指针充分利用对象。


2

有一种情况,我会三思而后行地使用const:基类的虚函数。继承类不能通过其覆盖函数进行更改可能是一件好事,但某些开发人员可能会持不同意见,因此必须费尽周折。


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