通过非const引用参数修改const引用参数

35

考虑以下代码:

#include <iostream>

void f(int const& a, int& b)
{
  b = a+1;
}

int main() {
  int c=2;
  f(c,c);
  std::cout << c << std::endl;
}
  • f接受两个引用类型参数:int const& aint& b。因此,f不应该修改a,但是可以修改b,实际上它确实这样做了。
  • 然而,在main函数中,我传递了同一个变量,分别由ab引用。由于f修改了b,也就修改了a,尽管不应该这样做。

这段代码编译时没有任何警告,并打印出3。如果我们单独跟踪每个变量,看起来const正确性得到了尊重:c是非const的,因此将其作为aconst引用以及作为非const引用的b是完全可以的,在f函数体内修改b(非const),同时不触及const的a。然而,当c被同时用作ab时,在f函数体内修改了a,违反了a是const的假设,即使没有明确调用const_cast

我尽可能简化了这个例子,但人们很容易想到不太明显的用例(例如在非const引用参数上执行const方法)。

我的问题是:

  • 我们真的可以说上面的代码是const正确的吗?
  • 上述用例是已知的模式吗?它被认为是一种不良实践吗?
  • 除了对读者来说令人困惑外,上述代码还可能是技术问题的源头吗?例如未定义的行为或编译器进行错误的简化假设?

如果您使用返回值而不是引用参数,会怎样呢? c = f(c); 看起来会很奇怪吗? - Fred Larson
我的猜测是:对于 f()const 的正确性是可以的。它做到了它承诺的不修改 a 并修改 b。滥用发生在 main() 中(对于 ab 使用同一变量)。你还能做什么:在 f() 中检查 &a != &b,并/或在 f() 的文档中标注相应的约束(如果必要)。 - Scheff's Cat
6
我认为这个教训是返回值比输出参数更清晰。建议使用返回值。 - Fred Larson
1
回答你的问题:是的。是的。是的,但不如使用返回值。对于程序员来说是的;对于编译器来说是否定的。不是和不是。 - Eljay
@Scheff,不,我认为这不会改变任何事情,在右手边的b的值将简单地是2,它将用于计算,结果将是b=5。我本可以写成b=3,我的问题仍然是一样的,我只是写了a+1,以便编译器不会抱怨未使用的变量。 - Gabriele Buondonno
显示剩余3条评论
4个回答

32

但是在main中,我传递了同一个变量,它被ab引用。由于f修改了b,它也修改了a,这本不应该发生。

f修改b所引用的内容时,并不会修改a。它只会修改a所引用的内容,但这没问题,因为b并不是const。只有当你试图通过使用a来修改a所引用的内容时才会出现问题。

我们真的能说上述代码是符合常量性正确的吗?

可以。您没有修改一个常量变量。

除了对读者具有迷惑性之外,上述代码可能成为技术问题的来源吗?例如未定义行为或编译器进行错误简化假设等。

否,您的代码是合法的,在所有符合规范的编译器上将产生相同的结果。


常量引用参数不会使其所引用的对象成为const,如果该对象一开始就没有被声明为const的话。它只会阻止您使用引用来修改对象。只要另一个指针或引用不是const本身,它仍然可以更改该对象。


顺便提一下,它表明额外的间接引用可能会让人困惑,而且有时效率也不高。 - Deduplicator

8
是的,这段代码是符合const正确性的,同时也没有未定义行为。你所写的是一个简单的引用别名情况,本质上可以归结为底层的指针别名
话虽如此,像这样的别名通常是不可取的,因为它确实更加复杂,无论是对程序员还是编译器来说。此外,这会阻止某些优化,特别是处理内存块的优化。

1
请注意,无论您是否实际上为指针设置别名,您所谈论的优化都将被阻止。由于编译器无法知道函数是否将使用别名参数调用,因此在为函数生成代码时,它总是必须假设最坏情况。这是Fortran(完全禁止指针别名)至今仍然比C和C ++具有的主要性能优势之一。 - ComicSansMS
除非编译器进行全局别名分析,据我所知,许多现代编译器可以做到这一点(甚至在不同的编译单元之前)。 - Hans Olsson

3

从API角度来看

void f(int const& a, int& b), 

f承诺不通过a引用修改任何内容,因此尊重对a进行的const修饰。它还向用户提示,另一方面,b很可能用于修改其所指对象;b可能会被记录为f的一个[in, out]参数或只是一个[out]参数。如果b实际上从未用于修改其所指对象,并且没有其他设计原因成为非const引用,那么这可能是f实现者(较弱的)破坏const的行为。

如何使用或滥用此API对于f本身是不直接关心的,特别是在其API设计选择已经完成后。然而,任何面向用户的API都应该被设计为在设计约束下最小化用户操作风险。例如,在这种情况下,可以使用值语义方法int f(int const& a)int f(int copy_in_a)来构建不同且更难受到滥用的接口。


1
这确实是一个不好的做法,因为许多程序员会(错误地)假设在调用期间int const& a的值确实是常量。请考虑。
void f(int const& a, int& b)
{
  b = a+1;
  if(something) b = a+2;
}

如果ab指向同一变量,那么看到b获取a+3的值将会非常惊讶。


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