std::vector, incomplete type and inherited constructors

230 views Asked by At

I have a class with a vector member variable whose T is declared, but not defined. This can be a problem if no destructor for the class is defined, since the compiler might pick some other translation unit to generate the destructor in. If that TU doesn't have a definition for T, compilation fails.

struct undefined;

struct S
{
    S(int);
    std::vector<undefined> v;
};

int main()
{
   S s(42);     // fails in ~vector(), `undefined` is undefined
}

To work around this problem, I usually add a defaulted destructor as an anchor in the file that implements the class, which has access to the definition.

struct undefined;

struct S
{
    S(int);
    ~S();       // implemented in another TU as `S::~S() = default;`
    std::vector<undefined> v;
};

int main()
{
   S s(42);     // ok
}

I'm trying to do the same thing with a derived class that inherits the base class' constructor:

#include <vector>

struct undefined;

struct base
{
   base(int);
};

struct derived : base
{
   using base::base;
   ~derived();
   std::vector<undefined> v;
};


int main()
{
   derived d(1);  // fails in ~vector(), `undefined` is undefined
}

If I change derived to not inherit the constructor, compilation succeeds:

#include <vector>

struct undefined;

struct base
{
   base(int);
};

struct derived : base
{
   //using base::base;           <--- note
   derived(int);
   ~derived();
   std::vector<undefined> v;
};


int main()
{
   derived d(1);  // ok
}

What's really confusing to me is that clang says this:

stl_vector.h:336:35: error: arithmetic on a pointer to an incomplete type 'undefined'

[blah]

a.cpp:12:14: note: in instantiation of member function 'std::vector<undefined>::~vector' requested here
        using base::base;
                    ^

It sounds like using base::base is also bringing in something that's destroying the vector, but I have no idea what. I've tried deleting copy/move constructors/operators in both classes, but the error remains. Both g++ and Visual C++ fail with similar errors.

What's going on?

3

There are 3 answers

0
isanae On BEST ANSWER

As mentioned by Raymond Chen in this comment, the reason using base::base requires the vector's destructor is because exceptions may be thrown from the constructor, which may require destroying member variables. The only solution is to write the constructor manually.

A constructor needs to destroy fully constructed member variables if an exception is thrown. In this example, a must be destroyed, but not b:

std::string throws()
{
    throw 1;
}

struct S
{
    std::string a, b;

    S() : a(""), b(throws()) {}
};

In the next example, the compiler generates a default constructor and a destructor. Both can call ~vector() and so both need the definition of incomplete:

struct incomplete;

struct S
{
    std::vector<incomplete> v;
};

int main()
{
    S s;  // error
}

Adding a declaration for both fixes the error:

struct incomplete;

struct S
{
    std::vector<incomplete> v;
    S();
    ~S();
};

int main()
{
    S s;   // ok
}

But an inherited constructor is also generated by the compiler and therefore needs ~vector() in case an exception is thrown:

struct undefined;

struct base
{
    base(int);
};

struct derived : base
{
    std::vector<undefined> v;

    using base::base;   // <- needs ~vector() in case of exceptions
    ~derived();
};

And so the only solution is to declare the constructor manually and implement it somewhere else.

2
Red.Wave On

std::vector has a 2nd type parameter called allocator with a default value:

template<typename T, typename alloc=std::allocator<T>
class std::vector;

This second type gets instantiated inside every constructor of std::vector. If allocator is not an empty type(has none-static data members), eventually one instance of the allocator is composed in the layout of std::vector. The default allocator(std::allocator<T>) needs to know size and constructor signature of elements. When the declaration of your class constructor - but not its definition - is present, no call to std::vector constructor and allocator functions is made; thus there's no demand for element type to be complete. If inheriting constructors is a demand, wrap your vector in a box whose destructor and constructors are defined in same TU as your class:

struct derived:base{
    using base::base;
    struct box_t{
      box_t();
      box_t(box_t const&);
      box_t(box_t&&);
     ~box_t();
      auto& operator=(box_t const&);
      auto& operator=(box_t &&);
      std::vector<fwd_element_t> vec;
    } box;
};

In the same TU where fwd_element_t is defined, you need define the box_t members as default:

auto& derived::box_t::operator(box_t&&)=default;
//Define the rest just the same

You can also use PIMPL with std:: unique_ptr<fwd_element_t[], custom_deleter> in a similar fashion. But this time, you'll only need to properly define the void custom_deleter::operator()(fwd_element_t *) instead.

Or try to use std::vector<fwd_element_t, custom_alloc> and define every single member of custom_alloc. But I am not sure about this last alternative.

1
fjs On

I think you should provide the definition of struct undefined through a header file, or use std::vector<undefined*> as a compromise.

Error message stl_vector.h:336:35: error: arithmetic on a pointer to an incomplete type 'undefined' probably relates to the implementation of std::vector - though I cannot guarantee since I did not read it. E.g. the implementation of method operator[](int idx) for template class T would likely look like:

return *((T*)(array_base_ptr + idx * sizeof(T)))

Obviously, the definition of template class T needs to be known for the template code to be correctly instantiated (otherwise sizeof cannot return the correct size). Remember the STL is standard template library, so the definition of T needs to be known at compile time.

Using std::vector<undefined*> without defining class undefined is not a problem since pointers have fixed size. I.e. sizeof(T) is always size of a pointer - 8 bytes on 64-bit platforms.