콘텐츠로 이동

C.hier-access

C.hier-access: Accessing objects in a hierarchy

C.145: Access polymorphic objects through pointers and references

Reason

If you have a class with a virtual function, you don't (in general) know which class provided the function to be used.

Example

struct B { int a; virtual int f(); virtual ~B() = default };
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
}

Both ds are sliced.

Exception

You can safely access a named polymorphic object in the scope of its definition, just don't slice it.

void use3()
{
    D d;
    d.f();   // OK
}

See also

A polymorphic class should suppress copying

Enforcement

Flag all slicing.

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

Reason

dynamic_cast is checked at run time.

Example

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

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

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

Use of the other casts can violate type safety and cause the program to access a variable that is actually of type X to be accessed as if it were of an unrelated type Z:

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
    user3(&b);  // OK *if* the programmer got the some_condition check right
}

Note

Like other casts, dynamic_cast is overused. Prefer virtual functions to casting. Prefer static polymorphism to hierarchy navigation where it is possible (no run-time resolution necessary) and reasonably convenient.

Note

Some people use dynamic_cast where a typeid would have been more appropriate; dynamic_cast is a general "is kind of" operation for discovering the best interface to an object, whereas typeid is a "give me the exact type of this object" operation to discover the actual type of an object. The latter is an inherently simpler operation that ought to be faster. The latter (typeid) is easily hand-crafted if necessary (e.g., if working on a system where RTTI is -- for some reason -- prohibited), the former (dynamic_cast) is far harder to implement correctly in general.

Consider:

struct B {
    const char* name {"B"};
    // if pb1->id() == pb2->id() *pb1 is the same type as *pb2
    virtual const char* id() const { return name; }
    // ...
};

struct D : B {
    const char* name {"D"};
    const char* id() const override { return name; }
    // ...
};

void use()
{
    B* pb1 = new B;
    B* pb2 = new D;

    cout << pb1->id(); // "B"
    cout << pb2->id(); // "D"


    if (pb2->id() == "D") {         // looks innocent
        D* pd = static_cast<D*>(pb2);
        // ...
    }
    // ...
}

The result of pb2->id() == "D" is actually implementation defined. We added it to warn of the dangers of home-brew RTTI. This code might work as expected for years, just to fail on a new machine, new compiler, or a new linker that does not unify character literals.

If you implement your own RTTI, be careful.

Exception

If your implementation provided a really slow dynamic_cast, you might have to use a workaround. However, all workarounds that cannot be statically resolved involve explicit casting (typically static_cast) and are error-prone. You will basically be crafting your own special-purpose dynamic_cast. So, first make sure that your dynamic_cast really is as slow as you think it is (there are a fair number of unsupported rumors about) and that your use of dynamic_cast is really performance critical.

We are of the opinion that current implementations of dynamic_cast are unnecessarily slow. For example, under suitable conditions, it is possible to perform a dynamic_cast in fast constant time. However, compatibility makes changes difficult even if all agree that an effort to optimize is worthwhile.

In very rare cases, if you have measured that the dynamic_cast overhead is material, you have other means to statically guarantee that a downcast will succeed (e.g., you are using CRTP carefully), and there is no virtual inheritance involved, consider tactically resorting static_cast with a prominent comment and disclaimer summarizing this paragraph and that human attention is needed under maintenance because the type system can't verify correctness. Even so, in our experience such "I know what I'm doing" situations are still a known bug source.

Exception

Consider:

template<typename B>
class Dx : B {
    // ...
};

Enforcement

  • Flag all uses of static_cast for downcasts, including C-style casts that perform a static_cast.
  • This rule is part of the type-safety profile.

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

Reason

Casting to a reference expresses that you intend to end up with a valid object, so the cast must succeed. dynamic_cast will then throw if it does not succeed.

Example

std::string f(Base& b)
{
    return dynamic_cast<Derived&>(b).to_string();
}

Enforcement

???

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

Reason

The dynamic_cast conversion allows to test whether a pointer is pointing at a polymorphic object that has a given class in its hierarchy. Since failure to find the class merely returns a null value, it can be tested during run time. This allows writing code that can choose alternative paths depending on the results.

Contrast with C.147, where failure is an error, and should not be used for conditional execution.

Example

The example below describes the add function of a Shape_owner that takes ownership of constructed Shape objects. The objects are also sorted into views, according to their geometric attributes. In this example, Shape does not inherit from Geometric_attributes. Only its subclasses do.

void add(Shape* const item)
{
  // Ownership is always taken
  owned_shapes.emplace_back(item);

  // Check the Geometric_attributes and add the shape to none/one/some/all of the views

  if (auto even = dynamic_cast<Even_sided*>(item))
  {
    view_of_evens.emplace_back(even);
  }

  if (auto trisym = dynamic_cast<Trilaterally_symmetrical*>(item))
  {
    view_of_trisyms.emplace_back(trisym);
  }
}

Notes

A failure to find the required class will cause dynamic_cast to return a null value, and de-referencing a null-valued pointer will lead to undefined behavior. Therefore the result of the dynamic_cast should always be treated as if it might contain a null value, and tested.

Enforcement

  • (Complex) Unless there is a null test on the result of a dynamic_cast of a pointer type, warn upon dereference of the pointer.

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

Reason

Avoid resource leaks.

Example

void use(int i)
{
    auto p = new int {7};           // bad: initialize local pointers with new
    auto q = make_unique<int>(9);   // ok: guarantee the release of the memory-allocated for 9
    if (0 < i) return;              // maybe return and leak
    delete p;                       // too late
}

Enforcement

  • Flag initialization of a naked pointer with the result of a new
  • Flag delete of local variable

C.150: Use make_unique() to construct objects owned by unique_ptrs

See R.23

C.151: Use make_shared() to construct objects owned by shared_ptrs

See R.22

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

Reason

Subscripting the resulting base pointer will lead to invalid object access and probably to memory corruption.

Example

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

void use(B*);

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 a[0].y

use(a);       // bad: a decays to &a[0] which is converted to a B*

Enforcement

  • Flag all combinations of array decay and base to derived conversions.
  • Pass an array as a span rather than as a pointer, and don't let the array name suffer a derived-to-base conversion before getting into the span

C.153: Prefer virtual function to casting

Reason

A virtual function call is safe, whereas casting is error-prone. A virtual function call reaches the most derived function, whereas a cast might reach an intermediate class and therefore give a wrong result (especially as a hierarchy is modified during maintenance).

Example

???

Enforcement

See C.146 and ???