使用const修饰函数参数会产生什么影响?为什么它不会影响函数的签名?

525
例如,想象一个简单的变异器,它接受一个布尔类型的参数:
void SetValue(const bool b) { my_val_ = b; }

那个const实际上有任何影响吗?个人而言,我倾向于广泛使用它,包括参数,但在这种情况下,我想知道它是否有任何区别。

我也很惊讶地发现,在函数声明中可以省略参数的const,但可以在函数定义中包含它,例如:

.h文件

void func(int n, long l);

.cpp文件
void func(const int n, const long l) { /* ... */ }

有什么原因吗?对我来说,这似乎有点不寻常。

我不同意。.h文件必须包含const定义。如果不包含,那么如果将const参数传递给函数,编译器将生成错误,因为.h文件中的原型没有const定义。 - selwyn
15
我同意。 :-) (同意问题,而不是最后一条评论!)如果函数体内的某个值不应该被更改,这可以帮助防止愚蠢的 == 或 = 错误,你不应该在两者中都加上 const(如果它是按值传递的,你必须这样做)。这并不严重到需要争论的地步! - Chris Huang-Leaver
27
即使你向函数传递了一个const int,由于它不是引用,所以仍然会被复制,因此const属性并不重要。 - jalf
1
同样的辩论正在这个问题中进行:https://dev59.com/qXI_5IYBdhLWcg3wDOZC - Partial
7
我知道这篇文章已经几年了,但作为一个新程序员,我也在思考这个问题,然后偶然看到了这段对话。在我看来,如果一个函数不应该改变某个值,无论是传递的引用还是值/对象的副本,它都应该被声明为const。这样更加安全,更有自说明性,且更易于调试。即使对于最简单的只有一条语句的函数,我仍会使用const。 - user898058
现在,使用移动语义时,const 可能会带来性能成本。 - Jimmy R.T.
31个回答

530

如果参数是按值传递的,那么使用const是没有意义的,因为您不会修改调用者的对象。

错了。

这是关于自我记录代码和假设的问题。

如果您的代码有很多人在开发,并且您的函数是非平凡的,则应将const标记为任何可以的内容。编写工业级别的代码时,您应始终假定您的同事是试图以任何方式攻击您的精神病患者(特别是因为未来的自己通常也是如此)。

此外,正如之前提到的,它可能有助于编译器优化一些东西(尽管这是一个长期的过程)。


56
完全同意。这完全是关于与人沟通,并将可变量的限制限定在应该做的事情上。 - Len Holgate
36
我已经投反对票。我认为你在将 const 应用于简单传值参数时,会削弱你试图表示的含义。 - tonylo
37
我已经为此投赞成票。声明参数“const”会增加有关参数的语义信息。这些信息突出了代码的原始作者所期望的内容,并且随着时间的推移,这将有助于代码的维护。 - Richard Corden
18
@tonylo:你误解了。这是关于在代码块(可能是函数)内将本地变量标记为const。对于任何本地变量,我都会这样做。这与具有const-correct的API是正交的,后者确实也很重要。 - rlerallut
71
如果你知道某个参数不应该被更改,那么将其声明为const可以使编译器在函数内部捕获到修改该参数的错误。 - Adrian
显示剩余15条评论

242

原因在于函数参数上的const只在函数内部局部适用,因为它是在数据的副本上操作。这意味着函数签名实际上仍然是相同的。虽然这样做可能不好看。

我个人倾向于只在引用和指针参数中使用const。对于复制的对象来说,它并不重要,尽管它可以作为函数内部意图的信号更安全。这真的是一个判断的问题。当我遍历某些东西并且不打算修改它时,我确实倾向于使用const_iterator,所以我想每个人都有自己的喜好,只要对于引用类型的const正确性得到严格维护即可。


77
我无法同意“不好的风格”这一部分。在函数原型中省略 const 的好处在于,如果您决定稍后从实现部分中省略 const,则无需更改头文件。 - Michał Górny
11
“我个人不倾向于在函数声明中使用const关键字,除非是用于引用和指针参数。”也许你应该澄清一下:“我倾向于在函数声明中不使用多余的修饰符,但在const能够起到有用作用的地方使用它。” - Deduplicator
8
我不同意这个回答。我倾向于另一种方式,尽可能标记参数为const ,这样更加表达清晰。当我阅读别人的代码时,我会使用像这样的小指示器来评估他们在编写代码时付出的关注程度,例如魔术数字、注释和正确的指针使用等。 - Ultimater
5
尝试这个:int getDouble(int a){ ++a; return 2*a; } 当然,++a 在这里没什么用,但在一个由多个程序员长时间编写的长函数中可能会找到它。我强烈建议编写 int getDouble( const int a ){ //... },这将在发现 ++a; 时生成编译错误。 - dom_beau
13
这完全取决于谁需要哪些信息。您通过按值传递参数,因此调用者无需知道您在内部对其执行的操作。因此,在您的头文件中编写class Foo{ int multiply(int a, int b) const; }。在您的实现中,您确实关心您能够承诺不更改ab,因此在此处编写int Foo::multiply(const int a, const int b) const { }是有意义的。(副笔:调用方和实现都关心函数不会改变其Foo对象,因此在其声明末尾使用const)。 - CharonX
显示剩余4条评论

182
有时(太频繁了!)我需要梳理别人的C++代码。我们都知道,几乎可以通过定义来证明别人的C++代码是一团糟:)因此,为了解密本地数据流,我做的第一件事就是在每个变量定义中放置const,直到编译器开始警告为止。这意味着也要将值参数进行const限定,因为它们只是被调用方初始化的花哨的局部变量。
啊,我希望变量默认情况下就是const,而对于非const变量则需要使用mutable :)

9
“我希望变量默认为const” - 这是矛盾的吗?认真地说,如何通过将所有内容都设置为“const”来帮助您解决代码问题?如果原始编写者更改了一个应该是常数的参数,那么您如何知道该变量应该是常数?此外,绝大多数(非参数)变量都意味着是可变的。那么,在您开始此过程后,编译器应很快中断,不是吗? - ysap
17
@ysap,1. 尽可能使用const关键字来标记变量,让我可以看出哪些部分是可变的,哪些部分是不可变的。根据我的经验,很多局部变量实际上是常量,而不是相反的情况。2. "Const variable"或者"Immutable variable"听起来好像是个矛盾的词组,但在函数式编程语言和一些非函数式编程语言中这是标准用法。例如 Rust 语言:https://doc.rust-lang.org/book/variable-bindings.html。 - Constantin
2
在某些情况下,lambda表达式[x](){return ++x;}在C++中现在是标准错误的;请参见此处 - anatolyg
23
在Rust中,变量默认为“const”。 :) - phoenix
5
当然,“常量”会这么说!抱歉,我很惊讶没有其他人说过这句话... - Joshua Jurgensmeier
显示剩余2条评论

96

从API的角度来看,过多的无意义const是不好的:

在代码中为传递的内在类型参数传递额外的无意义const会混淆您的API,同时对调用者或API用户没有任何有意义的承诺(它只会妨碍实现)。

当API中没有必要使用太多“const”时,就像“哭狼”一样,最终人们会开始忽略“const”,因为它到处都是,大部分时间都没有意义。

对于API中额外的常量的“归谬法”论据对这前两点的好处是,如果更多的常量参数是好的,那么每个可以有const的参数都应该有const。事实上,如果真的那么好,您希望const成为参数的默认值,并且只有在想要更改参数时才有像“mutable”这样的关键字。

因此,让我们尝试在所有可能的地方都加入const:

void mungerum(char * buffer, const char * mask, int count);

void mungerum(char * const buffer, const char * const mask, const int count);

考虑上面的代码行。不仅声明更加混乱、更长、更难以阅读,而且四个“const”关键字中有三个可以被API用户安全地忽略。然而,“const”的额外使用使第二行可能变得危险! 为什么呢?
对于第一个参数char * const buffer的快速误读可能会让您认为它不会修改传入的数据缓冲区中的内存——然而,这是不正确的!多余的“const”可能导致对您的API进行扫描或快速误读时产生危险和不正确的假设。
从代码实现的角度来看,多余的const也是不好的:
#if FLEXIBLE_IMPLEMENTATION
       #define SUPERFLUOUS_CONST
#else
       #define SUPERFLUOUS_CONST             const
#endif

void bytecopy(char * SUPERFLUOUS_CONST dest,
   const char *source, SUPERFLUOUS_CONST int count);

如果 FLEXIBLE_IMPLEMENTATION 不为真,则 API “承诺”不会使用下面的第一种方式来实现该函数。
void bytecopy(char * SUPERFLUOUS_CONST dest,
   const char *source, SUPERFLUOUS_CONST int count)
{
       // Will break if !FLEXIBLE_IMPLEMENTATION
       while(count--)
       {
              *dest++=*source++;
       }
}

void bytecopy(char * SUPERFLUOUS_CONST dest,
   const char *source, SUPERFLUOUS_CONST int count)
{
       for(int i=0;i<count;i++)
       {
              dest[i]=source[i];
       }
}

这是一个非常愚蠢的承诺。为什么要做出一项对您的呼叫者没有任何好处,只限制您的实现的承诺呢?

虽然这两种实现方式都是完全有效的,但您所做的只是不必要地把一只手绑在了背后。

此外,这是一个非常肤浅的承诺,很容易(且合法地)被规避。

inline void bytecopyWrapped(char * dest,
   const char *source, int count)
{
       while(count--)
       {
              *dest++=*source++;
       }
}
void bytecopy(char * SUPERFLUOUS_CONST dest,
   const char *source,SUPERFLUOUS_CONST int count)
{
    bytecopyWrapped(dest, source, count);
}

看,我无论如何都实现了它,尽管我承诺不这样做 - 只是使用一个包装函数。就像电影中坏人承诺不杀死某个人,却命令手下杀死他们一样。

那些多余的const与电影反派的承诺一样毫无价值。


但撒谎的能力变得更糟:

我已经意识到,通过使用虚假的const,您可以在头文件(声明)和代码(定义)中不匹配const。 热衷于const的倡导者声称这是一件好事,因为它让您只在定义中放置const。

// Example of const only in definition, not declaration
struct foo { void test(int *pi); };
void foo::test(int * const pi) { }

然而,相反的情况是真实的... 你可以在声明中添加一个虚假的const并在定义中忽略它。这只会使API中不必要的const变得更加糟糕和虚伪-参见以下示例:

struct foo
{
    void test(int * const pi);
};

void foo::test(int *pi) // Look, the const in the definition is so superfluous I can ignore it here
{
    pi++;  // I promised in my definition I wouldn't modify this
}

所有多余的const实际上只会使实现者的代码变得更加难以阅读,因为它强制他在想要更改变量或通过非const引用传递变量时使用另一个本地副本或包装函数。

看这个例子。哪个更容易理解?很明显第二个函数中额外变量的唯一原因是某个API设计者添加了一个多余的const。

struct llist
{
    llist * next;
};

void walkllist(llist *plist)
{
    llist *pnext;
    while(plist)
    {
        pnext=plist->next;
        walk(plist);
        plist=pnext;    // This line wouldn't compile if plist was const
    }
}

void walkllist(llist * SUPERFLUOUS_CONST plist)
{
    llist * pnotconst=plist;
    llist *pnext;
    while(pnotconst)
    {
        pnext=pnotconst->next;
        walk(pnotconst);
        pnotconst=pnext;
    }
}

希望我们从中学到了一些东西。过多的const会让API变得混乱不堪,烦人的催促,毫无意义的浅薄承诺,是一种不必要的阻碍,并且有时会导致非常危险的错误。


11
为什么会有负评?如果您在给出负评时留下简短的评论,那将更有帮助。 - Adisak
9
使用const参数的整个目的是为了使标记的行失败(plist = pnext)。这是一项合理的安全措施,以保持函数参数不可变。我同意您的观点,即在函数声明中它们是无用的(因为它们是多余的),但它们可以在实现块中发挥作用。 - touko
27
我认为您的回答本身没有任何问题,但是从您的评论中似乎缺少一个重要的观点。函数定义/实现不是API的一部分,只有函数声明才是API。正如您所说,使用const参数声明函数是毫无意义的,并且会增加混乱。然而,API的用户可能永远不需要看到它的实现。同时,实现者可能会决定仅出于清晰起见在函数定义中使用const修饰符来修饰某些参数,这是完全可以的。 - jw013
19
@jw013是正确的,void foo(int)void foo(const int)是完全相同的函数,不是重载。 ideone.com/npN4W4 ideone.com/tZav9R 这里的const只是函数体的实现细节,在重载解析上没有影响。在声明中省略const可以得到更安全、更整洁的API,但是在定义中加入const,如果你不打算修改复制的值。 - Oktalist
4
@Adisak,我知道这是老问题了,但我认为公共API的正确使用方式应该相反。这样,内部工作的开发人员不会在不应该执行操作时出错,例如pi++ - RamblingMad
显示剩余29条评论

95
以下两行代码具有相同的功能:

The following two lines are functionally equivalent:

int foo (int a);
int foo (const int a);

显然,如果a被第二种方式定义,你将无法在foo的主体中修改它,但从外部来看没有任何区别。

const真正方便的地方是引用或指针参数:

int foo (const BigStruct &a);
int foo (const BigStruct *a);

这句话的意思是 foo 可以接受一个大参数,可能是一种占用几个GB的数据结构,而不需要复制它。同时,它告诉调用者,“foo 不会改变该参数的内容”。传递一个const引用也允许编译器做出某些性能决策。

*:除非它强制转换掉 const 属性,但那是另一个帖子了。


4
这个问题不是关于这个的;当然,对于被引用或指向的参数,如果它们没有被修改,使用const是一个好主意。请注意,在你的指针示例中,并不是参数是const,而是参数指向的东西是const。 - tml
传递一个const引用还允许编译器做出某些性能决策。经典谬论 - 编译器必须自行确定const-ness,由于指针别名和const_cast的存在,const关键字对此并没有帮助。 - jheriko

38

在C ++中,const应该成为默认值。

int i = 5 ; // i is a constant

var int i = 5 ; // i is a real variable

27
兼容C语言对于设计C++语言的人来说太重要了,以至于他们甚至不会考虑这个问题。 - Roger Pate
4
有趣,我从来没有想过那个。 - Dan
7
同样地,在C++中,应该把 unsigned 作为默认值。就像这样:int i = 5; // i 是无符号的signed int i = 5; // i 是有符号的 - hkBattousai
@hkBattousai 为什么?const-by-default 有其合理性,但我在 unsigned-by-default 中没有看到它。事实上,Bjarne 认为 size() 返回 unsigned 类型是一个设计错误。在 span 的初始设计中,人们希望将 size() 设为 signed,但为时已晚——与容器的 size() 不兼容并不是大多数 C++ 标准委员会成员想要的。 - Yongwei Wu

24

当我以编写C++代码为生时,我会使用const来修饰所有可能需要的变量。使用const是帮助编译器更好地协助你的好方法。例如,对于方法返回值进行const修饰可以避免拼写错误,例如:

foo() = 42

当你想要表达的是:

foo() == 42

如果foo()被定义为返回非常量引用:

int& foo() { /* ... */ }

编译器会很乐意地让你给函数调用返回的匿名临时对象赋值。将其设为const:

const int& foo() { /* ... */ }

消除了这种可能性。


7
那是用什么编译器完成的? 尝试编译 foo() = 42 时 GCC 报错:error: lvalue required as left operand of assignment - gavrie
这只是不正确的。foo() = 42与2 = 3相同,即编译器错误。而返回一个const完全没有意义。对于内置类型它并没有任何作用。 - Josh
2
我遇到了const的使用,我可以告诉你,最终它会带来更多的麻烦而不是好处。提示:const int foo()int foo()是不同的类型,如果你正在使用函数指针、信号/槽系统或boost::bind等东西,那么这会给你带来很大的麻烦。 - Mephane
2
我已经更正了代码,包括引用返回值。 - Avdi
const int& foo()”和“int foo()”由于返回值优化,实际上是相同的吗? - Zantier

19

1. 根据我的评估,最佳答案是:

@Adisak的答案是我所评估的最佳答案。需要注意的是,这个答案在某种程度上之所以最好,是因为它不仅使用了合理且深思熟虑的逻辑,而且有着最充分的真实代码示例支持

2. 我自己的话(同意最佳答案):

  1. 对于按值传递的参数而言,添加const没有任何好处。它只会:
    1. 限制实现者每次想要更改源代码中的输入参数时必须进行复制(由于传递的是按值传递,因此这种更改不会产生任何副作用)。而且经常情况下,更改按值传递的输入参数用于实现函数,因此到处添加const可能会妨碍这一点。
    2. 不必要地将const添加到代码中,使得代码中到处都是const,从而分散注意力,远离确实需要安全代码的const
  2. 然而,在处理指针或引用时,const是至关重要的,必须使用它,因为它可以防止在函数外部进行持久更改时产生不需要的副作用,因此每个指针或引用在参数仅为输入时都必须使用const。仅对按引用或指针传递的参数使用const还有一个额外的好处,即使得这些参数是指针或引用变得非常明显。它是另一件可以突出并说“小心!任何带有const的参数都是引用或指针!”的事情。
  3. 我上面描述的情况在我工作过的专业软件组织中经常达成共识,并被认为是最佳实践。有时甚至规则是严格的:“永远不要对按值传递的参数使用const,但如果它们仅为输入,则始终对按引用或指针传递的参数使用它。”

3. 谷歌的话(同意我的和最佳答案):

(来自“谷歌C++样式指南”)

对于按值传递的函数参数,const 对调用者没有影响,因此不建议在函数声明中使用。请参阅TotW#109
在局部变量上使用 const 既不被鼓励也不被反对。
来源:Google C++ 风格指南的“Use of const”部分:https://google.github.io/styleguide/cppguide.html#Use_of_const。这实际上是一个非常有价值的部分,所以请阅读整个部分。
请注意,“TotW#109”代表“函数声明中有意义的const,也是一个有用的阅读材料。它提供的内容更具信息性和指导性,而非 Google C++ 风格指南关于 const 的规则引用,则基于背景情况添加,但由于提供的清晰度,上述的 const 规则已经被添加到 Google C++ 风格指南中。
还要注意,即使我在这里引用了 Google C++ 风格指南来证明我的立场,这并不意味着我总是遵循该指南或总是建议遵循该指南。他们推荐的一些东西就是明显的怪异,例如 他们用于“Constant Names”的kDaysInAWeek样式命名约定。然而,仍然需要指出,当世界上最成功和有影响力的技术和软件公司使用与我和其他人(如@Adisak)在此问题上支持我们观点的同样的理由时,这仍然是有用和相关的。
4. Clang 的 linter,clang-tidy,对此有一些选项:
A. 还值得注意的是,Clang 的 linter,clang-tidy,有一个选项 readability-avoid-const-params-in-decls在这里描述,以支持在代码库中强制执行不使用 const 作为传递按值函数参数的方法:
检查函数声明是否具有顶级 const 参数。
声明中的 const 值不会影响函数的签名,因此不应将其放在那里。
示例:
void f(const string);   // Bad: const is top level.
void f(const string&);  // Good: const is not top level.

为了完整和清晰起见,我自己添加了两个例子:

void f(char * const c_string);   // Bad: const is top level. [This makes the _pointer itself_, NOT what it points to, const]
void f(const char * c_string);   // Good: const is not top level. [This makes what is being _pointed to_ const]

B. 它还有这个选项:readability-const-return-type - https://clang.llvm.org/extra/clang-tidy/checks/readability-const-return-type.html

5. 我对如何撰写关于该问题的风格指南的务实方法:

我会简单地将以下内容复制并粘贴到我的风格指南中:

[COPY/PASTE START]

  1. 总是在参数为引用或指针时使用const,当它们所指的内容不想被改变时。这样,当一个变量通过引用或指针传递且没有const时,就可以明确表示该变量预计会被更改。在这种情况下,const可以防止函数外部意外产生副作用。
  2. 不建议在值传递的函数参数上使用const,因为对调用方没有影响:即使在函数中更改了变量,也不会在函数外部产生副作用。请参阅以下资源以获取额外的正当性和见解:
    1. “Google C++ Style Guide”中的“Use of const”章节
    2. “Tip of the Week #109: Meaningful const in Function Declarations”
    3. Adisak在Stack Overflow上关于“Use of 'const' for function parameters”的回答
  3. “永远不要在不是定义的声明中的函数参数上使用顶层const [即在传递的值上使用const] (并小心不要复制/粘贴无意义的const)。它是毫无意义的,并被编译器忽略,它是视觉噪音,可能会误导读者”(https://abseil.io/tips/109,强调添加)。
  4. 唯一对编译有影响的const限定符是放置在函数定义中的限定符,而不是头文件中函数(方法)声明中的那些。
  5. 永远不要在函数返回的值上使用顶层const [即在传递的值上使用const]。
  6. 在函数返回指针或引用时使用const取决于实现者,因为它有时很有用。
  7. TODO:使用以下的clang-tidy选项强制执行以上某些规则:
  8. https://clang.llvm.org/extra/clang-tidy/checks/readability-avoid-const-params-in-decls.html
  9. https://clang.llvm.org/extra/clang-tidy/checks/readability-const-return-type.html
这里有一些代码示例,用于演示上述const规则: const参数示例:
(部分内容来自此处
void f(const std::string);   // Bad: const is top level.
void f(const std::string&);  // Good: const is not top level.

void f(char * const c_string);   // Bad: const is top level. [This makes the _pointer itself_, NOT what it points to, const]
void f(const char * c_string);   // Good: const is not top level. [This makes what is being _pointed to_ const]

const返回类型示例:
(一些来自这里的借鉴)

// BAD--do not do this:
const int foo();
const Clazz foo();
Clazz *const foo();

// OK--up to the implementer:
const int* foo();
const int& foo();
const Clazz* foo();

[复制/粘贴结束]

关键字: 在函数参数中使用const; 编码标准; C和C++编码标准; 编码指南; 最佳实践; 代码标准; 常量返回值


15

在 comp.lang.c++.moderated 的旧版“每周大师”文章中,有一个关于这个主题的很好的讨论 在这里

相应的 GOTW 文章可以在 Herb Sutter 的网站上找到 在这里


1
Herb Sutter 是一个非常聪明的人 :-) 绝对值得一读,我同意他所有的观点。 - Adisak
2
很好的文章,但我不同意他关于参数的观点。我也会将它们声明为const,因为它们就像变量一样,我从不希望任何人更改我的参数。 - QBziZ

10

我在函数参数上使用const,这些参数是引用(或指针),仅为[in]数据,并且不会被函数修改。也就是说,当使用引用的目的是为了避免复制数据,而不是允许更改传递的参数时。

在你的示例中将const放在布尔型b参数上只对实现施加了限制,并没有为类的接口做出贡献(尽管通常建议不要更改参数)。

该函数的签名为

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

是相同的,这解释了你的 .c 和 .h 文件。

Asaf


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