Can I manually access fields by their raw offset in C++?

Viewed 235

Does the following snippet utilize undefined/unspecified/etc. behavior?

#include <cstddef>
#include <iostream>
#include <string>

class Test {
    std::string s1{"s1"}, s2{"s2"};
    std::ptrdiff_t offset = (char*)(&s2) - (char*)(this);
public:
    std::string& get() { return *(std::string*)((char*)(this) + offset); }
};

int main() {
    Test test;
    std::cout << Test{test}.get(); // note the copy
}

The purpose of that offset is pointing at either s1 or s2 (chosen at runtime) and containing no special logic for copying/moving/accessing. std::string here is just an example of a non-trivial-anything class.

2 Answers

Your proposed solution contains multiple instances of Undefined Behavior related to pointer arithmetic.

First (char*)(&s2) - (char*)(this) is Undefined Behavior. This expression is governed by expr.add#5. Since the pointers aren't nullptr and they don't point to elements in the same array, the behavior is undefined.

Second ((char*)(this) + offset) is Undefined Behavior. This time the applicable paragraph is expr.add#4. Since (char*)(this) isn't an element of an array, the only legal value for offset would be 0. Any other value is Undefined Behavior.

But C++ already provides the tool necessary to solve the problem you are describing : pointer to data member. These pointers point to a member of a type instead of a member of an instance. It can be combined with a pointer to an instance (in this case a this pointer) to get a normal object pointer.

Here is your example modified to use a pointer to data member (https://godbolt.org/z/161vT158q) :

#include <cstddef>
#include <iostream>
#include <string>

class Test {
    std::string s1{"s1"}, s2{"s2"};

    // A pointer to an `std::string` member of the type `Test`
    using t_member_pointer = std::string Test::*;

    // Points to `Test::s2`
    t_member_pointer s_ptr = &Test::s2;

public:
    std::string& get() { 
        // Combine the data member pointer with an instance to get an object
        return (this->*s_ptr);
    }
};

int main() {
    Test test1;
    Test test2 = test1;
    std::cout << test2.get(); // note the copy
}

Notice that s_ptr points to Test::s2 and not this->s2. The value of a data member pointer is independent of any instance, it is compatible with any instance of that type. It therefore does not need to be corrected during copy or move, it will behave as expected if simply copied by value between instances.

No, the difference between two pointers is valid only for pointers from the same array:

Only pointers to elements of the same array (including the pointer one past the end of the array) may be subtracted from each other.

https://en.cppreference.com/w/cpp/types/ptrdiff_t

This doesn't hold for different members of a class.

Related