std::vector, incomplete type and inherited constructors
Asked Answered
W

3

7

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?

Willodeanwilloughby answered 19/8, 2023 at 2:3 Comment(8)
vector<T> has to know the size of it's elements and that can't be determined from the incomplete type. You can have a vector of pointers to an incomplete type.Phenacite
@Phenacite vector can most definitely use an incomplete type as long as you don't do anything with it, see this and this.Willodeanwilloughby
Ok, it appears you can as of c++17 have an incomplete type so long as you don't use it until the type is complete. Interesting. Thanks for the links.Phenacite
Inherited constructors are as if you had written derived(int v) : base(v) = default; (not valid syntax but I hope you get the idea) - this means that the compiler has to generate code for the constructor, and that means also generating code for the vector's destructor, in case an exception is thrown from the constructor.Insipid
@RaymondChen I wish you'd write answers instead of comments so we could accept them.Willodeanwilloughby
You can write up your own answer based on the comment and accept it.Insipid
Just out of curiosity, what kind of application requires this kind of programming pattern? What prevents you from including the definition for struct undefined?Odelle
@Odelle Nothing prevents me, but it's defined in another header with a bunch of stuff in it, and I'd prefer not to include it to reduce compilation times. Users of struct S never need the definition of undefined itself, it's used internally by S.Willodeanwilloughby
W
0

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.

Willodeanwilloughby answered 28/11, 2023 at 17:53 Comment(0)
O
-1

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.

Odelle answered 19/8, 2023 at 4:31 Comment(1)
See the links the OP posted in the comment below the question. I think that using pointers is not the solution they look for.Rodrigo
S
-1

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.

Superphosphate answered 19/8, 2023 at 10:51 Comment(2)
I think most of this answer is incorrect. Instantiating an std::allocator<undefined> compiles fine and doesn't do anything that would need the definition. Your suggestion to replace a vector<T> with a unique_ptr<T[]> is also not very useful. As for creating my own allocator, it would make no difference because the allocator has nothing to do with this.Willodeanwilloughby
@Willodeanwilloughby every constructor and most other members of vector call allocator members. The specific case of constructor actually doesn't need to access members except through allocator. The default implementation is template, therefore inline and tries to call element constructors that you have not provided. In fact, the allocator solution enables inline definition of your member functions too. Using unique_ptr is standard practice for the PIMPL idiom. Obviously you are trying to hide implementation of elements; therefore PIMPL is the primary solution. Else, why hide the layout of elements?Superphosphate

© 2022 - 2025 — McMap. All rights reserved.