12

从 C++ Primer 第 5 版开始,它说:

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

这两种功能是无法区分的。但是如您所知,这两个函数在更新参数的方式上确实不同。

有人可以向我解释吗?


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


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

4

7 回答 7

11

允许这两个函数同时作为不同的函数,因为它们对于“是否可以写入参数”确实不同。直觉上应该是!

函数的重载基于调用者提供的参数。在这里,调用者确实可以提供一个值const或非const值,但从逻辑上讲,它应该对被调用函数提供的功能没有影响。考虑:

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

如果f()在每种情况下都做不同的事情,那将是非常混乱的!此代码调用的程序员f()可以对相同的行为有合理的期望,自由添加或删除传递参数的变量,而不会使程序无效。这种安全、理智的行为是您想要证明例外合理的出发点,并且确实有一个 - 当函数重载时行为可以改变ala:

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

所以,我想这就是你觉得不直观的:C++ 为 non-references 提供了比 references 更多的“安全性”(通过仅支持单个实现来强制执行一致的行为)

我能想到的原因是:

  • 因此,当程序员知道非const&参数将具有更长的生命周期时,他们可以选择最佳实现。例如,在下面的代码中,返回对T内部成员的引用可能会更快F,但如果F是临时的(如果编译器匹配,则可能是const F&),则需要按值返回。这仍然非常危险,因为调用者必须知道返回的引用仅在参数存在时才有效。
    T f(const F&);
    T&f(F&); // 如果更合适,返回类型可以是 const&
  • const通过函数调用传播诸如 -ness之类的限定符,如下所示:
    常量 T& f(常量 F&);
    T&f(F&);

在这里,一些(可能是F成员)类型的变量在被调用时T被暴露为const或非const基于const参数的 -ness f()。当希望扩展具有非成员函数的类时(以保持类极简,或编写可在许多类上使用的模板/算法时),可能会选择这种类型的接口,但这个想法类似于const成员函数vector::operator[](),如您想要的v[0] = 3允许在非const向量上,但不允许在一个向量上const

当值被值接受时,它们会在函数返回时超出范围,因此不存在涉及返回对部分参数的引用并希望传播其限定符的有效场景。

破解你想要的行为

给定引用规则,您可以使用它们来获得所需的行为类型 - 您只需要注意不要意外修改 by-non-const-reference 参数,因此可能需要采用如下做法非常量参数:

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

重新编译的影响

除了为什么语言基于按值参数的 -ness 禁止重载的问题之外const,还有一个问题是为什么它不坚持const声明和定义中 -ness 的一致性。

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

于 2013-06-20T08:35:00.457 回答
5

由于调用者没有区别,并且没有明确的方法来区分对具有顶级 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;
}
于 2013-06-20T08:19:41.017 回答
3

在“C++ 编程语言”第四版中,Bjarne Stroustrup 写道(第 12.1.3 节):

不幸的是,为了保持 C 兼容性,在参数类型的最高级别忽略了 const。例如,这是同一函数的两个声明:

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

因此,似乎与其他一些答案相反,之所以选择 C++ 的这条规则,是因为这两个函数的不可区分性或其他类似的基本原理,而是作为一个不太理想的解决方案,为了兼容性。

实际上,在D编程语言中,可能有这两个重载。然而,与这个问题的其他答案可能暗示的相反,如果使用文字调用函数,则首选非常量重载:

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

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

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

于 2013-12-01T16:10:42.300 回答
1

函数仅从调用者的角度有用。

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

于 2013-06-20T08:33:29.567 回答
1

正如评论所说,在第一个函数中,如果已命名参数,则可以更改参数。它是被调用者的 int 的副本。在第二个函数内部,对参数的任何更改(仍然是被调用者的 int 的副本)都将导致编译错误。Const 承诺您不会更改变量。

于 2013-06-20T08:19:14.223 回答
0

回答您问题的这一部分:

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

如果你再想一想,它一点也不直观——事实上,它没有多大意义。正如其他人所说,当函数按值获取其参数并且它也不在乎时,调用者绝不会受到影响。

现在,让我们暂时假设重载决议也适用于顶层const。两个这样的声明

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

将声明两个不同的函数。问题之一是该表达式将调用哪些函数:foo(42). 语言规则可以说文字是 const 并且在这种情况下将调用 const “重载”。但这是最小的问题。一个感觉足够邪恶的程序员可以这样写:

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

现在你有两个重载,它们在语义上与调用者等效,但做的事情完全不同。现在那会很糟糕。我们将能够编写界面来限制用户做事的方式,而不是他们提供的东西。

于 2013-06-20T08:51:03.247 回答
0

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

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

于 2013-06-20T08:37:58.157 回答