1

I'm working with vector classes containing either integer or floating point types. I'd like to choose one or the other function template accordingly, but the only way to deduce the type is through the subscript operator [].

Is there a way to use enable_if<is_integral< ... on the return type of the [] operator of the template parameter? Something like:

template<class V, typename enable_if<is_integral<V::operator[]>::value, int>::type = 0>
int MyFunc(V vec)
{ return vec[0]; }

template<class V, typename enable_if<!is_integral<V::operator[]>::value, int>::type = 0>
int MyFunc(V vec)
{ return ceil(vec[0]); }
  • `decltype(declval()[0])` – apple apple Mar 18 '21 at 15:33
  • @appleapple, this solution is not reliable. It doesn't work for `std::vector`, for instance. See my answer for details. – Enlico Mar 18 '21 at 19:59
  • @Enlico true, well nothing is really reliable if you know nothing about the target type, `operator[]` may not even accept `int`. – apple apple Mar 19 '21 at 07:30
  • @appleapple, this is inaccurate. Relying on `operator[]` means you're making assumptions on an implementation detail. Relying on `begin` member function means you're assuming the `V` uses a common and well known API. After all, if you're sending it to a function that expects it to be a kind of "collection", it must provide a method to access the elements. So the assumption boils down to that function being named `begin` and not `beginner` or anything else. – Enlico Mar 19 '21 at 07:46
  • @Enlico yes, it always need assumption, that's my point. – apple apple Mar 19 '21 at 14:45

2 Answers2

3

Some general suggestion

I would avoid using namespace std which your code implies (almost) that you use.

You can shorten and make more readable std::enable_if<T>::type and std::is_integral<T>::value by writing them as std::enable_if_t<T> and std::is_integral_v<T> respectively (search for "Helper types" here, for instance).

The answer to your question

You need to apply operator[] to something

What you really want to ask is if the items in the object of type V can be accessed via [], so you want to check whether something like V{}[0] is valid, so you would write something like this (as to why std::decay_t is needed, see note (¹)):

template<class V, typename std::enable_if_t<std::is_integral_v<std::decay_t<decltype(V{}[0])>>, int> = false>

But V might be not default constructible

If you want to support types V without assuming that they are default constructible you can use std::declval to "pretend" you create a value of that type (here std::decay_t is needed for the same reason as above):

template<class V, typename std::enable_if_t<std::is_integral_v<std::decay_t<decltype(std::declval<V>()[0])>>, int> = false>

And operator[] could return some proxy to the actual entities in V

In general operator[] doesn't return an object of the type which is "conceptually" the type stored in the container; for instance, for std::vector<bool>, operator[] doesn't return a bool&, but a proxy type²; in those cases, you really want to rely on the value_type type alias stored in the container:

template<class V, typename std::enable_if_t<std::is_integral_v<typename V::value_type>, int> = false>

Something might be not value_type-equipped but only provide .begin()/.end()

As a last improvement, we can also remove the assumption that the V has a value_type member alias (which is true for STL container), and simply require that it has a begin member function (STL container have it too), and make use of ranges::iter_value_t³:

template<class V, typename std::enable_if_t<std::is_integral_v<ranges::iter_value_t<decltype(std::declval<V>().begin())>>, int> = 0>

Notice that

ranges::iter_value_t<decltype(std::declval<V>().begin())>

is in general not the same same thing as

std::decay_t<decltype(*std::declval<V>().begin())>

a case of when they are different being, again, std::vector<bool>.


¹ std::vector<int>::operator[], for instance, returns int&, not int, and int& is not integral:

static_assert(std::is_integral_v<int&>); // fails

² For instance:

// this passes
static_assert(std::is_same_v<int, std::decay_t<decltype(std::declval<std::vector<int>>()[0])>>);
// this doesn't!
static_assert(std::is_same_v<bool, std::decay_t<decltype(std::declval<std::vector<bool>>()[0])>>);
// with my compiler, this one does pass
static_assert(std::is_same_v<std::_Bit_reference, std::decay_t<decltype(std::declval<std::vector<bool>>()[0])>>);

³ ranges::iter_value_t is from the header #include <range/v3/iterator/access.hpp> of the Range-v3 library. The documentation of that library is close to crap, so you can refer the doc of the analagous C++20 functionality on cppreference.

Enlico
  • 23,259
  • 6
  • 48
  • 102
0

Why are you not using std::vector::value_type?

template<class V, typename enable_if<is_integral<typename V::value_type>::value, int>::type = 0> 
int MyFunc(V vec)
{ return vec[0]; }

template<class V, typename enable_if<!is_integral<typename V::value_type>::value, int>::type = 0> 
int MyFunc(V vec)
{ return ceil(vec[0]); }
Enlico
  • 23,259
  • 6
  • 48
  • 102
rioki
  • 5,988
  • 5
  • 32
  • 55