Home > Net >  C template to check if input type implements `operator []`
C template to check if input type implements `operator []`

Time:01-10

I'm trying to use template to check if input type implements operator[]. Here is my code:

#include <iostream>
#include <vector>
using namespace std;

template <typename T, typename U = void>
struct has_bracket
{
    static constexpr int value = 0;
};

template <typename T>
struct has_bracket<T, decltype(T::operator[])>
{
    static constexpr int value = 1;
};

But it didn't work. It always output 0 no matter which type I input.

struct Test 
{
    int operator[](size_t n) { return 0; }
};

struct CTest
{
    int operator[](size_t n) const { return 0; }
};


int main()
{
    cout << has_bracket<int>::value << endl;             // output: 0
    cout << has_bracket<double>::value << endl;          // output: 0
    cout << has_bracket<Test>::value << endl;            // output: 0
    cout << has_bracket<CTest>::value << endl;           // output: 0
    cout << has_bracket<vector<int>>::value << endl;     // output: 0

    return 0;
}

I think that if T = int or T = double, decltype(&T::operator[]) will fail and the primary has_bracket will be used according to SFINAE. If T = Test or T = CTest or T = vector<int>, the specialization one will be instantiated, leads to the has_bracket<T>::value be 1.
Is there something wrong? How to fix this problem to let has_bracket<T> be 1 for T = Test, CTest and vector<int>?

CodePudding user response:

Thats not how SFINAE works. has_bracket<int> does not explicitly specify second template argument, default is void, hence it is has_bracket<int,void>.

decltype(T::operator[]) is never void. Hence, you always get the primary template. Moreover decltype(T::operator[]) would require operator[] to be a static member.

SFINAE works when the specialisation has void as second argument for the "true" case, because thats the default of the primary template. std::void_t can be handy to have a type that is either void or a substitution failure:

template <typename T, typename U = void>
struct has_bracket
{
    static constexpr int value = 0;
};

template <typename T>
struct has_bracket<T, std::void_t<decltype(std::declval<T>()[std::size_t{1}])> >
{
    static constexpr int value = 1;
};

Complete Demo

If you are resitricted to < C 17, ie you cannot use std::void_t you can replace it with a handwritten (taken from cppreference):

template< typename... Ts >
struct make_void { typedef void type; };
  
template< typename... Ts >
using void_t = typename make_void<Ts...>::type;

Summarizing from comments:

  • The main issue with your code is that the second argument of the specialization is not void, hence it is never choosen.
  • Your code uses decltype(T::operator[]) and irrespective of the first bullet, this requires the operator to be static (see here)
  • The issue with decltype(&T::operator[]) is that you cannot take the address when there is more than one overload (see here)
  • The previous solution in this answer uses decltype(std::declval<T>()[0]) which requires that operator[] can be called with 0 as argument. That this "works" with std::map<std::string,int>::operator[] is an unfortunate coincidence, because a null pointer can be converted to std::string. It does not work when operator[] takes an argument that cannot be constructed from 0 (see here).
  • For this reason I changed the code above to decltype(std::declval<T>()[std::size_t{1}]). The literal 1 does not have the problem of implicit conversion to a null pointer. The solution now only detects operator[] that can be called with an integer.
  • Related