内存引用重新绑定的可移植性

Portability of memory reference rebinding

看了C++中rebinding a reference的可能方式后,应该是illegal, I found a particularly ugly way of doing it. The reason I think the reference really gets rebound is because it does not modify the original referenced value, but the memory of the reference itself. After some more researching, I found a reference is not guaranteed才有内存,但是有的话,我们可以尝试使用代码:

#include <iostream>
using namespace std;

template<class T>
class Reference
{
public:
    T &r;

    Reference(T &r) : r(r) {}
};

int main(void)
{
    int five = 5, six = 6;

    Reference<int> reference(five);

    cout << "reference value is " << reference.r << " at memory " << &reference.r << endl;
    
    // Used offsetof macro for simplicity, even though its support is conditional in C++ as warned by GCC. Anyway, the macro can be hard-coded
    *(reinterpret_cast<int**>(reinterpret_cast<char*>(&reference) + offsetof(Reference<int>, r))) = &six;

    cout << "reference value changed to " << reference.r << " at memory " << &reference.r << endl;

    // The value of five still exists in memory and remains untouched
    cout << "five value is still " << five << " at memory " << &five << endl;
}

使用 GCC 8.1 并在 MSVC 中测试的示例输出是:

reference value is 5 at memory 0x7ffd1b4eb6b8

reference value changed to 6 at memory 0x7ffd1b4eb6bc

five value is still 5 at memory 0x7ffd1b4eb6b8

问题是:

  1. 上面的方法是否被认为是未定义的行为?为什么?
  2. 我们可以从技术上说参考得到反弹,即使它应该是非法的?
  3. 在实际情况下,当代码已经在特定机器上使用特定编译器工作时,上面的代码是否可移植(保证在每个操作系统和每个处理器中工作),假设我们使用相同的编译器版本?

以上代码有未定义的行为。 reinterpret_cast<int**>(…) 的结果实际上并不指向 int* 类型的对象,但您取消引用并覆盖该位置假设的 int* 对象的存储值,至少违反了严格的进程 [basic.lval]/11 中的别名规则。实际上,那个位置甚至没有任何类型的对象(引用不是对象)...

您的代码中恰好绑定了一个引用,这发生在 Reference 的构造函数初始化成员 r 时。在任何时候都不会将引用反弹到另一个对象。这似乎是有效的,因为编译器恰好通过一个字段来实现您的引用成员,该字段存储引用所引用的对象的地址,该地址恰好位于您的无效指针恰好指向的位置......

除此之外,我怀疑在参考成员上使用 offsetof 是否合法。即使是这样,您的代码的那部分也将 充其量 有条件地支持有效的实现定义的行为 [support.types.layout]/1, since your class Reference is not a standard-layout class [class.prop]/3.1 (它有一个引用类型的成员)。

因为你的代码有未定义的行为,它不可能是可移植的…

如其他答案所示,您的代码有UB。引用不能被重新定义——这是语言设计造成的,无论你尝试什么样的铸造技巧,你都无法绕过它,你仍然会得到 UB。

但是您可以使用 std::reference_wrapper:

重新绑定引用语义
int a = 24;
int b = 11;

auto r = std::ref(a); // bind r to a

r.get() = 5; // a is changed to 5

r = b; // re-bind r to b

r.get() = 13; // b is changed to 13

引用可以合法反弹,如果你跳过正确的圈:

#include <new>
#include <cassert>

struct ref {
  int& value;
};

void test() {
  int x = 1, y = 2;
  ref r{x};
  assert(&r.value == &x);
  // overwrite the memory of r with a new ref referring to y.
  ref* rebound_r_ptr = std::launder(new (&r) ref{y});
  // rebound_r_ptr points to r, but you really have to use it.
  // using r directly could give old value.
  assert(&rebound_r_ptr->value == &y);
}

编辑:godbolt link。你可以说它有效,因为函数总是 returns 1.