Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does this static C++ cast work?

Tags:

c++

casting

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?

like image 217
mcAngular2 Avatar asked May 19 '17 11:05

mcAngular2


People also ask

What is a static cast in C?

Static casts can be used to convert one type into another, but should not be used for to cast away const-ness or to cast between non-pointer and pointer types. Static casts are prefered over C-style casts when they are available because they are both more restrictive (and hence safer) and more noticeable.

What is the point of static cast?

In general you use static_cast when you want to convert numeric data types such as enums to ints or ints to floats, and you are certain of the data types involved in the conversion.

What happens if static cast fails?

If static_cast fails you will get a compile error and the program executable will never even be built. Your example has undefined behavior, not a failure or an error.

What does C-style cast do?

C-style casts can be used to convert any type into any other type, potentially with unsafe results (such as casting an integer into a pointer type).


Video Answer


3 Answers

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.

like image 80
Felix Glas Avatar answered Oct 15 '22 12:10

Felix Glas


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.

like image 45
Richard Hodges Avatar answered Oct 15 '22 11:10

Richard Hodges


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!
}
like image 37
Ajay Avatar answered Oct 15 '22 12:10

Ajay