Search code examples
c++pointersoperator-keywordsfinaerandom-access

SFINAE - detect if type T is pointer, array or container with random access operator and for given value type


I am fighting with SFINAE trying to have many functions that requires just to have access to the type T with operator []. So far I have the following code that compiles and works fine with Visual Studio 2017:

#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <list>
#include <array>
#include <map>
#include <set>

using namespace std;

template <typename T,  typename X = std::enable_if_t <std::is_array<T>::value || std::is_pointer<T>::value> > 
void DoIt(T& c)
{}

template <typename T, typename std::enable_if_t< std::is_same<std::random_access_iterator_tag,
typename std::iterator_traits<typename T::iterator>::iterator_category>::value, bool> = true >
void DoIt(T& c)
{}


int main() 
{
    int* a; 
    const int* ac;
    int b[10];
    array<int,6> c;
    vector<int> d;
    string s;

    DoIt(a);   // Ok, compile pass
    DoIt(ac);  // Ok, compile pass
    DoIt(b);   // Ok, compile pass
    DoIt(c);   // Ok, compile pass
    DoIt(d);   // Ok, compile pass
    DoIt(s);   // Ok, compile pass

    int i;
    float f;
    map<int, int> m;

    //DoIt(f);  // Ok, compile fails
    //DoIt(i);  // Ok, compile fails
   // DoIt(m);  // Ok, compile fails
    return 0;
}

Now I need the following :

  1. How to combine both SFINAE conditions checking for array & pointer and random access operator into one check? I have many functions and it is not convenient and too much code to have two declarations. But I somehow failed to combine the conditions in a single std::enable_if_t or in a template structure.

  2. Is it possible above to be extended and to check also and for the container type, so that for example:

    vector<int> a;
    vector<string> b;
    int* c;
    string* d;
    DoIt(a);  // must pass 
    DoIt(c);  // must pass
    DoIt(b);  // must fail
    DoIt(d);  // must fail

Solution

  • How to combine both SFINAE conditions checking for array & pointer and random access operator into one check? I

    The simplest way that come in my mind is check if you can write c[0u]

    template <typename T>
    auto DoIt(T& c) -> decltype( c[0u], void() )
    {}
    

    Not a perfect solution: works with types accepting an unsigned integer for as argument for operator[] (std::vectors, std::arrays, C-style arrays, pointers, std::maps with an integer key) but fails with maps with keys incompatibles with unsigned integers.

    You can reduce this problem adding a template parameter for the key type (defaulting it to std::size_t)

    template <typename K = std::size_t, typename T>
    auto DoIt(T& c) -> decltype( c[std::declval<K>()], void() )
    {}
    

    so works as follows

    std::array<int,6> c;
    
    DoIt(c);   // Ok, compile pass, no needs to explicit the key type
    
    std::map<std::string, int> m;
    
    DoIt(m);   // compilation error: std::size_t is a wrong key type
    DoIt<std::string>(m);  // Ok, compile: std::string is a good key type
    

    If you want enable the function checking also the type returned by the operator []... well... conceptually is simple but require a little typewriting

    I propose the following DoIt2() function where you have to explicit the required type for operator [] and std::size_t remain the default type for the argument of the operator (but you can explicit a different type)

    template <typename V, typename K = std::size_t, typename T>
    std::enable_if_t<
       std::is_same_v<V,
          std::remove_const_t<
             std::remove_reference_t<
                decltype(std::declval<T>()[std::declval<K>()])>>>>
       DoIt2 (T &)
     {}
    

    The idea is simple: get the type of std::declval<T>()[std::declval<K>()], remove reference (if present), remove const (if present) and check if the resulting type is equal to V (the requested type)

    You can use DoIt2() as follows

    std::vector<int>   v1;
    std::vector<float> v2;
    
    DoIt2<int>(v1);    // compile
    //DoIt2<int>(v2);  // compilation error
    
    //DoIt2<float>(v1);  // compilation error
    DoIt2<float>(v2);    // compile
    
    std::map<int, std::string>    m1;
    std::map<std::string, float>  m2;
    
    DoIt2<std::string, int>(m1);
    DoIt2<float, std::string>(m2);