3

Let's say that I would like to write universal function that prints out the standard output range from a collection. Since it supposed to be universal I assume that...

std::vector<std::string> names = { "John", "Henry", "Mark" };

as well as:

std::vector<int> years = { 100, 200, 400 };

.. will be possible to printed out.

Since types of collection may be different, and there is not base class for STL collection giving me chance to pass base class iterators I use template function:

template<typename TIterator>
void PrintRange( TIterator beginIter,TIterator endIter )
{           
    for( auto it = beginIter; it != endIter; ++it )
    {
        std::cout << *it << std::endl;
    }
}

Everything now works well, now I can write:

PrintRange( names.begin(), names.end() );

and:

PrintRange( years.begin(), years.end() );

But now I want to help client of my function to faster understand why there is an error when he use it. Now when I call:

PrintRange( 100, 400 );

There is error:

main.cpp:23:34: error: invalid type argument of unary ‘*’ (have ‘int’)

I would like to print something like:

One of arguments does not correspond to expected argument of type 'iterator'

So what approach to this problem is best:

  1. It's not important to care that the error message is not as meaningful as I expected. User should analyse template class code to establish reason of his mistake.

  2. Use static_assert to assert all know possibilities.. but how to assert that the argument of function is ANY iterator since there is no base class?

static_assert( std::is_base_of::iterator >::value );

This would only assert vector of string iterator...

Adam Stepniak
  • 815
  • 6
  • 21
  • Why is the beginning and ending iterator of different types? They should be of the same one – Fureeish Dec 31 '17 at 14:19
  • If you add good documentation to your function, then the user doesn't need to examine any of your template code, and you don't need to put in any additional effort into validating input. Let the compiler validate! – Cris Luengo Dec 31 '17 at 16:07

2 Answers2

2

Personally, I think that your first approach is totally fine, so you might not care much about additional error message.

On the other hand, if you decide on printing a meaningful message, you might implement a custom type trait for detecting iterators as it is explained here and then use it with static_assert. So the code transforms into something like:

template<typename TIterator>
void PrintRange(TIterator beginIter, TIterator endIter)
{        
    static_assert(is_iterator<TIterator>::value,
        "TIterator is not an iterator type");

    for( auto it = beginIter; it != endIter; ++it )
    {
        std::cout << *it << std::endl;
    }
}
Edgar Rokjān
  • 17,245
  • 4
  • 40
  • 67
0

The answer provided by Edgar Rokyan is quite helpful, but I am aware of another solution (probably a worse one, since we have to implement much more code).

This solution is not exactly a type check for an iterator, but rather a hint in which direction we can go. Given your PrintRange function, we assume that we need 3 operators to be defined for TIterator - operator*, operator++ and operator !=.

To check whether or not an operator is defined, you can use this:

template<typename T>
struct has_deref_op{
    private:
        template<typename U>
        static constexpr auto test(int) -> decltype(std::declval<U>().operator*() == 1,
                                                    std::true_type());

        template<typename U>
        static constexpr std::false_type test(...);

    public:
        static constexpr bool value = std::is_same<decltype(test<T>(0)),
                                                   std::true_type>::value;
}; 

This code will check for the presence of operator* in T implementation. You could then add a static_assert that will use it in order to validate an argument:

template<typename TIterator>
void PrintRange( TIterator beginIter, TIterator endIter )
{
    static_assert(has_deref_op<TIterator>::value, "argument must implement operator*");
    for( auto it = beginIter; it != endIter; ++it )
    {
        std::cout << *it << std::endl;
    }
}

This solution has a major flaw - It requires writing quite a lot of code in order to simplify an error message. To be honest, while this approach would work quite nicely, I would stick with the default error message. It is quite self-explanatory - if you provide an int, which does not have operator* defined, you get an error about that.

EDIT: After reading the question linked by Edgar in his answer, it appears that it recommends implementing is_iterator that would work similarly to this approach. My bad for not reading carefully the first time

Fureeish
  • 12,533
  • 4
  • 32
  • 62