Home > OS >  C 23 std::views::zip errors when given a view of references to abstract base class
C 23 std::views::zip errors when given a view of references to abstract base class

Time:01-22

Given this program, compiled with g -13 -std=c 2b test.cpp:

...where that is...

g  -13 (Debian 13-20230106-1) 13.0.0 20230106 (experimental) [master r13-5040-g53add162511]
#include <array>
#include <ranges>

auto
main() -> int
{
    struct B    { virtual void foo() = 0; };
    struct D: B { virtual void foo() {}   };

    auto ds = std::array<D, 10>{};
    auto to_b = [](D const& d) -> B const& { return d; };
    auto bs = ds | std::views::transform(to_b);

    for (auto& b: bs) {} // OK

    auto zip = std::views::zip(ds, bs);

    for (auto&& pair: zip) {} // error

    return 0;
}

Trying to loop over a std::views::zip in which one of the zipped ranges should be a reference to an abstract base class... just doesn't work. The errors are these:

In file included from /usr/include/c  /13/bits/stl_algobase.h:64,
                 from /usr/include/c  /13/array:43,
                 from test.cpp:1:
/usr/include/c  /13/bits/stl_pair.h: In instantiation of ‘struct std::pair<main()::D, main()::B>’:
/usr/include/c  /13/type_traits:3744:12:   recursively required by substitution of ‘template<class _Tp1, class _Tp2> struct std::__common_reference_impl<_Tp1&&, _Tp2&, 1, std::void_t<typename std::__common_ref_impl<_Tp1&&, _Tp2&, void>::type> > [with _Tp1 = std::pair<main()::D&, const main()::B&>; _Tp2 = std::pair<main()::D, main()::B>]’
/usr/include/c  /13/type_traits:3744:12:   required from ‘struct std::common_reference<std::pair<main()::D&, const main()::B&>&&, std::pair<main()::D, main()::B>&>’
/usr/include/c  /13/type_traits:3724:11:   required by substitution of ‘template<class ... _Tp> using std::common_reference_t = typename std::common_reference::type [with _Tp = {std::pair<main()::D&, const main()::B&>&&, std::pair<main()::D, main()::B>&}]’
/usr/include/c  /13/bits/iterator_concepts.h:324:13:   required by substitution of ‘template<class _Iterator>  requires (__iter_without_nested_types<_Iterator>) && (__cpp17_input_iterator<_Iterator>) struct std::__iterator_traits<_Iter, void> [with _Iterator = std::ranges::zip_view<std::ranges::ref_view<std::array<main()::D, 10> >, std::ranges::transform_view<std::ranges::ref_view<std::array<main()::D, 10> >, main()::<lambda(const main()::D&)> > >::_Iterator<false>]’
/usr/include/c  /13/bits/stl_iterator_base_types.h:177:12:   required from ‘struct std::iterator_traits<std::ranges::zip_view<std::ranges::ref_view<std::array<main()::D, 10> >, std::ranges::transform_view<std::ranges::ref_view<std::array<main()::D, 10> >, main()::<lambda(const main()::D&)> > >::_Iterator<false> >’
/usr/include/c  /13/bits/iterator_concepts.h:211:4:   required by substitution of ‘template<class _Iter, class _Tp>  requires  __primary_traits_iter<_Iter> struct std::__detail::__iter_traits_impl<_Iter, _Tp> [with _Iter = std::ranges::zip_view<std::ranges::ref_view<std::array<main()::D, 10> >, std::ranges::transform_view<std::ranges::ref_view<std::array<main()::D, 10> >, main()::<lambda(const main()::D&)> > >::_Iterator<false>; _Tp = std::incrementable_traits<std::ranges::zip_view<std::ranges::ref_view<std::array<main()::D, 10> >, std::ranges::transform_view<std::ranges::ref_view<std::array<main()::D, 10> >, main()::<lambda(const main()::D&)> > >::_Iterator<false> >]’
/usr/include/c  /13/bits/iterator_concepts.h:224:13:   required by substitution of ‘template<class _Iter, class _Tp> using std::__detail::__iter_traits = typename std::__detail::__iter_traits_impl::type [with _Iter = std::ranges::zip_view<std::ranges::ref_view<std::array<main()::D, 10> >, std::ranges::transform_view<std::ranges::ref_view<std::array<main()::D, 10> >, main()::<lambda(const main()::D&)> > >::_Iterator<false>; _Tp = std::incrementable_traits<std::ranges::zip_view<std::ranges::ref_view<std::array<main()::D, 10> >, std::ranges::transform_view<std::ranges::ref_view<std::array<main()::D, 10> >, main()::<lambda(const main()::D&)> > >::_Iterator<false> >]’
/usr/include/c  /13/bits/iterator_concepts.h:227:13:   required by substitution of ‘template<class _Tp> using std::__detail::__iter_diff_t = typename std::__detail::__iter_traits_impl<_Tp, std::incrementable_traits<_Iter> >::type::difference_type [with _Tp = std::ranges::zip_view<std::ranges::ref_view<std::array<main()::D, 10> >, std::ranges::transform_view<std::ranges::ref_view<std::array<main()::D, 10> >, main()::<lambda(const main()::D&)> > >::_Iterator<false>]’
/usr/include/c  /13/bits/iterator_concepts.h:232:11:   required by substitution of ‘template<class _Tp> using std::iter_difference_t = std::__detail::__iter_diff_t<typename std::remove_cvref<_Tp>::type> [with _Tp = std::ranges::zip_view<std::ranges::ref_view<std::array<main()::D, 10> >, std::ranges::transform_view<std::ranges::ref_view<std::array<main()::D, 10> >, main()::<lambda(const main()::D&)> > >::_Iterator<false>]’
/usr/include/c  /13/ranges:4436:26:   required from ‘constexpr auto std::ranges::zip_view<_Vs>::end() requires !((__simple_view<_Vs> && ...)) [with _Vs = {std::ranges::ref_view<std::array<main()::D, 10> >, std::ranges::transform_view<std::ranges::ref_view<std::array<main()::D, 10> >, main()::<lambda(const main()::D&)> >}]’
test.cpp:18:20:   required from here
/usr/include/c  /13/bits/stl_pair.h:194:11: error: cannot declare field ‘std::pair<main()::D, main()::B>::second’ to be of abstract type ‘main()::B’
  194 |       _T2 second;                ///< The second member
      |           ^~~~~~
test.cpp:7:16: note:   because the following virtual functions are pure within ‘main()::B’:
    7 |         struct B    { virtual void foo() = 0; };
      |                ^
test.cpp:7:36: note:     ‘virtual void main()::B::foo()’
    7 |         struct B    { virtual void foo() = 0; };

Is this correct? I don't think I'm asking for a std::pair<D, B>; I think I'm asking for a std::pair<D&, B&>. I don't see where I am or should be causing an abstract B to be constructed. I would totally understand the error if I was... but am I...?

I've read the 2 Standard papers about std::views::zip and various discussion of issues around references and value_type, but I don't see anything to explain this.

So, either I've missed some crucial detail in the Standard papers and/or diagnostics (more likely?)... or perhaps there's a gremlin in libstdc (less likely?) that tries to instantiate pair<D, B> when not needed - and I'd appreciate input from the better-versed anyway :)

CodePudding user response:

Iterators don't just have reference (unfortunately named, since not always a language reference), they also have a value_type. In Ranges, a lot of this model is more formalized. The core concept of C 20 input iterators is indirectly_readable, of which the key parts are basically:

  • an iterator has a value_type
  • an iterator has a reference, which is the type you get by doing *ci on a const I (note the const. Also note that *i and *ci have to be the same type)
  • there is a common reference between reference&& and value_type& (note the &)

There are other requirements which you can see, but those are the most relevant ones here. Typically (tho not always), value_type is just remove_cvref_t<reference>. One case where this isn't the case is zip_view<Rs...>, where reference is tuple<range_reference_t<Rs>...> and value_type is tuple<range_value_t<Rs>...>.

Now, with the transform example, you have reference is B& and value_type is B. This is fine, since the common reference requirement would use B& (not B), and you're not doing anything else that tries to construct a B (e.g. calling ranges::min or something).

But with the zip example, let's say actually we were just doing zip(bs). Here, reference is going to be tuple<B&>, but what is value_type? Well, it kind of has to be tuple<B>, but you can't make a tuple<B>, so that's ill-formed (in the actual OP it's a std::pair, but there was a recent change to always use std::tuple, the difference doesn't matter here).


So, okay, what can we actually do? There isn't actually another value_type we can produce. Reusing tuple<B&> is a bad idea, that would break any algorithm you use that actually wants to have a value type.

The only alternative is... not having a value_type at all here. There are quite a few algorithms that don't use value_type (the simplest of which is just the for loop you're trying to do), and it admittedly kind of sucks to not be able to support those algorithms due to a requirement that... you're not using. But that's the design as it exists right now and it would, I think, be difficult to change.


So the solution really is to just try to avoid ranges of Abstract&. Abstract* would work fine, since there you'd end up with a value_type of Abstract* also. Or reference_wrapper<Abstract>, which would at least make it clear that you don't have null pointers.

  • Related