Serge Weinstock
Serge Weinstock

Reputation: 1501

Which concept to use for checking an iterator's value type?

I'm new to concepts and ranges/views.

I'm trying to write the initialization of a class by passing a sequence of values defined by an iterator or by a range/view.

I'm able to check that the function arguments are iterators or ranges. But I can't check that the values returned by the iterators are a specific type.

For example (C++23):

#include <print>
#include <vector>

struct Data {
    int i;
    std::string l;
};

struct DataContainer {
    // iterator version
    // QUESTION: how can I check that I handles "Data"?
    template<std::input_iterator I, std::sentinel_for<I> S>
    void add(I start, S end) {
        for (auto data = start; data != end; ++data) {
            _data.push_back(*data);
        }
    }
    // range/views version
    // QUESTION: how can I check that R handles "Data"?
    template<std::ranges::input_range R>
    void add(R &&r) {
        add(std::begin(r), std::end(r));
    }
    void dump() const {
        for (const auto& d: _data){
            std::print("[{},'{}'], ", d.i, d.l);
        }
        std::println();
    }
    std::vector<Data> _data;
};

int main()
{
    std::vector<Data> init{{1, "one"}, {2, "two"}, {3, "three"}};
    {
        DataContainer dc;
        dc.add(init.begin(), init.end());
        dc.dump();
    }
    {
        DataContainer dc;
        dc.add(init);
        dc.dump();
    }
    return 0;
}

How can I check that *start returns a Data?

Upvotes: 5

Views: 143

Answers (3)

Ted Lyngmo
Ted Lyngmo

Reputation: 117832

In both cases you could add a constraint:

template <std::input_iterator I, std::sentinel_for<I> S>
    requires std::convertible_to<std::iter_value_t<I>, Data> // constraint added
void add(I start, S end) {
    // ...
}

template <std::ranges::input_range R>
    requires std::convertible_to<std::ranges::range_value_t<R>, Data> // constraint added
void add(R&& r) {
    // ...
}

std::convertible_to:

The concept convertible_to<From, To> specifies that an expression of the same type and value category as those of std::declval<From>() can be implicitly and explicitly converted to the type To, and the two forms of conversion produce equal results.

std::iter_value_t

  1. Computes the value type of T.
  • If std::iterator_traits<std::remove_cvref_t<T>> is not specialized, then std::iter_value_t<T> is std::indirectly_readable_traits<std::remove_cvref_t<T>>::value_type.
  • Otherwise, it is std::iterator_traits<std::remove_cvref_t<T>>::value_type.

std::ranges::range_value_t:

Used to obtain the value type of the iterator type of range type R.

A more relaxed version could use std::constructible_from instead of std::convertible_to.

Upvotes: 6

Pavel
Pavel

Reputation: 342

Barry gave a great comprehensive answer!

Just as a small addition - if you use standard algorithms (in this case std::ranges::copy), then the code can be made more concise, and additional requires do not need to be written - it is "sewn" into the algorithm itself:

template<std::ranges::input_range R>
void add1(R&& r) 
{
    std::ranges::copy(r, std::back_inserter(_data));
}

Upvotes: 0

Barry
Barry

Reputation: 303636

For this one:

    // iterator version
    // QUESTION: how can I check that I handles "Data"?
    template<std::input_iterator I, std::sentinel_for<I> S>
    void add(I start, S end) {
        for (auto data = start; data != end; ++data) {
            _data.push_back(*data);
        }
    }

The type of *data is iter_reference_t<I>. This is called the iterator's reference type, which is a bit misleading because it doesn't actually have to be any kind of reference. Typically, algorithms should be constrained based on the reference type - that's what you actually interact with. So this becomes:

template <std::input_iterator I, std::sentinel_for<I> S>
  requires std::convertible_to<std::iter_reference_t<I>, Data>
void add(I, S);

You really should only be using the value type of an iterator if you actually need to produce values.


For this one:

    // range/views version
    // QUESTION: how can I check that R handles "Data"?
    template<std::ranges::input_range R>
    void add(R &&r) {

The idea is the same, it's just spelled std::ranges::range_reference_t<R>. This is defined to be std::iter_reference_t<std::ranges::iterator_t<R>> (i.e. the reference type of the range is the reference type of the range's iterator type).

template <std::ranges::input_range R>
  requires std::convertible_to<std::ranges::range_reference_t<R>, Data>
void add(R &&r) {
  add(std::ranges::begin(r), std::ranges::end(r));
}

Note that this needs to use std::ranges::{begin,end}, not std::{begin,end}. The latter is incorrect for some range types - those that define begin and end as non-member functions that are found by ADL, but aren't in std.

Upvotes: 11

Related Questions