顶层const不影响函数签名。

14

来自《C++ Primer 第五版》的说明:

int f(int){ /* can write to parameter */}
int f(const int){ /* cannot write to parameter */}

这两个函数是无法区分的。但是正如你所知道的,这两个函数在更新它们的参数的方式上确实存在差异。

有人可以解释一下吗?


编辑
我想我没有正确解释我的问题。我真正关心的是为什么C++不允许同时作为不同函数的这两个函数,因为它们确实不同,即“参数是否可以被写入”。直觉上,它应该是可以的!


编辑
按值传递的本质实际上是将参数值复制到参数变量中。即使对于引用指针,复制的值也是地址。从调用者的角度来看,无论传递给函数的是const还是non-const,都不影响复制到参数的值(当然也不影响类型)。
在复制对象时,顶层const底层const之间的区别很重要。更具体地说,由于复制不会影响复制的对象,因此忽略了顶层const(不是低层const的情况)。复制到或从中复制的对象是否为const并不重要。
因此,对于调用者来说,区分它们是没有必要的。很可能从函数的角度来看,顶层const参数不会影响函数的接口和/或功能。这两个函数实际上完成了同样的事情。为什么要实现两个副本?


4
对于呼叫者来说,它们是无法区分的。呼叫者不需要关心它们是否修改了自己的副本。 - chris
如果你在谈论引用或指针,那就是另一回事了。 - Cogwheel
2
如果你不加上“&”,那么const就没有用了,因为参数会被复制。在另一种情况下,int f(int &)int f(const int &)是可以区分的,并且有用的。 - WiSaGaN
@WiSaGaN 谢谢。另一个类似的情况是使用指针 *。 - Zachary
在这种情况下,请参阅https://dev59.com/-lDTa4cB1Zd3GeqPImCS - doctorlove
然而,您的本地样式规则可能规定,在函数内更改函数参数的值不是良好的实践,因此使用“const int”。 - MikeW
7个回答

12

由于“参数是否可写”这一点的确不同,因此应同时允许这两个函数作为不同的函数。直观上看,它应该是这样的!

函数重载基于调用者提供的参数。在这里,调用者可能提供一个const或非const值,但从逻辑上讲,这对被调用函数提供的功能没有任何区别。考虑以下情况:

f(3);
int x = 1 + 2;
f(x);

如果f()在这些情况下做不同的事情,那将会非常令人困惑!调用f()的代码编写者可以合理地期望相同的行为,自由添加或删除传递参数的变量而不会使程序无效。这种安全、理智的行为是你要证明异常的出发点,确实有一个异常——当函数重载时,行为可以被改变,例如:

void f(const int&) { ... }
void f(int&) { ... }

所以,我猜这就是你觉得不直观的地方:C++为非引用提供了比引用更多的“安全性”(通过仅支持单个实现来强制执行一致的行为)
我能想到的原因有:
  • 因此,当程序员知道非const&参数将具有更长的生命周期时,他们可以选择最佳实现。例如,在下面的代码中,返回FT成员的引用可能更快,但如果F是临时的(如果编译器匹配const F&),则需要按值返回。这仍然非常危险,因为调用者必须意识到返回的引用只在参数存在时有效。
    T f(const F&);
    T& f(F&);    // 如果更合适,返回类型可以是by const&
  • 如下所示,传播限定符(如const)通过函数调用:
    const T& f(const F&);
    T& f(F&);

在这里,基于调用f()时参数的const属性,某些(可能是F成员)类型为T的变量被公开为const或非const。当希望使用非成员函数扩展类时(保持类的最小化,或编写可用于许多类的模板/算法),可能会选择这种类型的接口,但这个想法与vector::operator[]()之类的const成员函数相似,其中您希望在非const向量上允许v[0] = 3,但不允许在const向量上进行操作。

当通过值接受值时,它们随着函数返回而超出作用域,因此不存在涉及返回参数一部分的引用并希望传播其限定符的有效情况。

修改所需的行为

给定引用的规则,您可以使用它们来获得所需的行为-您只需要小心不要意外修改非const引用参数,因此可能需要采用以下练习来处理非const参数:

T f(F& x_ref)
{
    F x = x_ref;  // or const F is you won't modify it
    ...use x for safety...
}

重新编译的影响

除了为什么语言禁止基于按值传递参数的const重载的问题之外,还有一个问题,即为什么它不坚持在声明和定义中一致使用const

对于f(const int)/f(int)...如果您在头文件中声明函数,则最好不要包括const限定符,即使后来在实现文件中的定义会有它。这是因为在维护期间,程序员可能希望删除限定符...从头文件中删除它可能会触发客户端代码的无意义重新编译,因此最好不要坚持保持它们同步 - 实际上这就是编译器如果它们不同就不会产生错误的原因。如果您只是在函数定义中添加或删除const,那么它就接近于实现,当分析函数行为时,代码读者可能会关心常量性。如果您在头文件和实现文件中都使用了const,那么程序员希望将其变为非const并且忘记或决定不更新头文件以避免客户端重新编译,那么它比另一种方式更危险,因为当尝试分析当前实现代码时,程序员可能会将头文件中的const版本记在心中,从而导致对函数行为的错误推理。这是一个非常微妙的维护问题 - 只有在商业编程中才真正相关 - 但这是不使用接口中的const的指导方针的基础。此外,从接口中省略它更加简洁,这对于阅读您的API的客户端程序员更加友好。


@Zack:假设你有f(9 + 4)或者f(g()),且已经定义了int g();...编译器会将参数视为临时对象,所以无法使用函数f(int&)。但是可以将非const引用与临时对象进行绑定,因此f(const int&)能够接收上述调用。 - Tony Delroy
从头文件中删除它可能会触发客户端代码的无意义重新编译。为什么头文件中没有const不需要重新编译客户端代码? - Zachary
1
@Zack:关键在于对头文件的任何更改都会触发重新编译,因此如果您想自由更改实现而不重新编译,则必须接受头文件有时会与定义不同步。如果您接受了这一点,那么最好让头文件说得更加宽松,因为程序员错误地假设它更安全/常量并错误地推断代码行为的空间更小。对于客户端编码人员来说,头文件也更加简洁 - 他们无论如何都不关心它是否是“const”,为什么要告诉他们呢? - Tony Delroy
您如何在实现中添加/删除const而无需重新编译客户端?我无法想象一个场景。您能帮忙吗? - Zachary
1
@Zack,你只需要重新链接客户端,除非它使用的是一个头文件中的实现。 - juanchopanza
显示剩余3条评论

6

既然对于调用者没有区别,也没有明显的方法来区分带有顶层const参数和不带的函数调用,因此语言规则会忽略顶层const。这意味着这两个函数:

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

如果您提供了两个实现,那么它们将被视为相同的声明。这将导致多重定义错误。

在带有顶层const的函数定义中存在差异。在其中一个中,您可以修改参数的副本。在另一个中,则不行。您可以将其视为实现细节。对于调用者而言,没有区别。

// declarations
void foo(int);
void bar(int);

// definitions
void foo(int n)
{
  n++;
  std::cout << n << std::endl;
}

void bar(const int n)
{
  n++; // ERROR!
  std::cout << n << std::endl;
}

这类比如下:

void foo()
{
  int = 42;
  n++;
  std::cout << n << std::endl;
}

void bar()
{
  const int n = 42;
  n++; // ERROR!
  std::cout << n << std::endl;
}

我知道你的观点。但是为什么C++不允许两个函数有相同的函数名(从调用者的角度来看)。直觉上,应该可以! - Zachary
如果两个函数都允许存在,那么你会如何解决它?无论哪个函数都可以被称为 foo(5) - n. m.
显然,Zack的期望是foo(5)会调用foo(const int),而foo(my_var)会调用foo(int)。编译器可以协调这一点并不难想象,有趣的问题是为什么语言不支持它。 - Tony Delroy
@juanchopanza:恕我直言,我认为我们这些老旧的C++程序员对此的直觉已经因为多年来知道它是非法的而扭曲了。一个微不足道的可能性是:就像你在foo(const int&)foo(int&)之间选择一样,选择在foo(const int)foo(int)之间进行选择。无论如何,我认为这是被禁止的,因为它是危险的并且没有用处(根据我的回答),而不是因为它很难实现或者很难理解。 - Tony Delroy
@n.m. (a) 不是真的...可以只在函数重载时触发路由规则-目前是非法的-因此不会影响现有代码 (b) 断言比这个问题所要求的内容更少用-特别是与const和非const引用的合法重载相比较,解释它更有用;-) - Tony Delroy
显示剩余4条评论

6
在《C++程序设计语言》第四版中,Bjarne Stroustrup写道(§12.1.3):“不幸的是,为了保持与C的兼容性,在参数类型的最高级别上忽略const。例如,下面是同一个函数的两个声明:”。
void f(int);
void f(const int);

因此,与其他答案不同的是,C++的这个规则并不是因为无法区分这两个函数或其他类似的原因而选择的,而是出于兼容性的考虑而采用了一个不太理想的解决方案。
实际上,在D编程语言中,可以有这两个重载。然而,与本问题的其他答案所暗示的相反,如果使用字面值调用函数,则更喜欢非const重载。
void f(int);
void f(const int);

f(42); // calls void f(int);

当然,您应该为您的重载提供等效的语义,但这并不特定于此重载场景,因为有着几乎无法区分的重载函数。

1
正如评论所说,如果第一个函数的参数被命名,它可以被更改。它是调用方int的副本。在第二个函数内,对仍然是调用方int的副本的参数进行任何更改都会导致编译错误。Const是一个承诺,你不会改变变量。

1

从调用者的角度来看,函数才是有用的。

由于对于调用者来说没有区别,因此这两个函数没有区别。


0

回答你问题的这一部分:

我真正关心的是,为什么C++不允许这两个函数同时作为不同的函数存在,因为它们在“参数是否可以被写入”方面确实是不同的。直觉上,应该是可以的!

如果你再想一想,这并不是很直观——事实上,它并没有多少意义。正如其他人所说,调用者在函数按值接收参数时根本不会受到影响,也不会在意。

现在,让我们假设重载决议也适用于顶层const。就像这样声明两个变量:

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

会声明两个不同的函数。其中一个问题是这个表达式会调用哪个函数:foo(42)。语言规则可能会说字面量是const,并且在这种情况下将调用const“overload”。但那只是一个小问题。 一个足够邪恶的程序员可以这样写:

int foo(const int i) { return i*i; }
int foo(int i)       { return i*2; }

现在你会有两个重载,它们在语义上对调用者来说是等效的,但实际上执行完全不同的操作。这将是很糟糕的。我们应该编写限制用户如何完成任务而不是提供什么功能的接口。


这真的一点也不直观,因为顶层const函数按值调用。所以从调用者的角度来看,它并不关心。 - Zachary
请参考@Tony D的答案。客户端代码只需要包含头文件。通常,在头文件中,函数声明中不使用_NO_ const - Zachary
@Zack TonyD已经解释得很好了,为什么会这样。但是如果语言按照你认为的方式工作,那么你就必须在头文件中使用const,就好像它是签名的一部分一样。 - jrok
const 只是一个限定符。它不包含函数签名。 - Zachary
1
@Zack 嗯,我知道。参数类型确实会有影响(当它不在顶层时,例如 const int&int&)。问题是,如果你希望语言将 int foo(const int)int foo(int) 视为两个不同函数的声明,则顶层的 const 将是(间接地,如果你愿意)签名的一部分。我不确定你的困惑来自何处,以及你想从回答者那里听到什么。 - jrok
显示剩余3条评论

0

我认为“indistinguishable”这个词是在重载和编译器方面使用的,而不是在调用者可以区分它们的方面。

编译器不区分这两个函数,它们的名称以相同的方式混淆。这导致编译器将这两个声明视为重新定义的情况。


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