C++ Core Guidelines: Accessing Objects in a Hierarchy

There are nine rules to access objects in class hierarchies. Let's have a closer look.

 

doorSmall

Here are the nine rules.

Accessing objects in a hierarchy rule summary:

Believe me. Slicing is an issue in a lot of C++ codebases.

C.145: Access polymorphic objects through pointers and references

If you access a virtual function, you don't know which class provides the functionality; therefore, you should use a pointer or a reference. This means in the concrete example, that both d are sliced.

struct B{ 
  int a; 
  virtual int f(); 
};

struct D : B{ 
  int b; 
  int f() override; 
};

void use(B b)
{
    D d;
    B b2 = d;   // slice
    B b3 = b;
}

void use2()
{
    D d;
    use(d);   // slice
}

 

The first and the second slice cause that only the B part of D is copied.

Do you want to know more about slicing? C.67: A base class should suppress copying, and provide a virtual clone instead if “copying” is desired talks about this issue.

The three next rules are about dynamic_cast. Before I write about the dynamic_cast let me emphasise, casts including dynamic_cast are used way too often. The job of the dynamic_cast is it to "Safely converts pointers and references to classes up, down, and sideways along the inheritance hierarchy." (http://en.cppreference.com/w/cpp/language/dynamic_cast)

 

Rainer D 6 P2 540x540Modernes C++ Mentoring

Stay informed about my mentoring programs.

 

 

Subscribe via E-Mail.

C.146: Use dynamic_cast where class hierarchy navigation is unavoidable

Here is the use-case from the C++ Core Guidelines. You want to navigate in the class hierarchy.

struct B {   // an interface
    virtual void f();
    virtual void g();
};

struct D : B {   // a wider interface
    void f() override;
    virtual void h();
};

void user(B* pb)
{
    if (D* pd = dynamic_cast<D*>(pb)) {      // (1)
        // ... use D's interface ...
    }
    else {
        // ... make do with B's interface ...
    }
}

 

To detect the right type for pb (1) during run-time a dynamic_cast is necessary. If the cast fails, you will get a null pointer.

Because of performance reasons, you want to make the cast at compile-time; therefore, a static_cast is your friend. Now, you can violate the type of safety of the program.

void user2(B* pb)   // bad
{
    D* pd = static_cast<D*>(pb);    // I know that pb really points to a D; trust me
    // ... use D's interface ...
}

void user3(B* pb)    // unsafe
{
    if (some_condition) {
        D* pd = static_cast<D*>(pb);   // I know that pb really points to a D; trust me
        // ... use D's interface ...
    }
    else {
        // ... make do with B's interface ...
    }
}

void f()
{
    B b;
    user(&b);   // OK
    user2(&b);  // bad error   (1)
    user3(&b);  // OK *if* the programmer got the some_condition check right (2)
}

 

Casting a pointer to B to a pointer to D (1) is an error. This maybe holds for the last line (2).

 

C.147: Use dynamic_cast to a reference type when failure to find the required class is considered an error

If you make a dynamic_cast to a pointer, you will get in the case of a failure a null pointer; but if you make a dynamic_cast to a reference, you will get a failure. To be more specific, you will get a std::bad_cast exception.

// badCast.cpp

struct A{
    virtual void f() {}
};
struct B : A {};

int main(){
    
    A a;
    B b;

    B* b1 = dynamic_cast<B*>(&a);  // nullptr, because 'a' is not a 'B'
    B& b2 = dynamic_cast<B&>(a);   // std::bad_cast, because 'a' is not a 'B' 
   
}

 

The g++-6 compiler complains about both bad dynamic_cast's and the run-time throws the expected exception in case of the reference.

badCast

C.148: Use dynamic_cast to a pointer type when failure to find the required class is considered a valid alternative

Sometimes it may be a valid option to choose an alternative code path, if the dynamic_cast to a pointer type fails and, therefore, returns a null pointer.

C.149: Use unique_ptr or shared_ptr to avoid forgetting to delete objects created using new

Using std::unique_ptr or std::shared_ptr is a very important but also quite obvious rule to avoid resource leaks. In the case you build an application and not infrastructure such as a library, let me rephrase it: Never ever use new (and delete).

Applying this rule means that you should use std::make_unique and std::make_shared for creating smart pointers.

C.150: Use make_unique() to construct objects owned by unique_ptrs, C.151: Use make_shared() to construct objects owned by shared_ptrs

Both rules are quite similar; therefore I can handle them together. std::make_unique and std::make_shared give you the guarantee that the operation is never interleaved. That means in the following example: no memory leak can happen.

f(std::make_unique<Foo>(), bar());

 

This guarantee will not hold for the next call.

f(std::unique_ptr<Foo>(new Foo()), bar());

 

It may happen that Foo is at first allocated on the heap and then bar is called. If bar throws an exception, Foo will not be destroyed and we will get a memory leak.

The same observation holds for std::make_share for creating a std::shared_ptrstd::make_shared has an additional performance benefit. Creating a std::shared_ptr requires two memory allocations; one for the resource and one for the counter. By using std::make_shared, both expensive allocations will happen in one step. The performance difference is dramatic. Have a look at my post: Memory and Performance Overhead of Smart Pointers.

C.152: Never assign a pointer to an array of derived class objects to a pointer to its base

This may not happen so often but if it happens the consequences may be very bad. The result may be invalid object access or memory corruption. The former issue is shown in the example.

struct B { int x; };
struct D : B { int y; };

D a[] = {{1, 2}, {3, 4}, {5, 6}};
B* p = a;     // bad: a decays to &a[0] which is converted to a B*
p[1].x = 7;   // overwrite D[0].y

 

The last assignment should update the x attribute of an instance of B but it overwrites the y attribute of a D. The reason is that B* was assigned a pointer to an array of derived objects D.

Decay is the name of an implicit conversion, that applies lvalue-to-rvalue, array-to-pointer, and function-to-pointer conversions removing const and volatile qualifiers. That means in the concrete example you can call a function accepting a D* with an array of D's. The argument d of the following function will have a pointer to the first element of D. Valuable information such as the length of the array of D's is lost.

void use(D* d);
D d[] = {{1, 2}, {3, 4}, {5, 6}};

use(d);

C.153: Prefer virtual function to casting

You can use dynamic_cast to simulate virtual behaviour also often called late binding. But that is ugly and error-prone. You may get a null pointer or a std::bad_cast exception (see C.147). If you want to know more about virtual functions read the rule C67 in the post C++ Core Guidelines: Rules for Copy and Move.

What's next?

In C++ we can overload functions, function templates, and even operators. In particular, operator overloading is often very controversial discussed. For example, MISRA C++, a guideline for a safe subset of C++, forbids the overloading of operators. To be honest. I don't see why? The C++ Core Guidelines has ten rules to overloading that will be the topic of my next post.

 

 

 

 

Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Matthieu Bolt, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Dominik Vošček, and Rob North.

 

Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, and Slavko Radman.

 

 

My special thanks to Embarcadero CBUIDER STUDIO FINAL ICONS 1024 Small

 

My special thanks to PVS-Studio PVC Logo

 

My special thanks to Tipi.build tipi.build logo

Seminars

I'm happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

Bookable (Online)

German

Standard Seminars (English/German)

Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

  • C++ - The Core Language
  • C++ - The Standard Library
  • C++ - Compact
  • C++11 and C++14
  • Concurrency with Modern C++
  • Design Pattern and Architectural Pattern with C++
  • Embedded Programming with Modern C++
  • Generic Programming (Templates) with C++

New

  • Clean Code with Modern C++
  • C++20

Contact Me

Modernes C++,

RainerGrimmDunkelBlauSmall

Mentoring

Stay Informed about my Mentoring

 

English Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Interactive Course: The All-in-One Guide to C++20

Subscribe to the newsletter (+ pdf bundle)

All tags

Blog archive

Source Code

Visitors

Today 554

Yesterday 8050

Week 8604

Month 198680

All 11679834

Currently are 163 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments