0

After reading about possible ways of rebinding a reference in C++, which should be 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 to have memory, but when it does have, we can try to use the code:

#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;
}

A sample output using GCC 8.1, but also tested in MSVC, is:

reference value is 5 at memory 0x7ffd1b4eb6b8

reference value changed to 6 at memory 0x7ffd1b4eb6bc

five value is still 5 at memory 0x7ffd1b4eb6b8

The questions are:

  1. Is the method above considered undefined behavior? Why?
  2. Can we technically say the reference gets rebound, even though it should be illegal?
  3. In a practical situation, when the code has already worked using a specific compiler in a specific machine, is the code above portable (guaranteed to work in every operational system and every processor), assuming we use the same compiler version?
Community
  • 1
  • 1
Nighteen
  • 731
  • 1
  • 7
  • 16
  • If you need to rebind, instead use a pointer. That's one of the things that a pointer can do. The voodoo given here is bad mojo. – Eljay Apr 16 '19 at 22:55
  • @Eljay The problem comes when I need to modify/adapt 3rd party code. If I try to modify the header, I will have to recompile all its dependencies, which sometimes can lead to problems. I will also have to change every usage of the reference variable to pointer notation and I would possibly have to add checks for null pointer too. A solution would be to use reference_wrapper, but that would still require more work than a simple memory operation, since the memory operation can be done without modifying other people's work. – Nighteen Apr 16 '19 at 23:04

3 Answers3

1

Above code has undefined behavior. The result of your reinterpret_cast<int**>(…) does not actually point to an object of type int*, yet you dereference and overwrite the stored value of the hypothetical int* object at that location, violating at least the strict aliasing rule in the process [basic.lval]/11. In reality, there is not even an object of any type at that location (references are not objects)…

Exactly one reference is being bound in your code and that happens when the constructor of Reference initializes the member r. At no point is a reference being rebound to another object. This simply appears to work due to the fact that the compiler happens to implement your reference member via a field that stores the address of the object the reference is refering to, which happens to be located at the location your invalid pointer happens to point to…

Apart from that, I would have my doubts whether it's even legal to use offsetof on a reference member to begin with. Even if it is, that part of your code would at best be conditionally-supported with effectively implementation-defined behavior [support.types.layout]/1, since your class Reference is not a standard-layout class [class.prop]/3.1 (it has a member of reference type).

Since your code has undefined behavior, it cannot possibly be portable…

Michael Kenzel
  • 15,508
  • 2
  • 30
  • 39
  • Even if I use the same compiler and change only the OS/Processor, the code can stop working? – Nighteen Apr 17 '19 at 00:58
  • @Nighteen of course, unless your compiler's documentation specifically states otherwise. As far as the C++ standard is concerned, this code has undefined behavior… – Michael Kenzel Apr 17 '19 at 01:10
0

As shown in the other answer, your code has UB. A reference cannot be re-boud - this is by language design and no matter what kind of casting trickery you try you cannot get around that, you will still end up with UB.

But you can have re-binding reference semantics with 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
bolov
  • 72,283
  • 15
  • 145
  • 224
0

References can be rebound legally, if you jump through the right hoops:

#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);
}

Edit: godbolt link. You can tell that it works because the function always returns 1.

Filipp
  • 1,843
  • 12
  • 26