10

Imagine this code:

class Base {
 public:
  virtual void foo(){}
};

class Derived: public Base {
  public:
    int i;
    void foo() override {}
    void do_derived() {
      std::cout << i;
    }
};

int main(){
  Base *ptr = new Base;
  Derived * static_ptr = static_cast<Derived*>(ptr);
  static_ptr->i = 10;  // Why does this work?
  static_ptr->foo(); // Why does this work?
  return 0;
}

Why do I get the result 10 on the console? I wonder because I thought the ptr is a pointer to a base object. Therefore the object doesn't contain a int i or the method do_derived(). Is a new derived-Object automatically generated?

When I declare a virtual do_derived() method in the Base class too, then this one is chosen, but why?

Dinesh Maurya
  • 822
  • 8
  • 24
mcAngular2
  • 299
  • 1
  • 14
  • 33
    It doesn't work. It is undefined behavior. – NathanOliver May 19 '17 at 11:44
  • so is it just random that 10 is written to the console? – mcAngular2 May 19 '17 at 11:46
  • 1
    @mcAngular2 Yes. You're just lucky. – πάντα ῥεῖ May 19 '17 at 11:46
  • 17
    Undefined Behaviour may sometimes _appear_ to work, but this is just a ploy to lull you into a false sense of security. It's biding its time, and will start consistently and reproducibly failing at the worst possible moment. – Useless May 19 '17 at 11:47
  • 1
    Not really random but also not the only thing that could be printed. – NathanOliver May 19 '17 at 11:47
  • See http://stackoverflow.com/questions/6322949/downcasting-using-the-static-cast-in-c – jsn May 19 '17 at 11:48
  • 17
    @πάνταῥεῖ I think he's in fact **unlucky** :) This kind of silent error (or UB) can be hell to debug. – Jonas May 19 '17 at 11:48
  • UB. So many things that work until they suddenly don't. – didiz May 19 '17 at 11:49
  • 1
    for example [here](http://ideone.com/3Fbdo4) nothing gets printed. – 463035818_is_not_an_ai May 19 '17 at 11:50
  • 10
    @Useless surely you mean "*inconsistently and irreproducibly* failing at the worst possible moment" – Caleth May 19 '17 at 11:57
  • @tobi303 That's mostly because the code doesn't print anything :) – Felix Glas May 19 '17 at 12:02
  • 3
    @mcAngular2 It's very important to distinguish between code that *compiles* and code that is *correct*. Code that compiles is not necessarily correct. The c++ standard specifies many restrictions that compilers aren't required to detect, such as your use of `static_cast`. See [this link](http://en.cppreference.com/w/cpp/language/ub). – François Andrieux May 19 '17 at 12:06
  • @Snps I didnt expect anything to be printed, but it was just meant as demonstrating that behaviour observed by OP is nothing to rely on (note that the code is exactly OPs code, I only added an include to make it compile) – 463035818_is_not_an_ai May 19 '17 at 12:24
  • @tobi303 Yes, but OPs code doesn't print anything either. He got his output from somewhere else. The point is that the code doesn't even try to print anything, so your example doesn't prove anything (although I appreciate the intention). – Felix Glas May 19 '17 at 12:49
  • 2
    I think the overridden method OP MEANT it to be `void foo() override {do_derived();}`. – franji1 May 19 '17 at 12:54
  • @Snps "He got his output from somewhere else." how do you know that? OP writes "Why do I get the result 10 on the console?" which I understand as that code prints 10. Not what I would have expceted, but understandable. Btw it was not my intention to prove anything, but to the contrary point out that with UB nothing can be proved – 463035818_is_not_an_ai May 19 '17 at 12:54
  • @tobi303 Ok, it is just that the probability that the UB assignment and call to an unexisting empty function can result in outputting 10 to standard output is infinitesimal. It is hugely more probable that OP made a typo in the question, and I basically took it for granted. – Felix Glas May 19 '17 at 13:02
  • @Snps in that case you should complain to the OP not to me :P. Nevermind, its pointless to discuss what output one gets from UB. Printing 10 is just as unlikely as any other outcome. – 463035818_is_not_an_ai May 19 '17 at 13:05
  • @Snps - Or, the method table is totally munged and it called `do_dervied`. – Donnie May 19 '17 at 15:14
  • 'Twould be nice if compilers had an option to issue a warning on using a static_cast to cast down a hierarchy and suggest a dynamic_cast instead. (I know not everyone likes dynamic_cast because it depends on RTTI, which is why I said "an option.") – Adrian McCarthy May 19 '17 at 16:54
  • 2
    The contract with static_cast is to assume that the programmer knows what they're doing and assume that the result is valid. In cases like this where it might not be valid you should consider dynamic_cast, which would've returned nullptr. – Glen Knowles May 19 '17 at 16:55
  • @AdrianMcCarthy That warning makes sense as an option. What it made me think of, however, is how I'd like a debug-mode option to replace `static_cast`s down hierarchies with `dynamic_cast`s and fail loudly if invalid. It's tiring having to write `assert(dynamic_cast...); Type& blah = static_cast...`. That said... I guess it'd be fairly trivial to write my own helper template function for this, so perhaps this contributes to why compiler writers haven't taken the time to do this. – underscore_d May 19 '17 at 18:16
  • 1
    Back to the OP, of course, it works because you promised the compiler that you were making it perform a valid cast, and it believed you. Sure, it's imaginable to statically analyse the providence of the source pointer in this case and warn if an invalid cast is coded - but that quickly becomes impractical as we scale up to more complex programs, and no diagnostic is required since `static_cast` means 'trust me: I know what I'm doing'... so compiler authors typically just take the coder at her word. Oh, and by the way: You don't need `new` or `delete` for things like this. Just cast references. – underscore_d May 19 '17 at 18:25

4 Answers4

25
int* i = new int{1};
delete i;
std::cout << *i << std::endl;

This will also "work", if the definition of working is that the code will compile and execute.

However, it is clearly undefined behavior and there are no guarantees as to what might happen.


In your case, the code compiles as static_cast won't perform any checks, it just converts the pointer. It is still undefined behavior to access memory that hasn't been allocated and initialized though.

marsh
  • 2,592
  • 5
  • 29
  • 53
Felix Glas
  • 15,065
  • 7
  • 53
  • 82
8

As mentioned in the comments, "happens to do what you expected" is not the same as "works".

Let's make a few modifications:

#include <iostream>
#include <string>

class Base{
public:
    virtual  void foo(){
        std::cout << "Base::foo" << std::endl;
    }
};

class Derived: public Base{
public:
    int a_chunk_of_other_stuff[1000000] = { 0 };
    std::string s = "a very long string so that we can be sure we have defeated SSO and allocated some memory";
    void foo() override {
        std::cout << "Derived::foo" << std::endl;
    }
    void do_derived() {
        std::cout << s << std::endl;
    }
};

int main(){
    Base *ptr = new Base;
    Derived * static_ptr = static_cast<Derived*>(ptr);
    static_ptr -> foo(); // does it though?
    static_ptr -> do_derived(); // doesn't work?
    static_ptr->a_chunk_of_other_stuff[500000] = 10;  // BOOM!
    return 0;
}

Sample Output:

Base::foo

Process finished with exit code 11

In this case, none of the operations did what we expected. The assignment into the array caused a segfault.

Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
3

The statement:

Base *ptr = new Base;

Doesn't always allocate sizeof(Base) - it would probably allocate more memory. Even if it does allocate exact sizeof(Base) bytes, it doesn't necessarily mean any byte access after this range (i.e. sizeof(Base)+n, n>1) would be invalid.

Hence let's assume the size of class Base is 4 bytes (due to virtual function table in most compiler's implementation, on a 32-bit platform). However, the new operator, the heap-management API, the memory management of OS, and/or the hardware does allocate 16 bytes for this allocation (assumption). This makes additional 12 bytes valid! It makes the following statement valid:

static_ptr->i = 10;

Since now it tries to write 4 bytes (sizeof(int), normally) after the first 4 bytes (size of polymorphic class Base).

The function call:

static_ptr->foo();

would simply make a call to Derived::foo since the pointer is of type Derived, and nothing is wrong in it. The compiler must call Derived::foo. The method Derived::foo doesn't even try to access any data member of derived class (and even base class).

Had you called:

static_ptr->do_derived();

which is accessing i member of derived. It would still be valid, since:

  • The function call is always valid, till method tries to access data-member (i.e. accesses something out of this pointer).
  • Data-member access became valid due to memory allocation (UD behaviour)

Note that following is perfectly valid:

class Abc
{
public:
void foo() { cout << "Safe"; }
};

int main()
{
   Abc* p = NULL;
   p->foo(); // Safe
}

The call it valid, since it translates to:

    foo(NULL);

where foo is:

void foo(Abc* p)
{
    // doesn't read anything out of pointer!
}
Ajay
  • 18,086
  • 12
  • 59
  • 105
  • I think that your `p->foo()` example is not safe, even though it does not read anything out of the pointer. As mentioned in this answer http://stackoverflow.com/a/2474021/2689797, `p->foo()` is equivalent to `(*p).foo()`, which dereferences the null pointer. – Eldritch Cheese May 19 '17 at 22:07
  • Compiler would translate both of them as `foo(p)` where `p` will be the `this` pointer in `foo`. – Ajay May 20 '17 at 05:16
  • @EldritchCheese, however, I do agree what it is in fact UD. However, I still say that if `p->f()` is valid, on some compiler then `(*p).f()` would be valid. – Ajay May 20 '17 at 06:51
0

why does this static cast work?

Because static cast is compile time checker. There is a relationship between Base and Derived. Since it has relationship, static cast believe's that relationship and believe's the programmer too. So As a programmer, you should make sure that Base object should not be static casted to derived class object.

VINOTH ENERGETIC
  • 1,775
  • 4
  • 23
  • 38
  • `you should make sure that Base object should not be static casted to derived class object.` Objection! If the source of such a cast will always genuinely be that derived type (or a subclass of it), then such a `static_cast` is perfectly valid. I don't believe in always `dynamic_cast`ing in cases where the cast is always going to be valid; that's paying for what you don't use. – underscore_d May 19 '17 at 18:21