6

以下代码在 C++11、C++14 和 C++17 中编译,但在 C++20 中不编译。对标准的哪些更改破坏了此代码?

#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;
}

错误很长,但可以总结为:

模板参数必须是一个完整的类

4

1 回答 1

11

这始终是未定义的。[res.on.functions]/2.5说:

特别是,在以下情况下效果是不确定的:

  • [...]
  • 如果在实例化模板组件或评估概念时将不完整的类型 ([basic.types]) 用作模板参数,除非该组件特别允许。

std::pair不(也不能)支持不完整的类型。你只是依靠实例化的顺序来解决这个问题。库中的某些更改略微更改了评估顺序,从而导致错误。但是未定义的行为是未定义的——它以前碰巧起作用,现在碰巧不起作用。


至于为什么它特别是 C++20 导致它失败。在 C++20 中,迭代器改变了这个新iterator_concept想法。为了实例化,reverse_iterator需要确定概念应该是什么。看起来像这样

#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

现在,在检查过程中random_access_iterator,迭代器概念层次结构的根被奇妙地命名为input_or_output_iterator,在[iterator.concept.iterator]中指定:

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

所以,我们必须*i对我们的迭代器类型进行处理。也就是说__gnu_cxx::__normal_iterator<std::pair<foo, foo>**, std::vector<std::pair<foo, foo>*> > ,在这种情况下。现在,*i触发 ADL - 因为它当然会。并且 ADL 需要实例化所有关联类型——因为这些关联类型可能已经注入了可能成为候选对象的朋友!

反过来,这需要实例化pair<foo, foo>——因为,我们必须检查。然后在这种特定情况下最终失败,因为实例化一个类型需要实例化该类型的所有特殊成员函数,而 libstdc++ 实现条件赋值的方式std::pair是使用Eric Fisellier 的技巧

      _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;
      }

并且is_copy_assignable需要完整的类型,而我们没有。

但实际上即使pair在这种情况下使用概念来检查,这仍然会涉及实例化相同的类型特征,因此我们最终会处于相同的位置。

故事的寓意是,未定义的行为是未定义的。

于 2020-07-31T16:08:02.503 回答