7

The following code compiles in C++11, C++14, and C++17, but does not compile in C++20. What change to the standard broke this code?

#include <vector>
#include <utility>

template<typename T>
struct bar
{
    typename T::reverse_iterator x;
};

struct foo
{
    bar<std::vector<std::pair<foo, foo>*>> x;
};

int main()
{
    foo f;
}

The error is quite long, but can be summarized as:

template argument must be a complete class

TylerH
  • 20,799
  • 66
  • 75
  • 101
Pubby
  • 51,882
  • 13
  • 139
  • 180

1 Answers1

13

This was always undefined. [res.on.functions]/2.5 says:

In particular, the effects are undefined in the following cases:

  • [...]
  • If an incomplete type ([basic.types]) is used as a template argument when instantiating a template component or evaluating a concept, unless specifically allowed for that component.

std::pair does not (and cannot) support incomplete types. You were just relying on order of instantiation to kind of get around that. Something changed in the library that slightly changed the evaluation order, leading to the error. But undefined behavior is undefined - it happened to work before and it happens to not work now.


As to why it's specifically C++20 that is causing this to fail. In C++20, iterators changed to have this new iterator_concept idea. In order to instantiate that, reverse_iterator needs to determine what the concept should be. That looks like this:

#if __cplusplus > 201703L && __cpp_lib_concepts
      using iterator_concept
    = conditional_t<random_access_iterator<_Iterator>,
            random_access_iterator_tag,
            bidirectional_iterator_tag>;
      using iterator_category
    = __detail::__clamp_iter_cat<typename __traits_type::iterator_category,
                     random_access_iterator_tag>;
#endif

Now, in the process of checking random_access_iterator, the root of iterator concept hierarchy is wonderfully named input_or_output_iterator, specified in [iterator.concept.iterator]:

template<class I>
  concept input_or_output_iterator =
    requires(I i) {
      { *i } -> can-reference;
    } &&
    weakly_incrementable<I>;

So, we have to do *i on our iterator type. That's __gnu_cxx::__normal_iterator<std::pair<foo, foo>**, std::vector<std::pair<foo, foo>*> > , in this case. Now, *i triggers ADL - because of course it does. And ADL requires instantiation of all the associated types - because those associated types might have injected friends that could be candidates!

This, in turn, requires instantiating pair<foo, foo> - because, we have to check. And then that ultimately fails in this specific case because instantiating a type requires instantiating all of the type's special member functions, and the way that libstdc++ implements conditional assignment for std::pair is using Eric Fisellier's trick:

      _GLIBCXX20_CONSTEXPR pair&
      operator=(typename conditional<
        __and_<is_copy_assignable<_T1>,
               is_copy_assignable<_T2>>::value,
        const pair&, const __nonesuch&>::type __p)
      {
    first = __p.first;
    second = __p.second;
    return *this;
      }

And is_copy_assignable requires complete types and we don't have one.

But really even if pair used concepts to check in this case, that would still involve instantiating the same type traits, so we'd ultimately end up in the same position.

Moral of the story is, undefined behavior is undefined.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • Note the argument isn't a pair, it's a pointer to a pair. Ignoring pointer arithmetic, pointers to incomplete types are complete types, no? – Pubby Jul 31 '20 at 16:15
  • @Pubby: But by typing the text `std::pair`, you *require* the compiler to instantiate that template. And that instantiation requires `T` to be a complete type. It doesn't matter (for the most part) *where* you type that text. – Nicol Bolas Jul 31 '20 at 16:21
  • @NicolBolas I don't think that's true. Consider literally any templated node class http://coliru.stacked-crooked.com/a/655220ab33da9f2a – Pubby Jul 31 '20 at 16:24
  • @Pubby: The template `node` does not use `T` in any way that requires it to be complete. `pair` does. When I said "that instantiation", I meant "`std::pair`'s instantiation", not instantiation in general. – Nicol Bolas Jul 31 '20 at 16:25
  • @NicolBolas Consider a second example then. The compiler isn't instantiating the template when used in a pointer type: http://coliru.stacked-crooked.com/a/613ae814412efb4c – Pubby Jul 31 '20 at 16:31
  • 1
    @Pubby I asked exactly [this](https://stackoverflow.com/questions/63061188/what-are-the-rules-for-standard-library-containers-and-incomplete-types) recently. Conclusion there at least is that it's UB. – cigien Jul 31 '20 at 16:49