7

(This is another question about undefined behaviour (UB). If this code 'works' on some compiler, then that means nothing in the land of UB. That is understood. But exactly at what line below do we cross into UB?)

(There are a number of very similar questions on SO already, e.g. (1) but I'm curious what can be safely done with the pointers before dereferencing them.)

Start off with a very simple Base class. No virtual methods. No inheritance. (Maybe this can be extended to anything that's POD?)

struct Base {
        int first;
        double second;
};

And then a simple extension that adds (non-virtual) methods and doesn't add any members. No virtual inheritance.

struct Derived : public Base {
        int foo() { return first; }
        int bar() { return second; }
};

Then, consider the following lines. If there is some deviation from defined behaviour, I'd be curious to know which lines exactly. My guess is that we can safely perform much of the calculations on the pointers. Is it possible that some of these pointer calculations, if not fully defined, at least give us some sort of 'indeterminate/unspecified/implementation-defined' value that isn't entirely useless?

void foo () {
    Base b;
    void * vp = &b;     // (1) Defined behaviour?
    cout << vp << endl; // (2) I hope this isn't a 'trap value'
    cout << &b << endl; // (3a) Prints the same as the last line?
                        // (3b) It has the 'same value' in some sense?
    Derived *dp = (Derived*)(vp);
                        // (4) Maybe this is an 'indeterminate value',
                        // but not fully UB?
    cout << dp << endl; // (5)  Defined behaviour also?  Should print the same value as &b

Edit: If the program ended here, would it be UB? Note that, at this stage, I have not attempted to do anything with dp, other than print the pointer itself to the output. If simply casting is UB, then I guess the question ends here.

                        // I hope the dp pointer still has a value,
                        // even if we can't dereference it
    if(dp == &b) {      // (6) True?
            cout << "They have the same value. (Whatever that means!)" << endl;
    }

    cout << &(b.second) << endl; (7) this is definitely OK
    cout << &(dp->second) << endl; // (8)  Just taking the address. Is this OK?
    if( &(dp->second) == &(b.second) ) {      // (9) True?
            cout << "The members are stored in the same place?" << endl;
    }
}

I'm slightly nervous about (4) above. But I assume that it's always safe to cast to and from void pointers. Maybe the value of such a pointer can be discussed. But, is it defined to do the cast, and to print the pointer to cout?

(6) is important also. Will this evaluate to true?

In (8), we have the first time this pointer is being dereferenced (correct term?). But note that this line doesn't read from dp->second. It's still just an lvalue and we take its address. This calculation of the address is, I assume, defined by simple pointer arithmetic rules that we have from the C language?

If all of the above is OK, maybe we can prove that static_cast<Derived&>(b) is OK, and will lead to a perfectly usable object.

Community
  • 1
  • 1
Aaron McDaid
  • 26,501
  • 9
  • 66
  • 88
  • I've changed my question title slightly, so that I'm asking more about various pointer operations. The cast to `static_cast(b)` is perhaps too controversial to put directly in the title! – Aaron McDaid Nov 02 '13 at 12:26
  • Is multiple inheritance out of the scope of the question? What about if the derived class introduces virtual methods? These could change the outcome. – Luchian Grigore Nov 02 '13 at 12:26
  • 1, 2, 3a=good. 3b I think it's implementation defined (as printing pointers in general); if instead you casted one of them to the other's type (the only way to compare them) and checked with == they should be the same. 4 is UB due to strict aliasing rules; anything that depends on such a cast is doomed to be UB. Instead, if you use a union in C++11 you should be safe, since you are working with standard layout classes with a compatible layout. – Matteo Italia Nov 02 '13 at 12:41
  • (sorry, I mixed up the 3s; 3a=implementation defined, 3b "more or less" true (a == comparison should print true)) – Matteo Italia Nov 02 '13 at 12:49
  • @LuchianGrigore, if multiple inheritance can be added, without making things more difficult, then that's good. In the question, I ruled out `virtual` methods. For inheritance, I guess I'd need to rule out [`virtual`-inheritance](http://stackoverflow.com/questions/21558/in-c-what-is-a-virtual-base-class) also. My goal is to keep things simple at first in order to push the 'defined' behaviour as far as possible. – Aaron McDaid Nov 02 '13 at 13:04
  • @MatteoItalia, "anything that depends on such a cast is doomed to be UB." Performing the cast itself is defined, then? And printing the address to `cout` is defined? Perhaps when you said "4 is UB", you meant to say "8 is UB" - it's not until (8) that I actually 'depend on the result of the cast'? If you clearly state that the cast itself is UB, that's cool! I'm just looking for clarity. Thanks! – Aaron McDaid Nov 02 '13 at 13:10
  • I'm not so sure that *strict aliasing* applies here. The two types are not entirely incompatible, as clearly we can cast from `Derived*` to `Base*`, therefore the compiler already knows that such pointers can point to the same area (i.e. 'alias' each other.) Of course, I'm turning the argument around here, casting in the other direction. But I don't think strict-aliasing cares about direction? Thoughts? – Aaron McDaid Nov 02 '13 at 14:37
  • @AaronMcDaid: exceptions to strict aliasing do care about direction; it's access an object through a pointer to derived to pointer to base, and it's allowed to access it through a pointer to the dynamic type of the object (C++11 §3.10 ¶10), but here you are trying to cast from base to derived, without derived being the dynamic type of the object. – Matteo Italia Nov 02 '13 at 15:02
  • Errata corrige: *casting* to base class regardless of dynamic type seems to be admitted (§5.2.9 ¶11), while using this pointer to access the object instead violates the strict aliasing rules. – Matteo Italia Nov 02 '13 at 15:09

2 Answers2

4
  1. Casting from a data pointer to void * is always guaranteed to work, and the pointer is guaranteed to survive the roundtrip Base * -> void * -> Base * (C++11 §5.2.9 ¶13);
  2. vp is a valid pointer, so there shouldn't be any problem.
  3. a. albeit printing pointers is implementation-defined1, the printed values should be the same: in facts operator<< by default is overloaded only for const void *, so when you write cout<<&b you are converting to const void * anyway, i.e. what operator<< sees is in both cases &b casted to const void *.

    b. yes, if we take the only sensible definition of "has the same value" - i.e. it compares equal with the == operator; in facts, if you compare vp and &b with ==, the result is true, both if you convert vp to Base * (due to what we said in 1), and if you convert &b to void *.

    Both these conclusions follow from §4.10 ¶2, where it's specified that any pointer can be converted to void * (modulo the usual cv-qualified stuff), and the result «points to the start of the storage location where the object [...] resides»1

  4. This is tricky; that C-style cast is equivalent to a static_cast, which will happily allow casting a «"pointer to cv1 B[...] to [...] "pointer to *cv2 D", where D is a class derived from B» (§5.2.9, ¶11; there are some additional constraints, but they are satisfied here); but:

    If the prvalue of type “pointer to cv1 B” points to a B that is actually a subobject of an object of type D, the resulting pointer points to the enclosing object of type D. Otherwise, the result of the cast is undefined.

    (emphasis added)

    So, here your cast is allowed, but the result is undefined...

  5. ... which leads us to printing its value; since the result of the cast is undefined, you may get anything. Since pointers are probably allowed to have trap representations (at least in C99, I could find only sparse references to "traps" in the C++11 standard, but I think that probably this behavior should already be inherited from C89) you may even get a crash just by reading this pointer to print it via operator<<.

If follows that also 6, 8 and 9 aren't meaningful, because you are using an undefined result.

Also, even if the cast was valid, strict aliasing (§3.10, ¶10) would block you to do anything meaningful with the pointed objects, since aliasing a Base object via a Derived pointer is only allowed when the dynamic type of the Base object is actually Derived; anything that deviates from the exceptions specified at §3.10 ¶10 results in undefined behavior.


Notes:

  1. operator>> delegates to num_put which conceptually delegates to printf with %p, whose description boils down to "implementation defined".

  2. This rules out my fear that an evil implementation could in theory return different but equivalent values when casting to void *.

Matteo Italia
  • 123,740
  • 17
  • 206
  • 299
  • You mentioned unions in another comment. If we had `union u { Base b; Derived d; }`, and if the sub types line up correctly, then this is probably the only case where an object of dynamic type `Base` can be accessed via a `Derived&`? Otherwise, all bets are off. – Aaron McDaid Nov 02 '13 at 15:59
  • @AaronMcDaid: C++11 `union`s allow to access objects of layout-compatible classes via different types, but the "derived class" bit seems to ruin it all. Checking §9 ¶7, the definition of "standard layout class" seems to be done to allow this stuff, but the "layout-compatible" definition (§9.2 ¶18) doesn't consider base classes at all, I think that may be a bug in the standard. – Matteo Italia Nov 02 '13 at 16:04
1

(Attempting to answer my own question, from the point of view of strict aliasing. A good optimizer is entitled to do some unexpected things, which effectively give us UB. But I'm not an expert, by any means!)

In this function,

 void foo(Base &b_ref) {
     Base b;
     ....
 }

it is obvious that b and b_ref can't refer to each other. This particular example doesn't involve analysis of compatible types, it's just the simple observation that a new-constructed local variable is guaranteed to be the only reference to itself. This allows the optimizer to do some tricks. It can store b in a register and it can then execute code, such as b_ref.modify(), that modifies b_ref, safe in the knowledge that b is not affected. (Perhaps only really smart optimizers will notice this, but it is allowed.)

Next, consider this,

void foo(Base &b_ref, Derived&d_ref);

Within the implementation of this function, the optimize cannot assume that b_ref and d_ref refer to different objects. Therefore, if the code calls d_ref.modify(), then the next time the code is accessing b_ref it must look again at the memory that stores the b_ref object. If there is a copy of the b_ref data in the CPU registers, then it is possibly out-of-date data.

But if the types having nothing to do with each other, then such optimizations would be allowed. e.g.

struct Base1 { int i; };  struct Base2 { int i; };
void foo(Base1 & b1_ref, Base2 &b2_ref);

These can be assumed to point to different objects and therefore the compiler is allowed to make certain assumptions. b2_ref.i=5; cannot change b1_ref.i, therefore the compiler can make some assumptions. (Actually, there might be other threads, or even POSIX signals, making changes behind the scenes, and I must admit I'm not to clear on threads!)

So, there are assumptions the compiler is allowed to make for optimization. Consider this:

Base b; // a global variable
void foo() {
    Derived &d_ref = some_function();
    int x1 = b.i;
    d_ref.i = 5;
    int x2 = b.i;
}

With this, the optimizer knows the dynamic type of b, it's a Base. The two consecutive calls to b.i should give the same value (excepting other threads or whatever), and therefore the compiler is allowed to optimize the latter to int x2 = x1. If some_function returned a Base&, i.e. Base &d_ref = some_function(); then the compiler would not be allowed to make such an optimization.

So, given an object where the compiler knows its dynamic type is Base, and a reference to a derived type Derived&, the compiler is entitled to assume they refer to different objects. The compiler is allowed to rewrite the code a little, making the assumption that the two objects don't refer to each other. This can, at the very least, lead to unpredictable behaviour. And anything you do that violates assumptions the optimizer is allowed to make is undefined behaviour.

Aaron McDaid
  • 26,501
  • 9
  • 66
  • 88