在头文件中作为函数参数使用的“原始”类型前是否最好删除“const”?

55
在代码审查过程中,我的一位同事向我提到,在头文件中作为函数参数使用的“primitive types”前面的“const”是毫无意义的,他建议删除这些“const”。他建议只在源文件中在这种情况下使用“const”。原始类型指如“int”,“char”,“float”等类型。
以下是例子。
example.h
int ProcessScore(const int score);

例子.cc

int ProcessScore(const int score) {
  // Do some calculation using score
  return some_value;
}

他的建议是按照以下方式操作:

example.h

int ProcessScore(int score);  // const is removed here.

示例.cc

int ProcessScore(const int score) {
  // Do some calculation using score
  return some_value;
}

但我有点困惑。通常,用户只会查看头部信息,因此如果头部信息和源文件不一致,可能会引起混乱。

有人可以给出建议吗?


19
不完全正确。 - StoryTeller - Unslander Monica
16
与声明任何常量局部变量并非总是无用的方式相同。 - juanchopanza
11
修改“变量”可能会在函数实现中导致问题,就像修改任何其他“变量”一样。对于实现而言,该参数只是另一个本地变量。 - juanchopanza
13
const是向编译器发出的信号,表明该参数不打算被修改。如果代码试图修改它,那么会发出警告(或错误),因为这与方法/函数的意图相违背。 - Steve
19
在我看来,更重要的是const关键字向其他程序员发出了一个信号,表明该参数不打算被修改。这比编译器是否会执行任何有意义的操作更重要。首先为其他人阅读编写代码,仅在必要时才让编译器解析它,这使得维护起来不那么烦人。 - Dan Mills
显示剩余9条评论
5个回答

80

对于所有类型(不仅限于基本类型),函数声明中的顶层const限定符将被忽略。所以下面这四种形式都声明了相同的函数:

void foo(int const i, int const j);
void foo(int i, int const j);
void foo(int const i, int j);
void foo(int i, int j);

在函数的函数体内,const限定符并不会被忽略。它可以对const正确性产生影响,但这是函数的实现细节。所以一般的共识是:

  1. 在声明中省略const。这只是无用的冗余代码,不会影响客户端如何调用该函数。

  2. 如果您希望编译器捕获任何意外修改参数的情况,请在定义中保留const。


6
我认为这是一件相当重要的事情。如果我遇到一个按值传递的const参数,我会认为开发者犯了一个错误或者对C++还不太熟悉。对于参数而言,只有当它们用作引用或指针的修饰符时,const才具有实际意义,因为它们控制输入的可变性。对于通过值传递的类型来说,将其添加为const并没有提供任何真正的语义意义。 编辑:我应该补充说明,无论您选择如何接受参数,都要保持一致。一致性对于可读性很重要;否则const参数可能看起来具有特殊含义。 - Human-Compiler
2
@Bitwize,这怎么能成为“相当重要的事情”的论据呢?我认为你对个人编码能力的假设并不是很相关。大多数好的IDE都可以从源签名生成函数模板,其中const值限定实际上很重要,因此更改签名似乎很麻烦,而且没有任何好处。只有最近像Clion这样的工具才开始检查这个问题(过去一个月左右)。 - Krupip
1
@snb 我不是在争论,我只是陈述了我的观点。实现中的 const 限定符对于字面类型仍然没有太大的区别。由于它们是字面类型,它并不能阻止任何人简单地“复制”该值并修改该副本。由于这些值不是通过引用或指针传递的(因此不会从当前目录以外修改任何内容),参数的 const 特性实际上并不改变开发人员使用字面类型参数时所能做的事情。 - Human-Compiler
4
@snb - 这并不是非常重要。除非你是那种必须要求声明和定义完全一致的人。因此,如果你一开始到处都加上了const,客户端使用了你的API,然后你改变了主意...构建系统将无缘无故地重新构建所有内容,除非你咬紧牙关并保持头文件不变。所以最好一开始在声明中不要加上const。 - StoryTeller - Unslander Monica
1
@underscore_d 我遵循const的正确性,请不要误解我原先的讯息。我并不是在主张写糟糕的代码。我只是严格针对公共API中的const传值参数进行讨论。当在API中标记它时,这并没有对使用者有任何帮助,因为它没有暗示的意义。使用者不需要知道内部/实现开发的做法。至于在实现方面,对于const的正确性有不同的意见 - 例如你的意见,即使是参数也全部以const开始;这是一种有效的方法。 - Human-Compiler
显示剩余4条评论

43

当涉及到重载解析时,声明为const和没有声明为const的函数参数是相同的。因此,例如函数:

void f(int);
void f(const int);

相同的参数类型不能一起定义。因此,最好不要在声明参数时使用const,以避免可能的重复(我不是在谈论const引用或const指针-因为const修饰符不是顶级的)。

这里是标准中的确切引用。

生成参数类型列表后,在形成函数类型时删除修改参数类型的任何顶层cv限定符。转换后的参数类型列表以及省略号或函数参数包的存在或不存在是函数的参数类型列表。[注意:此转换不影响参数的类型。例如,int(*)(const int p, decltype(p)*)int(*)(int, const int*)是相同的类型。——注释结束]

在函数定义中使用const的实用性有争议-背后的推理与使用const声明局部变量相同-它向阅读代码的其他程序员演示该值不会在函数内被修改。


5
“对于所有类型的参数,声明为const和非const在重载决议时是相同的。”实际上,在语言层面上它们具有相同的签名;不仅重载决议不区分它们:即使在同一程序中也不能定义两个只有顶层参数constness不同的函数。这样做的原因显然是在C/C++中所有参数(包括指针)都是按值传递的,因此没有函数可以更改它们。理解const char *修饰的是目标而不是指针,这一点很重要。 - Peter - Reinstate Monica
1
例如,对于任意复杂的S,TU struct S {}; void f(const S s){} void f(S s) {} 是非法的。 - Peter - Reinstate Monica
@Rakete1111 以什么方式? - Artemy Vysotsky
@Artemy,你提到了原始类型,但是这个引用并没有提到。实际上,这些类型不一定需要是原始类型。 - Rakete1111
@Rakete1111已被移除 - 但我感觉现在我只是在重复StoryTeller所说的话。 - Artemy Vysotsky
显示剩余5条评论

15

遵循代码审查中给出的建议。

对于值参数使用const没有语义价值,它仅对函数的实现有意义 - 即使在这种情况下,我认为也是不必要的。

编辑: 只是为了明确:你的函数原型是你的函数的公共接口。使用const可以保证你不会修改引用。

int a = 7;
do_something( a );

void do_something(       int& x );  // 'a' may be modified
void do_something( const int& x );  // I will not modify 'a'
void do_something(       int  x );  // no one cares what happens to x

使用 const 有点类似于 TMI——在函数内部是否修改 'x' 对外部来说都不重要。

编辑2:我也非常喜欢StoryTeller的回答中的信息。


7

像其他人所回答的一样,从 API 的角度来看,以下几种方式都是等效的,对于重载解析而言它们是相同的:

void foo( int );
void foo( const int );

但更好的问题是,这是否为使用此API的消费者提供了任何语义意义,或者它是否从实现的开发者中提供了任何良好行为的强制执行?
在没有定义明确的开发人员编码指南的情况下,const标量参数没有明显的语义含义。
对于消费者: const int不会改变您的输入。它仍然可以是文字,也可以来自另一个变量(无论是const还是非const)。
对于开发人员: const int对变量(在这种情况下是函数参数)的本地副本施加了限制。这只是意味着要修改参数,您需要获取该变量的另一个副本并对其进行修改。
调用按值接受参数的函数时,在调用的函数栈上制作该参数的副本。这给予该函数在其整个范围内一个参数的本地副本,可随后被修改、用于计算等——而不影响传递到调用中的原始输入。有效地,这提供了其输入的局部变量参数。
通过将参数标记为const,它只是意味着这个副本不能被修改;但它不禁止开发人员复制它并对这个副本进行修改。由于这从一开始就是一个副本,因此它并没有从内部实现方面强制执行太多——从消费者的角度来看,这并没有什么区别。
这与通过引用传递形成对比,其中int&的引用在语义上不同于const int&。前者能够改变其输入;后者只能观察输入(假设实现不会const_cast去掉const);因此,const性对引用具有隐含的语义含义。
它在公共API中并没有提供太多好处;并且(我认为)在实现中引入了不必要的限制。作为任意的、刻意的例子——一个简单的函数如下:
void do_n_times( int n )
{
   while( n-- > 0 ) {
       // do something n times
   } 
}

现在必须使用一个不必要的副本来编写代码:
void do_n_times( const int n )
{
    auto n_copy = n;
    while( n_copy-- > 0 ) {
        // do something n times
    }
}

无论公共 API 是否使用 const 标量,关键是要在设计中保持一致。如果 API 随机地在使用 const 标量参数和非 const 标量之间切换,则会导致消费者是否存在任何暗示意义的混淆。
简而言之,在公共 API 中使用 const 标量类型不会传达语义意义,除非你为你的域明确定义了自己的指南。

-3

我曾经认为const是编译器的提示,表明某些表达式不会改变,并进行相应的优化。例如,我正在测试一个数字是否为质数,通过查找小于该数字平方根的除数,我认为声明参数const将把sqrt(n)带到for循环之外,但实际上并没有。

在头文件中可能并非必要,但你可以说你只需要不修改参数即可,因此从来不需要。我宁愿看到const在它是常量的地方,不仅在源代码中,而且在头文件中也是如此。声明和定义之间的不一致让我感到怀疑。这只是我的观点。


const与函数是否为"纯"函数(无副作用,结果仅取决于输入)无关。如果编译器未能将(int)sqrt((double)n))提升到i < (int)sqrt((double)n))循环条件之外,请尝试提高优化级别。希望您没有写成i < sqrt(n),因为这会进行double比较。 - Peter Cordes
你仍然认为人类可以理解double foobar(const double)表示foobar是一个纯函数,并且使用相同的参数进行重复调用可以手动优化掉。但这不是大多数人会理解的,因为在这种情况下它的含义并非如此。你要找的是double foobar(double) PURE;,其中PURE可以是一个空宏,但对人类仍然有意义,或者在支持它的编译器上,类似于__attribute__((const))的东西。 - Peter Cordes
哦,我想我明白你的意思了。你的意思是在你自己函数的定义中声明了 const n?优化编译器已经能够找出变量自身不会改变的情况。其他评论者或回答(我忘了)已经提到,在定义中使用 const 参数通常是可以的,并且如果你打算保持变量不被修改,这样做可以避免错误,例如防止你通过非 const 引用传递它。(在 C++ 中绝对有用,因为有像 mutate(int &x) 这样的东西,在 C 中则没有。) - Peter Cordes
strcpy的第二个参数是指向const的指针。这与const值完全不同(例如,如果它是const char* const src:一个指向常量的常量指针)。我在谈论纯函数,因为这是编译器知道它可以优化多次调用它们的唯一方法。但我现在想我明白你的意思了:您没有意识到编译器已经找出变量是否为优化目的而声明为const或不声明。 - Peter Cordes
@peter 当然编译器知道变量何时未被更改,否则当它们被更改时,它如何指出错误并且不应该。我一直说这些“const”限定符是为我们而不是为编译器。我知道纯函数是什么,以及在strcpy()const char *的(相关的)影响,但所有这些都与手头的问题无关,我觉得我们已经在这个线程中污染了它。还是谢谢你想启发我。 - lion
显示剩余3条评论

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