1

I have read this article here: When to use virtual destructors? and I got the idea that, whenever we create an object dynamically using new or smart pointers, the base class should have a proper virtual destructor for destruction of objects at deletion.

Then I have found some code like follows(Simplified form), which has missed the virtual destructor in the Base:

class Base
{
public:
    // some static members
};

class Derived1 final : public Base
{
public:
    // other members
    // default constructor does not construct the `Base` in constructor member intilizer
    Derived1() {};
    virtual ~Derived1() = default;
};
class Derived2 final : public Base
{
public:
    // other members
    Derived2() {}; // default constructor does not construct the `Base`
    ~Derived2() = default;
};

int main()
{
    // creating Derived1 dynamically        // 1
    Derived1 *d1Object = new Derived1{};

    // creating Derived2 dynamically        // 2
    Derived2 *d2Object1 = new Derived2{};

    // creating Derived2 statically         // 3
    Derived2 d2Object2{};

    // clean up
    delete d1Object;
    delete d2Object1;
}

My qestion is:

  • Do I have Undefined Behavior in any of the cases(1, 2, 3) ? Why?
  • Isn't essential to construct the Base, in the member initializer lists of the constructors of the both derived classes(in the above particular case)?

I am using C++11.

UserUsing
  • 678
  • 1
  • 5
  • 16
  • Deleting a derived class object using a pointer to a base class that has a non-virtual destructor results in undefined behavior. As your pointers are not to a base class that has a non-virtual destructor, I think you've avoided UB here. – Harrand Sep 17 '19 at 10:57
  • In addition to the already given answers, you don't need to declare the destructor of `Derived1` as `virtual` since `Derived1` is `final` (and so will never be inherited). – Fareanor Sep 17 '19 at 11:13

2 Answers2

6

The object pointers in that code aren’t actually polymorphic: the static type of *d1Object is the same as its dynamic type, i.e. Derived1& (the same is true for the other objects).

As a consequence, destroying it directly calls the correct destructor, ~Derived1. Thus everything is fine. An issue occurs in the following code:

Base* b = new Derived1();
delete b;

This implicitly calls b->~Base();. And since ~Base isn’t virtual, ~Derived1 does not get called, and you consequently get UB.

The same is true for std::unique_ptr<Base>. However, it’s not true for std::shared_ptr<Base> because the shared_ptr<> constructor is templated, and stores the destructor of the actual object it’s constructed with. That is, the following is fine and invokes the correct destructors for both objects:

std::shared_ptr<Base> p1{new Derived1{});
std::shared_ptr<Base> p2 = std::make_shared<Derived1>();

As for your question regarding constructors, the same as for data members holds: even if they are missing from the initialiser list they are still getting default initialised in the order of their declaration and, in the case of base classes, in the order from most distant to most recent parent. In other words, you can’t not initialise a parent class, it always happens.

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • is it really UB also when all constructors involved do literally nothing? – 463035818_is_not_an_ai Sep 17 '19 at 11:12
  • @formerlyknownas_463035818 “do nothing” and “defaulted” are two very different statements. OP’s code contains a comment saying “other members”. If any of these have nontrivial destructors, then the defaulted destructor of `Derived1` is nontrivial too, and thus not invoking it is UB. — However, I believe that even for a defaulted destructor this would technically probably be UB, but I’m too lazy to look it up: I just don’t write such code. – Konrad Rudolph Sep 17 '19 at 11:14
  • I was refering to OPs code as posted (ie no members anywhere). Anyhow, it was mainly out of academical interest, I also dont write code like that. – 463035818_is_not_an_ai Sep 17 '19 at 11:16
  • @formerlyknownas_463035818 Yes, for the code as written I have to admit that I don’t know the answer though, as indicated, I believe that it’s probably still UB. – Konrad Rudolph Sep 17 '19 at 11:17
  • Even `shared_ptr` can't work magic though. If you do `Base* b = new Derived1{}; std::shared_ptr p1(b);` it has the same problem. Still, good observation about the per-instance deleter. – Max Langhof Sep 17 '19 at 11:25
  • what is the reason of `shared_ptr`'s constructor is templated, but `unique_ptr`'s not? – fas Sep 17 '19 at 11:30
  • @formerlyknownas_463035818 -- yes, it's still undefined behavior. "Undefined behavior" means only that the language definition does not tell you what your program does. It does not mean "something bad will happen". There are lots of parlor discussions about what might or might not happen when you write code with various forms of undefined behavior. They're rather pointless. – Pete Becker Sep 17 '19 at 12:10
  • @KonradRudolph I would strongly assume that it would still be defined as UB in that case. Just because the correct behavior in this case would be "do nothing" code, that does not mean that UB which (probably) also would result in "do nothing" code would turn that UB into defined behavior. Instead it would just be UB code that randomly worked as "expected". – Frodyne Sep 17 '19 at 12:10
  • @PeteBecker I disagree (slightly) on your "They're rather pointless". Discussing code with undefined behavior can be used to explore why something is undefined, highlight why it does not make sense to try and define it, and thus underscore why that code is bad (even in the cases where it randomly works). Of course, in cases where the question is: "Look at my UB code, lets discuss if it actually should return 7, 8 or segfault here!", I solidly agree with your statement. – Frodyne Sep 17 '19 at 12:21
  • @user3365922 Because `std::unique_ptr`’s deleter is part of its type (via a template parameter) so even if its constructor were templated it couldn’t do anything with this information. The destructor call is hard-coded. The reason for this is that `std::unique_ptr` is designed not to have any overhead compared to a raw pointer. It uses the same memory as a single raw object pointer, and has no access overhead. By contrast, `std::shared_ptr`’s deleter is type-erased and thus polymorphic. – Konrad Rudolph Sep 17 '19 at 12:27
  • That was great explanation for *question-1* (thanks for the extra explanation). What about second qeustion? – UserUsing Sep 17 '19 at 13:47
  • @KonradRudolph Yes, that cleared my doubts. Thanks! – UserUsing Sep 17 '19 at 15:07
3

Question 1: Do I have Undefined Behavior in any of the cases(1, 2, 3) ? Why?

There is no undefined behavior in the provided code sample. There would be undefined behavior, if you were to try to hold a pointer to Base and delete through that pointer. In the provided examples, you know the concrete class that is being deleted

Question 2: Isn't essential to construct the Base, in the member initializer lists of the constructors of the both derived classes(in the above particular case)?

The base class constructor is always called, no matter if you explicitly call it. If there is no explicit invocation, the default constructor will be invoked. If there is no default, no-argument constructor and you do not invoke a specific constructor, the code would fail to compile.

divinas
  • 1,787
  • 12
  • 12