container of container of incomplete type: UB or ok?

so usually you cant instantiate a std::pair with an incomplete type like so:
1
2
class incomplete;
std::pair<incomplete, int> myPair;

^This yields a compile error.

However i noticed i *can* instantiate a std::pair of an incomplete type as a template argument of another STL container, ex. vector:
1
2
class incomplete;
std::vector< std::pair<incomplete, int> > myPairs; 

^This doesn't yield any compiler errors or runtime errors AFAIK (tested on g++ only)

Can anyone explain why this works (or if its UB)? i know since c++17 std::vector is allowed to be instantiated with incomplete types, however, in this scenario its the std::pair that gets instantiated with an incomplete type, which you shouldn't be able to do, right?

I see how, theoretically, the compiler can make it work regardless of the fact that the std::pair is being instantiated with an incomplete type in this scenario: because the vector only stores pointers anyway, so maybe it can just ignore the fact that the std::pair is being instantiated with an incomplete type until runtime when it needs to be allocated on the heap (at which point the incomplete type should now be complete). However, even though this *can* compile, i'm kinda worried that this might be UB because i remember reading somewhere that its always UB to instantiate any STL container with an incomplete type (the exception being std::vector in c++17) so since i am technically instantiating a std::pair of an incomplete type it should be UB. Can anyone confirm or deny this?
Last edited on
because the vector only stores pointers anyway,


Sorry, what? No. The vector creates and stores actual std::pair objects. The fact that it allocates memory on the heap to do so, rather than using memory on the stack, does not change that.

maybe it can just ignore the fact that the std::pair is being instantiated with an incomplete type until runtime


Requiring complete types is something the compiler needs, so it will happen at compile-time. It can't possibly be checked at runtime.

I suspect the reason your code compiles is that nothing in the code you've posted actually requires the type to be complete, because nothing in the code you've posted invokes anything that actually requires a complete type - that is, nothing requires the creation of any std::pair<incomplete, int> objects. It's only at the point where your code actually creates such objects - e.g. when you actually store any such objects in the vector - that the compiler would complain.

I am slightly surprised that the compiler doesn't consider a std::pair<incomplete, int> to be incomplete, but I suspect if you read the documentation for std::pair, it would become clear why.
Last edited on
because the vector only stores pointers anyway,
Eh, I see nothing wrong with wording it like this. If it stored full objects, you'd need to know the definition. If it stores only a pointer, then it doesn't need to know the full definition until the point in the code where an object is actually created, as you mentioned.

The question is about how stringent the implementation of the standard containers must be as to whether or not they allow incomplete types in them (just so happening to work vs. being guaranteed to work on all conforming compilers), which I'm not exactly sure of.

Edit: According to https://stackoverflow.com/a/41913654, as of C++17 it is std::vector, std::list, and std::forward_list that can handle incomplete types. If you use other standard containers, it sounds like it's not guaranteed to work.

I am slightly surprised that the compiler doesn't consider a std::pair<incomplete, int> to be incomplete
It is incomplete, though. Creating an object of type std::pair<incomplete, int> will not work.
Last edited on
Do you have a better way of communicating the distinction between an object + all of the memory it owns, versus just the data members (+ a vtable pointer in some cases)? I'm pretty sure OP was referring to the latter.

I cannot reproduce the described compilation success using g++ 10.1 or clang++ 10.0.1 (and libstdc++). It actually makes sense for it to fail: while the default constructor shouldn't necessarily require T to be complete, the destructor might need to iterate over the vector's elements to destroy them (we know at compile time that it doesn't, but that doesn't matter here).

And this is what happens in practice using libstdc++. One runs into both a case of invalid pointer arithmetic, and a failed static assertion in the instantiation of the std::vector's destructor.

-Albatross
just to rephrase i meant the incomplete type is only incomplete at the instantiation of the pair & vector, but after that it becomes complete like this:
1
2
3
class incomplete;
std::vector< std::pair<incomplete, int> > myPairs;
class incomplete{};


doing this i am then able to create pairs objects and push them into the vector just fine, ex:
1
2
3
4
5
6
7
8
9
class incomplete;
std::vector< std::pair<incomplete, int> > myPairs;
class incomplete{};

int main()
{
  myPairs.emplace_back( std::make_pair(incomplete{}, 3) );
  return 0;
}


But the thing im unsure of is whether its UB since technically at the point of template instantiation the class is incomplete
Last edited on
That's what I thought you meant, too, otherwise it wouldn't make sense since I agree that it has to know how to destruct any potential objects, like Albatross said.

My interpretation is that it's not UB, but someone can please feel free to disagree.
This is apparently the paper that was implemented into C++17:
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4510.html

New paragraph in 20.7.9 [default.allocator], before the synopsis, as the first paragraph:

All specializations of the default allocator satisfy the allocator completeness requirements (17.6.3.5.1).

// ...

New paragraph in 23.3.6.1 [vector.overview], as paragraph 3, after the synopsis:

An incomplete type T may be used when instantiating vector if the allocator satisfies the allocator completeness requirements (17.6.3.5.1). T shall be complete before any member of the resulting specialization of vector is referenced.
Last edited on
just to rephrase i meant the incomplete type is only incomplete at the instantiation of the pair & vector,


It's incomplete at the time the vector is instantiated. But do we have any reason to believe any pair objects are being instantiated when the vector is instantiated, if you're using the default constructor to create it in an empty state?

Seems to me the answer is "no" - and that compilation would fail if the implementation of std::vector that you're using were actually trying to instantiate the std::pair objects before the type was complete.

Albatross wrote:
Do you have a better way of communicating the distinction between an object + all of the memory it owns, versus just the data members (+ a vtable pointer in some cases)? I'm pretty sure OP was referring to the latter.


Not in any way more concise than yours, I suppose. But given that other things in the OP were vague and seemed to suggest some misunderstandings, I thought it would be best to make sure we were being absolutely clear.

The point is that at some point, the vector code will need to use actual pair objects, rather than just pointers, and that when that happens, the definition will need to be complete.

To summarize:

1. When the vector code needs to create std::pair objects, those objects will need to be completely defined.
2. The default constructor for vector may not need to create any std::pair objects, so they can be incomplete at this point.
Last edited on
Topic archived. No new replies allowed.