Search code examples
c++language-lawyerstd-rangesc++23

How to write a C++23 constant_range factory a la iota, with by-value dereference operator


I have written a range factory very similar to iota, but generating instances of a user-defined type. The synopsis looks something like this:

struct MyType;

struct MyIterator {
    using value_type = MyType;
    using iterator_concept  = std::input_iterator_tag;
    // ...
    auto operator*() const -> MyType;
};

struct MyView : public std::ranges::view_interface<MyView> {
    auto begin() const -> MyIterator;
    auto end() const -> MyIterator;
};

I've done my best to make this follow iota_view's design. However, I find that MyView is not a constant range:

static_assert( std::ranges::constant_range<std::ranges::iota_view<int>>);
static_assert(!std::ranges::constant_range<MyView>); // Not a constant_range!

After looking at the reference, I traced this back to MyIterator not being a constant-iterator because:

  • std::same_as<std::iter_const_reference_t<MyIterator>, std::iter_reference_t<MyIterator>> doesn't hold because
  • std::iter_reference_t<MyIterator> is MyType but std::iter_const_reference_t<MyIterator> is const MyType.

If I replace MyType with int, just like iota_view, then both are non-const.

So I went to figure out where the difference comes from, and it turns out that std::iter_const_reference_t is defined in terms of std::common_reference_t, which has been extensively covered here. And it, in turn, has this complicated strategy to figure out the resulting type, but actually both int and MyType fall into the same branch, which is (see cppreference):

template<class T> T val();
decltype(false? val<const T&&>() : val<T>())

And it turns out that this is int if T is int, and const MyType if T is MyType.

I don't know why the conditional operator resolves types like that, but anyway I cannot change it; but fortunately the standard allows me to inject my own behavior by specializing std::basic_common_reference, which I can do for example like this:

template<template<class> class TQual, template<class> class UQual>
struct std::basic_common_reference<MyType, MyType, TQual, UQual>
{
    using type = std::common_type_t<TQual<MyType>, UQual<MyType>>;
};

And, indeed, this fixes the issue. But I have no idea whether this is correct or even intended to be used like that. In general, it doesn't seem very practical since there is nothing range-specific about MyType - it just happens to be the type I'm generating, and in my real use case it's a template parameter of the view. Which means that whether the view conforms to constant_view entirely depends on whether the type it's templated on has this basic_common_reference specialization... which seems odd.

For your reference, here is a godbolt link.

So here, finally, is the question: What is the correct way to implement a constant_view with an lvalue reference type? Appreciate your insights.


Solution

  • The simplest way is to return a const MyType object:

    struct MyIterator {
        // ...
        auto operator*() const -> const MyType;
    };
    
    static_assert(std::ranges::constant_range<MyView>);