9

We all know that when using simple single inheritance, the address of a derived class is the same as the address of the base class. Multiple inheritance makes that untrue.

Does virtual inheritance also make that untrue? In other words, is the following code correct:

struct A {};

struct B : virtual A
{
    int i;
};

int main()
{
    A* a = new B; // implicit upcast
    B* b = reinterpret_cast<B*>(a); // fishy?
    b->i = 0;

    return 0;
}
user1610015
  • 6,561
  • 2
  • 15
  • 18
  • "If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined: a type that is a (possibly cv-qualified) base class type of the dynamic type of the object" https://timsong-cpp.github.io/cppwp/n4659/basic.lval#8.7 It doesn't mention if the base class is `virtual` or not. But I'm not sure. – L. F. Feb 13 '20 at 08:12
  • 3
    *"We all know that when using simple single inheritance, the address of a derived class is the same as the address of the base class"* is quite a strong statement. Are you *sure* the standard guarantees this? – hyde Feb 13 '20 at 08:20
  • @hyde Did I say anything about the standard? C++ would be a pretty impractical language if we only relied on what the standard guarantees... – user1610015 Feb 13 '20 at 08:26
  • 7
    It is an interesting quirk of human languages that no sentence beginning with "we all know that" is true. – molbdnilo Feb 13 '20 at 08:27
  • 6
    _"C++ would be a pretty impractical language if we only relied on what the standard guarantees"_ I'm not developing C++ for decades like others might have, but I never had to violate the standard or rely on unspecified / undefined behavior in my applications. – Timo Feb 13 '20 at 08:31
  • 1
    @user1610015 Then what do you mean when asking whether the code is "correct"? – Mark Feb 13 '20 at 08:34
  • 3
    @user1610015 Your first sentence is not always true with some major compilers and it is not so by C++ standard so there has to be some other specification (about particular compiler or ABI) that guarantees it for your particular case. – Öö Tiib Feb 13 '20 at 08:39
  • 1
    @hyde Casting to `void*` and back doesn't require `reinterpret_cast`. `static_cast` does that as well, so this isn't a good `reinterpret_cast` use case either. – walnut Feb 13 '20 at 08:48
  • @walnut Ah. Well, I'd still use reinterpret_cast, I think that captures the intention better. Or is there some reason to use static_cast other than personal preference? – hyde Feb 13 '20 at 11:49
  • 1
    @hyde `reinterpret_cast` between object pointer types is basically defined as `static_cast` to `void*` followed by `static_cast` to destination, so there is no difference, but I think `static_cast` avoids potential mistakes better, e.g. if `x` is of integer type `reinterpret_cast(x)` may (implementation-defined) compile without warning although you meant `reinterpret_cast(&x)`. Similarly if `x` is a function and it should have been `reinterpret_cast(x())`. – walnut Feb 13 '20 at 12:06
  • 1
    Actually, the cast *to* `void*` is implicit anyway, so it shouldn't be needed at all. The cast back to the pointer type has similar issues though, e.g. `reinterpret_cast(ptr)` as a mistake for `reinterpret_cast(ptr)` if `T` happens to be an integral type large enough to hold all pointer values. Also if `ptr` isn't actually a `void*`, the `reinterpret_cast` will compile when the `static_cast` would fail, etc. – walnut Feb 13 '20 at 12:06
  • @hyde "_Are you sure the standard guarantees this?_" Where does the std "guarantees" that you can use atomics or thread w/o UB? – curiousguy Feb 20 '20 at 00:17
  • @curiousguy Standard defines how those things work, IOW they work in the way defined in the standard, if the compiler is standard compliant.Also compiler implementation may define details about how things work, even if standard leaves them undefined. But there is no "if not specified in the standard or by the compiler implementation, do the reasonable thing" guarantee. Consider signed integer overflow. – hyde Feb 20 '20 at 04:41
  • @hyde Then please provide a program that can't have UB, and prove it. You can't and it does not. Are you also saying that you never used a literal string or `typeid`? – curiousguy Feb 20 '20 at 06:28
  • @curiousguy TBH, I'm not sure what you are going on about. From the context, it *sounds* like you are advocating simply wrong use of `reinterpret_cast`, saying that it is ok because it works in some situations. When programming with C and C++, that's a dangerous attitude, IMNSHO. – hyde Feb 20 '20 at 10:45
  • @hyde 1) The Q here contains a clearly incorrect premise; not only the assumption over SI (single inheritance) is not guaranteed in any revision of the std, it isn't guaranteed by any compiler I know of nor any ABI. The guarantee that &derived == &base for all cases SI simply was never written not meant. The assumption is not just not made in the std, it's wrong in practice in at least some cases (which aren't even bizarre corner cases). 2) Why even make the assumption? Use `static_cast` and be safe: it will work as well w/ equal or diff addresses. I see no reason to be on the unsafe side. – curiousguy Feb 20 '20 at 23:22
  • 3a) I felt no need to elaborate on the practical wrongness of that assumption because multiple ppl made the case clearly. That says nothing about other assumptions being made that are not written in the std, nor about assumption being made in old code, that were not guaranteed at the time but which are not clearly spelled out. 3b) Would you make the case that assuming 2compl representation of `int` (as seen in bit fiddling: bit operators `^ & | ~`) was not "portable" for C++ written for an old C++ std? – curiousguy Feb 20 '20 at 23:27
  • 4) There are plenty of things that are not (or were not) written down in a std that we know are true in practice: a) `int` is 2compl on all compilers for all std C++ versions. b) Dynamic dispatch is implemented with a vtable accessed via a vptr (not that knowing that allows you to portably access the vtable, of course, as the details vary). c) `volatile int` has the same semantic as relaxed non RMW operations on `std::atomic`. d) Any reasonable impl of C/C++ MT on multi core/multi CPU machines with caches must rely on consistent caches. – curiousguy Feb 20 '20 at 23:32
  • 5) **Assuming that things guaranteed by the std are safer assumptions than practically known stuff is simply wrong.** The probability of compilers getting `mo_consume` wrong and mis-compiling cases of an effectively constant dependent value is infinitely higher than the probability of `volatile int` crashing the program because of "UB" data race. 6) *Stating you write code correct WRT to the std is patently wrong for MT code because the std does not define what a correct MT program is, and could not because there is no attempt to say what is sequential and what is not.* – curiousguy Feb 20 '20 at 23:36
  • @hyde "_But there is no "if not specified in the standard or by the compiler implementation, do the reasonable thing" guarantee. Consider signed integer overflow._" Simplifying integer computations using mathematical rules (rules not applicable to modulo `2**n` arithmetic) is what many ppl would call very reasonable. And of course it's incorrect in any language or dialect that defines all integer computations are done `mod 2**n`. So compilers do routinely simplify integer computations assuming no overflow. (You can avoid that with judicious use of `volatile`.) – curiousguy Feb 20 '20 at 23:39
  • @hyde "_But there is no "if not specified in the standard or by the compiler implementation, do the reasonable thing" guarantee_" yes there is, hopefully. How do you know that invalid ptr values are not going to randomly pop up in an `atomic`? There is no guarantee but everyone assumes that. How do you know `cout << "hello";` is valid? Formally it's UB. But it works! – curiousguy Feb 20 '20 at 23:55

4 Answers4

7

We all know that when using simple single inheritance, the address of a derived class is the same as the address of the base class.

I think the claim is not true. In the below code, we have a simple (not virtual) single (non multiple) inheritance, but the addresses are different.

class A
{
public:
   int getX()
   {
      return 0;
   }
};

class B : public A
{
public:
   virtual int getY()
   {
      return 0;
   }
};

int main()
{
   B b;
   B* pB = &b;

   A* pA = static_cast<A*>(pB);

   std::cout << "The address of pA is: " << pA << std::endl;
   std::cout << "The address of pB is: " << pB << std::endl;

   return 0;
}

and the output for VS2015 is:

The address of pA is: 006FF8F0
The address of pB is: 006FF8EC

Does virtual inheritance also make that untrue?

If you change the inheritance in the above code into virtual, the result will be the same. so, even in the case of virtual inheritance, the addresses of base and derived objects can be different.

TonySalimi
  • 8,257
  • 4
  • 33
  • 62
  • Actually g++ also confirms your case with bit modified code: http://coliru.stacked-crooked.com/a/ccea741b7126ee8a – Öö Tiib Feb 13 '20 at 08:48
  • @ÖöTiib `void main()` is acceptable even in modern MSVS compilers. BTW, thanks for the comment. I have updated the code. – TonySalimi Feb 13 '20 at 08:54
  • 1
    No, `void main()` is not acceptable. It has to be `int main()` according to the standard. And please remove that `dynamic_cast` from the code, it is not needed there, and it causes confusion. – geza Feb 13 '20 at 09:06
3

The result of reinterpret_cast<B*>(a); is only guaranteed to point to the enclosing B object of a if the a subobject and the enclosing B object are pointer-interconvertible, see [expr.static.cast]/3 of the C++17 standard.

The derived class object is pointer-interconvertible with the base class object only if the derived object is standard-layout, does not have direct non-static data members and the base class object is its first base class subobject. [basic.compound]/4.3

Having a virtual base class disqualifies a class from being standard-layout. [class]/7.2.

Therefore, because B has a virtual base class and a non-static data member, b will not point to the enclosing B object, but instead b's pointer value will remain unchanged from a's.

Accessing the i member as if it was pointing to the B object then has undefined behavior.

Any other guarantees would come from your specific ABI or other specification.

walnut
  • 21,629
  • 4
  • 23
  • 59
2

Multiple inheritance makes that untrue.

That is not entirely correct. Consider this example:

struct A {};
struct B : A {};
struct C : A {};
struct D : B, C {};

When creating an instance of D, B and C are instantiated each with their respective instance of A. However, there would be no problem if the instance of D had the same address of its instance of B and its respective instance of A. Although not required, this is exactly what happens when compiling with clang 11 and gcc 10:

D: 0x7fffe08b4758 // address of instance of D
B: 0x7fffe08b4758 and A: 0x7fffe08b4758 // same address for B and A
C: 0x7fffe08b4760 and A: 0x7fffe08b4760 // other address for C and A

Does virtual inheritance also make that untrue

Let's consider a modified version of the above example:

struct A {};
struct B : virtual A {};
struct C : virtual A {};
struct D : B, C {};

Using the virtual function specifier is typically used to avoid ambiguous function calls. Therefore, when using virtual inheritance, both B and C instances must create a common A instance. When instantiating D, we get the following addresses:

D: 0x7ffc164eefd0
B: 0x7ffc164eefd0 and A: 0x7ffc164eefd0 // again, address of A and B = address of D
C: 0x7ffc164eefd8 and A: 0x7ffc164eefd0 // A has the same address as before (common instance)

Is the following code correct

There is no reason here to use reinterpret_cast, even more, it results in undefined behavior. Use static_cast instead:

A* pA = static_cast<A*>(pB);

Both casts behave differently in this example. The reinterpret_cast will reinterpret pB as a pointer to A, but the pointer pA may point to a different address, as in the above example (C vs A). The pointer will be upcasted correctly if you use static_cast.

mfnx
  • 2,894
  • 1
  • 12
  • 28
-2

The reason a and b are different in your case is because, since A is not having any virtual method, A is not maintaining a vtable. On the other hand, B does maintain a vtable.

When you upcast to A, the compiler is smart enough to skip the vtable meant for B. And hence the difference in addresses. You should not reinterpret_cast back to B, it wouldn't work.

To verify my claim, try adding a virtual method, say virtual void foo() {} in class A. Now A will also maintain a vtable. Thus downcast(reinterpret_cast) to B will give you back the original b.

theWiseBro
  • 1,439
  • 12
  • 11
  • vtables are not relevant here. – mfnx Feb 13 '20 at 09:18
  • @mfnx OP's question `Subclass address equal to virtual base class address? ` has all to do with virtual inheritance. And virtual inheritance has all to do with vtables. – theWiseBro Feb 13 '20 at 09:24
  • @walnut OP's example performs a virtual inheritance. The reason that casting will give wrong result is because of absence of vtable in class A. It's true that this should not be done and is illegal, but well, let's be practical. – theWiseBro Feb 13 '20 at 09:26
  • @geza Yes, sorry for my stupid comment. Still, I am doubtful that adding virtual methods to `A` is going to guarantee that the addresses match and the cast works, either in theory or in practice. I cannot remove my downvote without a post edit, because it has been locked-in. – walnut Feb 13 '20 at 09:37
  • "The reason a and b are different in ...": they may share the same address. Having a vtable or not does not mean you'll have the same addresses are not. Note the differences I got with clang and gcc compared to @gupta with VS2015. – mfnx Feb 13 '20 at 09:44
  • @mfnx hmm. Interesting results. True that it may be dependent on the compiler :/ – theWiseBro Feb 13 '20 at 10:15
  • Anyways, the standard doesn't state the relation between the addresses of base and derived classes in function of whether or not they have a vtable. Therefore, imo, vtables are not relevant here. – mfnx Feb 13 '20 at 10:23