const参数是“真正”的常量吗?

10

据我所知,从常量变量中移除const属性是未定义行为:

const int i = 13;
const_cast<int&>(i) = 42;      //UB
std::cout << i << std::endl;   //out: 13

但是const函数参数是“真正”的常量吗?让我们考虑以下示例:

void foo(const int k){
    const_cast<int&>(k) = 42;    //UB?
    std::cout << k << std::endl;
}

int main(){
    foo(13); //out: 42
}

看起来编译器对 const int kconst int i 没有应用相同的优化。

第二个示例中是否存在未定义行为?const int k 是否有助于编译器优化代码,还是编译器只检查 const 的正确性而已?

示例


4
@Acorn,不,UB允许编译器忽略一些边界情况并生成更有效的代码。而且,const 有助于优化 很多。 - yrHeTateJlb
3
橡树儿优化与未定义行为有很大关系。这是他们能够对代码进行假设的重要部分,比如利用严格别名来避免加载命中存储。 - user4442671
1
@Acorn 我认为 const 在一定程度上会有助于优化,可以说很多。https://youtu.be/zBkNBP00wJE?t=26m55s - user2486888
2
@yrHeTaTeJlb:抱歉,您似乎没有理解。UB提供了优化的机会,但优化与某些东西是否为UB无关。 - Acorn
3
你知道我们告诉人们,表现出“工作”是UB最糟糕的表现方式吗?就是那个意思。 - StoryTeller - Unslander Monica
显示剩余8条评论
3个回答

5
在“const int i = 13;”中,“i”的前缀“const”意味着它可以用作常量表达式(如模板参数或case标签),并且试图修改它将导致未定义的行为。这是为了向后兼容没有“constexpr”的C++11之前的代码。
声明“void foo(const int k);”和“void foo(int k);”是声明相同函数;参数的顶层“const”不参与函数的签名。参数“k”必须按值传递,因此不能是“真正”的常量。取消其常量性不是未定义的行为。但是,任何尝试修改它仍然是未定义的,因为它是const对象[basic.type.qualifier](1.1):
“const对象是类型为const T的对象或此类对象的非可变子对象。”
根据[dcl.type.cv] 4,const对象无法修改:
“除非任何声明为mutable(10.1.1)的类成员都可以修改,在其生命周期(6.8)期间尝试修改const对象的任何尝试都会导致未定义的行为。”
由于函数参数具有自动存储期,因此其存储中不能创建新对象,也不能通过[basic.life] 10创建曾经占据其存储的const对象的存储中的新对象:
“在完全静态、线程或自动存储期的const对象所占用的存储器内创建一个新对象,或者在其生命周期结束之前曾经占用这样的const对象的存储器内创建一个新对象,都会导致未定义的行为。”
如果计划取消其常量性,那么不清楚为什么首先要将“k”声明为“const”?它唯一的目的似乎是混淆和使模糊不清。
通常我们应该在任何地方都支持不可变性,因为它有助于人们推理。此外,它可能有助于编译器进行优化。然而,在我们仅声明不可变性但不尊重它的情况下,它起到相反的作用并且会引起混淆。
我们应该支持的另一件事是纯函数。这些函数不依赖或修改任何外部状态,也没有副作用。这些函数对人和编译器来说都更容易推理和优化。当前这样的函数可以声明为“constexpr”。但是,据我所知,将按值传递的参数声明为“const”在优化方面并没有帮助,即使在“constexpr”函数的上下文中也是如此。

实际上,这可能会帮助而不是混淆。McConnell在他的《代码大全》中说,函数参数应该是不可变的,这样可以使代码更清晰。但我问这个问题是因为我的同事认为void foo(const int k)有助于编译器优化代码。对我来说,他的说法是可疑的。 - yrHeTateJlb
@yrHeTaTeJlb 我不同意麦康奈尔的观点:如果参数是副本,那么它们是否是不可变的并不重要。特别是如果解决方法是再次复制它们并改变该副本。 - Caleth
@Caleth,关键在于参数const int size表示整个函数体中_smth._的大小。您不需要怀疑是否已更改某些参数。 - yrHeTateJlb
@yrHeTaTeJlb 我所说的“混淆和模糊”是指当您声明const但随后将其强制转换时,这可能是混淆的主要来源。这比一开始没有const要糟糕得多。我已更新答案。 - Öö Tiib
要明确的是:强制转换去除const属性并不是未定义行为。后续对其进行赋值才会引起未定义行为。 - Andre Kostur
显示剩余2条评论

4

但是常量函数参数是否是“真正的”常量呢?

我认为答案是是。

它们不会被存储在ROM中,所以去掉const修饰符至少表面上看起来没有问题。但是根据标准,该参数的类型是const int,因此它是一个const对象([basic.type.qualifier]),因此修改它是未定义的([dcl.type.cv])。

您可以相对容易地确认参数的类型:

static_assert( std::is_same_v<const int, decltype(k)> );

第二个例子中是否存在未定义行为(UB)?

是的,我认为存在。

const int k 是否有助于编译器优化代码?或者编译器只是检查 const 的正确性而已?

理论上,编译器可以假设在以下示例中,调用 g(const int&) 不会改变 k,因为修改 k 会导致 UB:

extern void g(const int&);

void f(const int k)
{
  g(k);
  return k;
}

但实际上,我认为编译器并没有利用它的优势,而是假设k可能被修改(编译器探索演示)。


0

我不确定你所说的“真正”的常量是什么意思,但我会解释一下。

你的const int i是一个函数外的变量声明。由于修改该变量会导致未定义行为,编译器可以假设它的值永远不会改变。其中一个简单的优化是,在任何读取i的地方,编译器都不必去读取主存中的值,而是可以发出汇编指令直接使用该值。

你的const int k(或更有趣的const int & k)是一个函数参数。它只承诺这个函数不会改变k的值。这并不意味着它不能在其他地方被改变。每次调用函数时,k的值可能不同。


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