内存引用重新绑定的可移植性
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
问题是:
- 上面的方法是否被认为是未定义的行为?为什么?
- 我们可以从技术上说参考得到反弹,即使它应该是非法的?
- 在实际情况下,当代码已经在特定机器上使用特定编译器工作时,上面的代码是否可移植(保证在每个操作系统和每个处理器中工作),假设我们使用相同的编译器版本?
以上代码有未定义的行为。 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.
看了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
问题是:
- 上面的方法是否被认为是未定义的行为?为什么?
- 我们可以从技术上说参考得到反弹,即使它应该是非法的?
- 在实际情况下,当代码已经在特定机器上使用特定编译器工作时,上面的代码是否可移植(保证在每个操作系统和每个处理器中工作),假设我们使用相同的编译器版本?
以上代码有未定义的行为。 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.