9

This is a slightly esoteric question, but I was curious whether the following class extension pattern is legal (as in, does not constitute UB) in modern C++ (for all intends and purposes I am fine with restricting the discussion to C++17 and later).

template<typename T>
struct AddOne {
    T add_one() const {
        T const& tref = *reinterpret_cast<T const*>(this);
        return tref + 1;
    }
};

template<template<typename> typename  E, typename T>
E<T> const& as(T const& obj) {
    return reinterpret_cast<E<T> const&>(obj);
} 

auto test(float x) {
    return as<AddOne>(x).add_one();
}

auto test1(int x) {
    return as<AddOne>(x).add_one();
}

// a main() to make this an MVCE
// will return with the exit code 16
int main(int argc, const char * argv[]) {
  return test1(15);
}

The above code is a complete example, it compiles, runs and produces the expected result with at least clang in C++17 mode. Check the disassembly code on compiler explorer: https://godbolt.org/z/S3ZX2Y

My interpretation is as follows: the standard states that reinterpret_cast can convert between pointers/references of any types, but that accessing there resulting references might be UB (as per to aliasing rules). At the same time, converting the resulting value back to the original type is guaranteed to yield the original value.

Based on this, merely reinterepting a reference to float as a reference to AddOne<float> does not invoke UB. Since we never attempt to access any memory behind that reference as instance of AddOne<float>, there is no UB here either. We only use the type information of that reference to select the correct implementation of add_one() member function. The function itself coverts the reference back to the original type, so again, no UB. Essentially, this pattern is semantically equivalent to this:

template<typename T>
struct AddOne {
   static T add_one(T const& x) {
      return x + 1;
   }
};

auto test(float x) {
  return AddOne<Int>::add_one(x);
}

Am I correct or is there something I miss here?

Consider this as an academic exercise in exploring the C++ standard.

Edit: this is not a duplicate of When to use reinterpret_cast? since that question does not discuss casting this pointer or using reinterpret_cast to dispatch on the reinterpreted type.

MrMobster
  • 1,851
  • 16
  • 25
  • 1
    I do not think this is a duplicate, OP knows what reinterpret cast does and is asking about this specific case that the linked question does not answer very clearly. – Quimby Mar 02 '19 at 11:26
  • I am not sure why my question was closed as duplicate of https://stackoverflow.com/questions/573294/when-to-use-reinterpret-cast The case I describe here is not part of that discussion. I've edited my question to point out the difference. – MrMobster Mar 02 '19 at 11:30
  • @M.M I have added a link to Compiler Explorer that that this code compiles correctly. Not sure what you mean with "this is not defined" — its the built-in instance pointer. – MrMobster Mar 02 '19 at 12:01
  • See [How to create a Minimal, Complete, and Verifiable example](https://stackoverflow.com/help/mcve), as it stands the code might be correct so long as none of the functions are called – M.M Mar 02 '19 at 12:06
  • @M.M There is a class definition, its `struct AddOne`? The code I have posted above is 100% complete and compiles with both clang and gcc correctly. If you want, I can add a trivial main that does nothing, but how would that be helpful? – MrMobster Mar 02 '19 at 12:06
  • @M.M Ok, I've added a main() function that invokes the entire machinery – MrMobster Mar 02 '19 at 12:12
  • Is `struct AddOne` big enough to hold the result of the reinterpret cast? It doesn't appear to have a member which *could* alias with the result of the cast on the first place. – Ext3h Mar 02 '19 at 12:16
  • @Ext3h the struct template itself has no data members at all. Neither does it need to be big enough since nothing is ever stored within that struct. In fact, there is never an instance of the struct that exists. The struct itself is just a dispatch mechanism for type-casted data and is not meant to ever be used separately. I suppose one would need to delete the default constructor to make it "safe". – MrMobster Mar 02 '19 at 12:27
  • `tref + 1` sure looks like it accesses memory – M.M Mar 02 '19 at 12:39
  • @M.M, yes, but it accesses the original `int` value, not any instance of `AddOne` – MrMobster Mar 02 '19 at 12:41
  • A call to a non-static member function constitutes access to `*this`. It doesn't matter what the function actually does. – n. m. could be an AI Mar 02 '19 at 12:43
  • @n.m. That is what I am curious about. Can you point me to the relevant excerpt in the standard? – MrMobster Mar 02 '19 at 12:49
  • 2
    \[basic.life] "The program has undefined behavior if: ... the glvalue is used to call a non-static member function of the object". – n. m. could be an AI Mar 02 '19 at 13:56

2 Answers2

10

No, that's definitely not legal. For a number of reasons.

The first reason is, you've got *this dereferencing an AddOne<int>* which doesn't actually point to an AddOne<int>. It doesn't matter that the operation doesn't really require a dereference "behind the scenes"; *foo is only legal if foo points to an object of compatible type.

The second reason is similar: You're calling a member function on an AddOne<int> which isn't. It likewise doesn't matter that you don't access any of AddOne's (nonexistent) members: the function call itself is an access of the object value, running afoul of the strict aliasing rule.

Sneftel
  • 40,271
  • 12
  • 71
  • 104
  • Good point about `*this`. So what if I change it to `T const& tref = *reinterpret_cast(this);`? No pointer to `AddOne` is being dereferenced here. As to the second part, that's the core of the question I think. Does the mere fact of the member function call break the aliasing rules or does it only rely on type-based name dispatch and therefore is not subject to aliasing rules? I guess what I am looking for is some confirmation for one or other interpretation from the standard. – MrMobster Mar 02 '19 at 12:47
  • Yes, moving the dereference outside the cast expression would fix the first problem. For the second problem, the standard doesn't draw a relevant distinction: `.` and `->` are "class member access", regardless of whether they're accessing a function member or a data member. (That's not necessarily just a technicality. Consider that the compiler may make certain assumptions about `this`, e.g. based on its alignment.) – Sneftel Mar 02 '19 at 12:58
0

The full answer was provided by user @n.m in the comments, so I will copy it here for the sake of completeness.

A paragraph from [basic.life] c++ standard sections states:

Before the lifetime of an object has started [...] The program has undefined behavior if [...] the pointer is used to access a non-static data member or call a non-static member function of the object

As to seems, this prohibits dispatch via the reinterpreted reference since that reference does not refer to a live object.

MrMobster
  • 1,851
  • 16
  • 25
  • Answer by Sneftel makes essentially the same statement, but it lacks the reference to the standard, which is why I didn't mark it as accepted. – MrMobster Mar 05 '19 at 11:35