1

I want to write a class that takes a pair of iterators as parameters to the constructor, but I dont know how to raise an error at compile-time when those iterators' value_type doesn't match an expected type. This is what I tried using typeid:

#include <vector>

struct foo {
    std::vector<double> data;
    template <typename IT>
    foo(IT begin, IT end){
        typedef int static_assert_valuetype_is_double[
               typeid(typename IT::value_type) == typeid(double) ? 1 : -1
        ];
        std::cout << "constructor called \n";
        data = std::vector<double>(begin,end);
    }    
};

int main()
{
    std::vector<double> x(5);
    foo f(x.begin(),x.end()); // double: ok

    std::vector<int> y(10);
    foo g(y.begin(),y.end()); // int: should not compile
}

Note that in this case, int to double would be fine, but thats just an example and in the real code the types have to match exactly. To my surprise in both cases, the constructor works without errors (there is only a warning about the unused typedef). Does the -1 sized array static assert trick not work when the typedef is declared inside a method? How do I produce an error when IT::value_type is the wrong type?

PS: would be nice if there was an easy C++98 solution, but if this gets too complicated, I could also accept a C++11 solution.

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
  • `double*` is a valid iterator whose value type is obviously `double`, yet `double*` has no nested `double*::value_type`. I think you need `typeid(*begin)` – MSalters Mar 19 '18 at 09:07
  • @MSalters sorry, if i wasnt clear in my question. I am mainly concerned about std containers and their iterators. If a solution excludes some exotic iterators, thats not a problem – 463035818_is_not_an_ai Mar 19 '18 at 09:10
  • Well, pointers aren't exotic, and they are the natural iterators of arrays. See `std::begin(MyArray)` – MSalters Mar 19 '18 at 09:12
  • 1
    This doesn't address the question, but the constructor should use an initializer list instead of assigning a new value to the (default-constructed) vector in the constructor body. So: `foo(IT begin, IT end) : data(begin, end) { /* static assert goes here */ }`. In general, use the initializer list for all members unless there's a really good reason to delay setting the right values. – Pete Becker Mar 19 '18 at 13:04
  • @PeterBecker i usually always do that, no idea why I didnt do it here. Actually it makes me nervous if a constructors body is anything different than `{}` – 463035818_is_not_an_ai Mar 19 '18 at 13:09

3 Answers3

3

In modern C++, you could have used std::is_same and static_assert:

static_assert(std::is_same_v<typename std::iterator_traits<IT>::value_type, double>,
     "wrong iterator");

See also std::iterator_traits: an iterator it is not guaranteed to have a value_type typedef, and one should use std::iterator_traits<it>::value_type instead.

In C++ 98, is_same is trivial to implement, static_assert needs a negative-size array trick or the BOOST_STATIC_ASSERT.

YSC
  • 38,212
  • 9
  • 96
  • 149
1

Here is a C++98 compliant way to implement it.....

First the fun part: Implementing a is_same is rather straightforward

template <typename T,typename U> struct is_same_type { static const bool value; };
template <typename T,typename U> const bool is_same_type<T,U>::value = false;
template <typename T> struct is_same_type<T,T> { static const bool value; };
template <typename T> const bool is_same_type<T,T>::value = true;

Now the not-so-fun part (C++11 really helps to statically assert without causing coworkers raising some eyebrows):

struct foo {
    std::vector<double> data;
    template <typename IT>
    foo(IT begin, IT end) : data(begin,end) {
        typedef int static_assert_valuetype_is_double[
             is_same_type<double,typename IT::value_type>::value ? 1 : -1
        ];
        std::cout << "constructor called \n";
    }

};

int main(){
    std::vector<double> x(5,2.3);
    foo f(x.begin(),x.end());
    for (std::vector<double>::iterator it = f.data.begin(); it != f.data.end();++it) std::cout << *it << " ";
    //std::vector<int> y(10,3);
    //foo g(y.begin(),y.end());  // THIS FAILS (AS EXPECTED)        
}

As pointed out by others, I should actually be using std::iterator_traits<IT>::value_type as not every iterator has a value_type. However, in my case I rather want to restrict the possible iterators to a small set and disallowing iterators without a value_type isnt a problem in my specific case.

Also note that the code in the question assigned to the member, while it is of course better to use the initializer list.

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
1

For a solution that works in C++98 and later.....

#include <iterator>

template<class T> struct TypeChecker
{};

template<> struct TypeChecker<double>
{
     typedef double valid_type;
};

template <typename IT>
    void foo(IT begin, IT end)
{
      typename TypeChecker<typename std::iterator_traits<IT>::value_type>::valid_type type_checker;
      (void)type_checker;

        // whatever
}

Instantiations of foo() will succeed for any for an iterator for which value_type is double, and fail to compile otherwise.

The premise is that TypeChecker<x> does not have a valid_type for any x other than double, but we attempt to instantiate an instance of that type in foo(). The (void)type_checker prevents warnings, from some compilers about a variable that is never used, for valid types.

Peter
  • 35,646
  • 4
  • 32
  • 74