关于常量引用(和常量指针)的弱语义

On the weak semantics of references-to-const (and pointers-to-const)

这可能已经有人问过了。

为什么允许将常量引用分配给非常量变量?

为什么允许这样做

int mut {0};
const int & r_to_c {mut};
mut = 1;
// now r_to_c changed to 1!
// But it was supposed to be a reference to something constant!

?

当然,我不能改变常量引用本身的值。我不能

r_to_c = 2;

但是const资格执行的太少了吗?我希望,从 const-ness 的承诺来看,不允许绑定到可变变量。

不然const给我什么保证?他们看起来很弱,看来这很容易让程序员搬起石头砸自己的脚。

我知道 C++ 以允许人们搬起石头砸自己的脚而闻名。我对 允许 危险的事情没有意见。在这种情况下,我的问题是,在这种情况下,它似乎是故意欺骗,因为这里 const 的语义不是人们所期望的。

我的是关于编译器和语言语义的问题,而不是特别是关于引用的问题(我本可以使用分配给非常量变量地址的指向常量的指针来问同样的问题。喜欢 int mut{0}; const int * p_to_c{&mut};).

为什么常量引用(或常量指针)的语义只是“你不能使用这个特定的 window 来修改你看到的东西(但是如果你有其他非常量的windows,你可以修改它)”而不是更强大的“这只能是一个window到声明为常量并且编译器保证它的东西保持不变"?


[术语说明:我使用表达式“reference-to-const”而不是“const reference”,因为“const reference”解释为 T& const - 与调用 T* const 一致一个“常量指针”-,不存在。]

but isn't the const qualification enforcing too little? I would expect, from a promise of const-ness, that binding to mutable variables was disallowed.

不,这不是“太少”。你期待错了。

首先,是否绑定一个const引用并不会影响对象本身const。那会很奇怪:

void foo(int& x) {
    static const int& y = x;
}

当我调用foo时:

int x = 42;
foo(x);

我不知道其他人是否会保留 const 对我的非常量 x 的引用。

Otherwise what guarantees is const giving me?

您不能通过 const 参考修改某些内容:

void bar(const int& x);
int x = 0;
bar(x);

当我调用一个带有 const& 的函数时,我知道它不会修改我的(非常量)参数。如果 const 引用不会绑定到非常量对象,那么就没有办法使最后一个示例工作,即您只能将非常量对象传递给修改它们的函数,而不是传递给不修改它们的函数。

P.S。我能理解你的困惑。有时会忽略持有常量引用并不意味着对象不能被修改。考虑这个例子:

#include <cstddef>
#include <iostream>

struct foo {
    const int& x;
};

int main() {
    int y = 0;
    foo f{x};
    std::cout << f.x;   // prints 0
    y = 42;
    std::cout << f.x;   // prints 42
}

将成员的值打印到屏幕上会产生两种不同的结果,即使 foo::x 是一个常量引用!它是“常量引用”而不是“对常量的引用”。 const 在这里的实际含义是:您不能通过 f.x.

修改 y

将常量引用绑定到可变变量的能力实际上是该语言中一个非常有价值的特性。考虑到我们可能想要一个可变变量。

int mut {0};
// ... some time later
mut = 1;

这是完全合理的;它是一个在程序执行期间会发生变化的变量。

现在假设我们要打印此变量的值,并想编写一个函数来执行此操作。

void print(int param)  // or 'int &' to avoid a copy, 
                       // but the point here is that it's non-const 
{
  std::cout << param;
}

很好,但显然函数没有更改参数。我们希望强制执行此操作,以便编译器捕获 param = 42; 之类的错误。为此,我们将使 param 成为 const & 参数。

void print(int const & param);

如果我们不能用非常量参数调用这个函数,那将是非常不幸的。毕竟,我们不关心参数可能在函数外被修改。我们只想说参数保证不会被 print 修改,而将 const & 绑定到可变变量正是为了这个目的。

对 const 对象的引用与对非 const 对象的 const 引用不同,但 C++ 的类型系统不区分它们。

这有点违反 LSP;它与对可变正方形和矩形的引用有同样的问题。

您可以创建“真正的常量”,但在声明时需要帮助。

template<class T>
struct true_const {
  const T value;
};

a true_const<int>&true_const<T> const& 可以作为参考传递,没有人可以“在背后”编辑它(不调用 UB)。

当然,一个取真const的函数不能同时取一个普通对象。

void bob( true_const<int>& x ) {
  auto local = x.value;
  call_some_other_function();
  assert(local == x.value); // guaranteed to be true
}

void bob( const int& x ) {
  auto local = x;
  call_some_other_function();
  assert(local == x); // NOT guaranteed to be true
}

const classes 中的字段是真正的 const;修改它们是未定义的行为。

class 中 const 类型的薄包装因此可以保证数据是 const。然后参考那个。

现在,true_const 可以使用一些运算符支持。

template<class T>
struct true_const {
  const T value;
  constexpr T const& get() const { return value; }
  constexpr T const& operator*() const { return get(); }
  constexpr T const* operator->() const { return std::addressof(value); }
  constexpr operator T const&() const { return get(); }

  // concepts-defended operator+,==, etc
};