在合适的地方,在成员函数的末尾添加const是不是一个好习惯?

is it good practice to add const at end of member functions - where appropriate?

本文关键字:是不是 好习惯 一个 const 函数 成员 添加      更新时间:2023-10-16

在c++中,每次函数不修改对象时,即每次函数'符合条件'使用const时,在成员函数定义的末尾添加const是否是一个好做法?我知道在这种情况下这是必要的:

class MyClass {
public:
int getData() const;
};
void function(const MyClass &m) { int a = m.getData(); dosomething... }

但除此之外,以及const在实际功能中的其他用途,在末尾添加const是否真的改变了代码执行的方式(更快/更慢),或者它只是编译器处理上述情况的一个"标志"?换句话说,如果类中的功能不需要const(在末尾),那么添加它会有什么不同吗?

请参阅Herb Sutter (c++标准委员会秘书10年)关于常量正确性的优秀文章

关于优化,他后来写了这篇文章,他说"const主要是为人类服务的,而不是为编译器和优化器服务的。"优化是不可能的,因为"太多事情可能出错……[你的函数]可能执行const_cast。"

然而,const正确性是一个好主意,有两个原因:它是一个便宜的(就你的时间而言)断言,可以找到错误;而且,它表明了函数理论上不应该修改对象的外部状态的意图,这使得代码更容易理解。

每次函数不修改对象时,即每次函数"符合"const?

在我看来,是的。它确保在const对象或涉及对象的const表达式上调用这样的函数:

void f(const A & a)
{
   a.inspect(); //inspect must be a const member function.
}

即使它修改了一个或几个内部变量一次或两次,即使这样,我通常使它成为const成员函数。这些变量用mutable关键字声明:

class A
{
     mutable bool initialized_cache; //mutable is must!
     public:
         void inspect() const //const member function
         {
               if ( !initialized_cache)
               {
                    initialized_cache= true;
                    //todo: initialize cache here
               }
               //other code
         }
};

是。一般来说,每个逻辑上为const的函数都应该被设置为const。唯一的灰色地带是通过指针修改成员的地方(可以将其设置为const,但有争议的是不应该将其设置为const),或者修改用于缓存计算但没有效果的成员的地方(有争议的是应该将其设置为const,但需要使用关键字mutable来完成)。

使用const非常重要的原因是:

  1. 这是其他开发人员的重要文档。开发人员将假设任何标记为const的东西都不会改变对象(这就是为什么在通过指针对象改变状态时使用const可能不是一个好主意),并且将假设任何未标记为const的东西都会改变。

  2. 它将导致编译器捕获无意的突变(如果标记为const的函数无意地调用非const函数或改变元素,则会导致错误)。

是的,这是一个很好的做法。

在软件工程层面,它允许你拥有只读对象,例如,你可以通过使对象为const来防止对象被修改。如果对象是const,则只允许在其上调用const函数。

此外,我相信如果编译器知道一个对象只会被读取(例如,在对象的几个实例之间共享公共数据,因为我们知道它们永远不会被修改),那么编译器可以进行某些优化。

'const'系统是c++中真正混乱的特性之一。它在概念上很简单,用' const '声明的变量成为常量,不能被程序改变,但是,它必须被用来代替c++缺少的一个特性,它变得非常复杂和令人沮丧的限制。下面试图解释const是如何使用的以及它为什么存在。在指针和' const '的组合中,指向变量的常量指针对于可以改变值但不能在内存中移动的存储有用,指针(常量或其他)对于从函数返回常量字符串和数组有用,因为它们是作为指针实现的,否则程序可能会试图改变并崩溃。在编译过程中,将检测到更改不可更改值的尝试,而不是难以跟踪崩溃。

例如,如果一个函数返回一个固定的' Some text '字符串,写为

char *Function1()
{ return “Some text”;}

那么程序可能会崩溃,如果它不小心试图改变

的值
Function1()[1]=’a’;

而如果原函数写成

,编译器会发现错误
 const char *Function1()
 { return "Some text";}

,因为编译器会知道该值是不可改变的。(当然,从理论上讲,编译器可以解决这个问题,但C并没有那么聪明。)当带参数调用子例程或函数时,可以读取作为参数传递的变量以将数据传输到子例程/函数中,也可以写入以将数据传输回调用程序,或者两者兼而有之。有些语言允许直接指定,例如' in: ', ' out: ' &' inout: '参数类型,而在C语言中,必须在较低的级别上工作,并指定传递变量的方法,选择一个也允许所需的数据传输方向。

例如,像

这样的子程序
void Subroutine1(int Parameter1)
{ printf("%d",Parameter1);}

接受传递给它的默认参数C &c++的方式,这是一个副本。因此,子例程可以读取传递给它的变量的值,但不能更改它,因为它所做的任何更改都只对副本进行更改,并且在子例程结束时丢失

void Subroutine2(int Parameter1)
{ Parameter1=96;}

将使调用它的变量保持不变,而不是设置为96。

在c++中,在参数名后面加上一个' & '(这是一个非常令人困惑的符号选择,因为在C中其他地方的变量前面加上' & '会生成指针!)会导致实际变量本身,而不是一个副本,被用作子例程中的参数,因此可以被写入,从而将数据传递回子例程。因此

void Subroutine3(int &Parameter1) 
{ Parameter1=96;}

将调用它的变量设置为96。这种将变量作为自身而不是副本传递的方法在c语言中称为"引用"。

传递变量的方式是c++在C中添加的。要在原始C中传递可变变量,使用了一种相当复杂的方法,使用指向变量的指针作为参数,然后改变它指向的对象。例如

void Subroutine4(int *Parameter1) 
{ *Parameter1=96;}

可以工作,但是要求每次调用的例程中变量的使用都要改变,并且调用的例程要改变传递指向变量的指针,这是相当麻烦的。

但是const在哪里出现呢?通过引用或指针而不是复制传递数据还有第二种常见用法。在这种情况下,复制变量会浪费太多内存或花费太长时间。对于大型复合用户定义变量类型(C &c++中的"类")。所以子程序声明了

void Subroutine4(big_structure_type &Parameter1);

可能使用' & ',因为它将改变传递给它的变量,或者它可能只是为了节省复制时间,并且如果函数是在其他人的库中编译的,则无法判断它是哪个。如果需要信任子例程不会更改变量,那么这可能是一种风险。

要解决这个问题,可以在形参列表中使用' const ',如
void Subroutine4(big_structure_type const &Parameter1);

将导致变量传递而不复制,但阻止它被修改。这是很混乱的,因为它本质上是从双向变量传递方法中创建一个唯一的变量传递方法,而双向变量传递方法本身是由一个唯一的变量传递方法创建的,只是为了欺骗编译器做一些优化。

理想情况下,程序员不需要控制变量传递的细节,只需要说明信息传递的方向,然后让编译器自动优化即可,但C是为在远不如当今标准的计算机上进行原始低级编程而设计的,因此程序员必须显式地完成。

我的理解是,它确实只是一面旗帜。然而,也就是说,您希望在任何可能的地方添加它。如果您没有添加它,并且代码中其他地方的函数做了类似

的事情
void function(const MyClass& foo)
{
   foo.getData();
}

你会遇到问题,因为编译器不能保证getData不会修改foo。

将成员函数设置为const确保具有const对象的调用代码仍然可以调用该函数。这与编译器检查有关——它有助于创建健壮的自文档代码并避免对对象的意外修改——而与运行时性能无关。所以,是的,如果函数的性质不需要修改对象的可观察值,那么您应该始终添加const(它仍然可以修改带有mutable关键字前缀的成员变量,这是为了一些奇怪的用途,如内部缓存和计数器,而不影响对象的客户可见行为)。