Search code examples
c++inheritancegccclangdefault-copy-constructor

Is a default-generated constructor not required to construct all base classes?


I've come across a case in which type-safe c++ produces non-matching ctor/dtors. The following code generates two constructors for A. The default constructor also constructs its base (B), but the default-generated copy/move ctor does not construct B. Later, it destroys B, so we get non-matching ctor/dtor.

I've tried this with gcc and clang and both fail. In the gcc bug report forum they suggested this isn't a gcc problem. I may be missing something, but isn't there something odd when type-safe code results in a dtor call for a class that hasn't been constructed?

Program output:

B() -> INSERT: 0x7fff55398b2f
~B() -> ERASE: 0x7fff55398b2f
~B() -> ERASE: 0x7fff55398b40         // <- unmatched dtor call 
Assertion failed: (!all.empty()), function ~B, file gcc_bug.c, line 20.

Code follows:

#include <set>
#include <iostream>
#include <cstdint>
#include <cassert>
#include <experimental/optional>


std::set<std::uintptr_t> all;

struct B
{
    B()
    {
        std::cerr << "B() -> INSERT: " << this << "\n";
        all.insert((std::uintptr_t)this);
    }
    ~B()
    {
        std::cerr << "~B() -> ERASE: " << this << "\n";
        assert(!all.empty());                                // FAILS
        assert(all.find((std::uintptr_t)this) != all.end()); // FAILS
        all.erase((std::uintptr_t)this);
    }
};
struct A : B {};

static std::experimental::optional<A> f()
{
    A a;
    return a;
}

int main()
{
    auto a = f();
    return 0;
}

Solution

  • You have a B being created by the implicitly defined copy constructor. This, of course, is not calling B::B(). If you add the following constructor:

    B(const B& other) : B()
    {
        *this = other;
    }
    

    you will see the output:

    B() -> INSERT: 0x7ffe57ef918f
    B() -> INSERT: 0x7ffe57ef91b0
    ~B() -> ERASE: 0x7ffe57ef918f
    ~B() -> ERASE: 0x7ffe57ef91b0
    

    The important point to take away is this: every constructor completely constructs the object. By default, a copy constructor will not call the default constructor (and, obviously, vice versa). Therefore if you have something that needs to be done in every constructor, you must explicitly do that in every constructor, either by direct call, or by constructor chaining.