0

The c++ macro offsetof is just defined behaviour when used on standard layout types. As I understood this is because the compiler can change the memory layout of the data depending on the context of the code it runs. (For example when a variable is never used)

However, what I wondered is whether all elements stored in a range share the same layout. Or, in other words, if following code is well defined:

template<typename T>
concept has_member_int = requires(const T& x)
{
  { x.member } -> std::same_as<int>;
};

template <std::ranges::Range Range_t, has_member_int T>
void setEveryMemberTo20(Range_t<T> range)
{
  if (range.size() > 0)
  {
    auto& firstElement = *(range.begin());
    
    auto ptrdiffToMember = &(firstElement.member) - &firstElement;
    
    for (auto& element : range)
    {
      *(reinterpret_cast<int*>(&element + ptrdiffToMember)) = 20;
    }
  }
}
πάντα ῥεῖ
  • 1
  • 13
  • 116
  • 190
user3520616
  • 60
  • 3
  • 17
  • 2
    What's wrong with `element.member = 20;`? Are you interested in the case where types may be erased or the names of member variables are not known, and pointers-to-members for some reason are not viable? – alter_igel Jan 12 '21 at 23:09
  • @RemyLebeau yes, but arrays are a special kind of container, storing all data contiguous in memory. – user3520616 Jan 12 '21 at 23:10
  • @alterigel obviously the code I've posted is a minimal reproducible example illustrating the question I have – user3520616 Jan 12 '21 at 23:11
  • @user3520616 being in a contiguous array or not that doesn't change what I said. – Remy Lebeau Jan 12 '21 at 23:21
  • "if following code is well defined" -- well, it doesn't compile, so no. – Barry Jan 13 '21 at 00:30
  • 1
    Compilers don't tend to remove structure members that are unused. Such things would tend to break in a lot of real-world cases - for example, if a new object file is linked that now uses a previously unused member of a data structure. Elimination of unused variables tends to happen because all the information about usage of variables is visible in (say) a single function. That's not true for data structures that are used across multiple compilation units. – Peter Jan 13 '21 at 02:39

1 Answers1

3

what I wondered is whether all elements stored in a range share the same layout

Of course they do, otherwise traversing through multiple elements of the same type and accessing the same member of each element would be impossible. The offset of a given member is relative to the element's type. That offset is the same for all instances of that type. Thus, the combination of all the members within a type constitutes the type's layout, and that layout remains consistent across all uses of the type.

However, your handling of the member offset is all wrong. You are calculating the offset by subtracting a T* pointer from an int* pointer, which

  • should not even compile, as you can't subtract pointers of different types.
  • even if it did compile, that would not give you the correct byte offset of member within T.

And then you are applying that offset to a T* pointer, which will advance the pointer by that many T instances, not by that many bytes. IOW, if the offset of member within T is 4, you are advancing the T* pointer by sizeof(T) * 4 bytes, not by 4 bytes only.

I think you need to brush up on how pointer arithmetic actually works.

Try something more like this instead:

auto& firstElement = *(range.begin());
// or: T& firstElement = ...
    
auto ptrdiffToMember = reinterpret_cast<uintptr_t>(&firstElement.member) - reinterpret_cast<uintptr_t>(&firstElement);
// or: auto ptrdiffToMember = offsetof(T, member);
    
for (auto& element : range)
{
    *(reinterpret_cast<int*>(reinterpret_cast<uintptr_t>(&element) + ptrdiffToMember)) = 20;
}

But, as @alterigel stated in comments, just use element.member = 20; instead. You don't need to deal with pointer manipulation at all:

template <std::ranges::Range Range_t, has_member_int T>
void setEveryMemberTo20(Range_t<T> range)
{
  for (auto& element : range)
  {
    element.member = 20;
  }
}
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Arrays are a special kind of container, storing all data contiguous in memory. Your explanation doesn't fit on `std::map` for example. And to your last paragraph: obviously the code I've posted is a minimal reproducible example illustrating the question I have – user3520616 Jan 12 '21 at 23:17
  • "*Your explanation doesn't fit on std::map for example*" - yes, it does. Regardless of the type of container used, the byte offset of a given member inside a given type is always the same in all instances of that type. – Remy Lebeau Jan 12 '21 at 23:20
  • Okay, but you also stated that I could use "offsetof" . But in the standard it says that offsetof is just valid with standard layout types. Could it be that my example still stands, because it is in a strict context? The offset to the member is just valid in this scope? – user3520616 Jan 12 '21 at 23:24
  • See ["standard layout type"](https://en.cppreference.com/w/cpp/language/data_members#Standard_layout) and [`std::is_standard_layout`](https://en.cppreference.com/w/cpp/types/is_standard_layout) – Remy Lebeau Jan 12 '21 at 23:30
  • @user3520616 You're not wrong, and technically using `offsetof` with a non-standard-layout type has undefined behaviour up to and including C++17. In practice, then, you could get the wrong answer, or worse. This isn't because member offsets can be different for different instances (they can't); it's just because the standard doesn't require implementations to provide a well-defined `offsetof` in those cases. At a guess, a committee member concluded it would be too hard to implement to be a strict requirement. – Asteroids With Wings Jan 12 '21 at 23:32
  • "This isn't because member offsets can be different for different instances (they can't); it's just because the standard doesn't require implementations to provide a well-defined offsetof in those cases" But why is it not well-defined if the offset can not be different in different instances of the class? @AsteroidsWithWings – user3520616 Jan 12 '21 at 23:44
  • @user3520616 As I say, perhaps it was deemed too difficult to impose on compilers as a requirement. You'd really have to ask the committee. It doesn't matter anyway - you can't use `offsetof` in that case and that's that. Though if you're writing C++20, you're in luck, because `offsetof` is now conditionally-supported for non-standard-layout types! (In other words, a compiler may now opt to do it, no matter how hard it is, and it should be well-defined where provided.) – Asteroids With Wings Jan 12 '21 at 23:48
  • @AsteroidsWithWings yep I know that I can't use `offsetof` butmy question is if my code on top has defined behaviour for every container and type... – user3520616 Jan 12 '21 at 23:49
  • @user3520616 That I don't know. Too much `auto` and fishy-looking arithmetic... don't have time to examine it to find out what it does, sorry. – Asteroids With Wings Jan 12 '21 at 23:51
  • @user3520616 "*my question is if my code on top has defined behaviour*" - I already explained in my answer why it does not. – Remy Lebeau Jan 12 '21 at 23:51
  • @user3520616 if you fix the arithmetic the way I showed, then yes, it should. I don't have a C++20 compiler I can try it with, but here is a working example: https://onlinegdb.com/r1Q4A3iAv – Remy Lebeau Jan 13 '21 at 00:24