How to check for the existence of a subscript operator?
Asked Answered
S

3

12

I want to write a type trait which uses SFINAE to check a type for the existence of a subscript expression. My initial attempt below seems to work when the subscript expression is possible but does not work when the bracket operator does not exist.

#include <iostream>
#include <vector>
#include <cassert>

template<class T, class Index>
struct has_subscript_operator_impl
{
  template<class T1,
           class Reference = decltype(
             (*std::declval<T*>())[std::declval<Index>()]
           ),
           class = typename std::enable_if<
             !std::is_void<Reference>::value
           >::type>
  static std::true_type test(int);

  template<class>
  static std::false_type test(...);

  using type = decltype(test<T>(0));
};


template<class T, class Index>
using has_subscript_operator = typename has_subscript_operator_impl<T,Index>::type;

struct doesnt_have_it {};

struct returns_void
{
  void operator[](int) {}
};

struct returns_int
{
  int operator[](int) { return 0; }
};

int main()
{
  std::cout << "has_subscript_operator<doesnt_have_it,int>: " << has_subscript_operator<doesnt_have_it,int>::value << std::endl;
  assert((!has_subscript_operator<doesnt_have_it,int>::value));

  std::cout << "has_subscript_operator<returns_void,int>: " << has_subscript_operator<returns_void,int>::value << std::endl;
  assert((!has_subscript_operator<returns_void,int>::value));

  std::cout << "has_subscript_operator<returns_int,int>: " << has_subscript_operator<returns_int,int>::value << std::endl;
  assert((has_subscript_operator<returns_int,int>::value));

  std::cout << "has_subscript_operator<int*,int>: " << has_subscript_operator<int*,int>::value << std::endl;
  assert((has_subscript_operator<int*,int>::value));

  std::cout << "has_subscript_operator<std::vector<int>,int>: " << has_subscript_operator<std::vector<int>,int>::value << std::endl;
  assert((has_subscript_operator<returns_int,int>::value));

  return 0;
}

clang-3.4's output:

$ clang -std=c++11 -I. -lstdc++ test_has_subscript_operator.cpp 
test_has_subscript_operator.cpp:10:14: error: type 'doesnt_have_it' does not provide a subscript operator
             (*std::declval<T*>())[std::declval<Index>()]
             ^~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
test_has_subscript_operator.cpp:25:1: note: in instantiation of template class 'has_subscript_operator_impl<doesnt_have_it, int>' requested here
using has_subscript_operator = typename has_subscript_operator_impl<T,Index>::type;
^
test_has_subscript_operator.cpp:41:66: note: in instantiation of template type alias 'has_subscript_operator' requested here
  std::cout << "has_subscript_operator<doesnt_have_it,int>: " << has_subscript_operator<doesnt_have_it,int>::value << std::endl;
                                                                 ^
1 error generated.

How can I fix my has_subscript_operator such that it works correctly for all types?

Somatoplasm answered 9/7, 2015 at 0:0 Comment(0)
D
8

SFINAE only works when substitution failure happens in the immediate context. The template parameter Index is already known by the time the member function template test is being instantiated, so instead of substitution failure you get a hard error.

The trick to working around this is to deduce Index again by adding an additional template type parameter to test and default it to Index.

template<class T1,
       class IndexDeduced = Index,  // <--- here
       class Reference = decltype(
         (*std::declval<T*>())[std::declval<IndexDeduced>()] // and use that here
       ),
       class = typename std::enable_if<
         !std::is_void<Reference>::value
       >::type>
static std::true_type test(int);

Now your code works as intended.

Live demo

Drama answered 9/7, 2015 at 0:20 Comment(1)
Thanks. What I really wanted to know was why it was being reported as a hard error.Somatoplasm
F
4

Once you have C++11, it's a lot easier to write type traits... you don't need to use the ellipsis overload trick. You can just use your decltype expression directly with the help of the magical:

template <typename... >
using void_t = void;

We have our base case:

template<class T, class Index, typename = void>
struct has_subscript_operator : std::false_type { };

And our expression SFINAE valid case:

template<class T, class Index>
struct has_subscript_operator<T, Index, void_t<
    decltype(std::declval<T>()[std::declval<Index>()])
>> : std::true_type { };

And then you can write the same alias:

template <class T, class Index>
using has_subscript_operator_t = typename has_subscript_operator<T, Index>::type;

You can also use @Yakk's favorite method, which given this boilerplate that he copies in every answer:

namespace details {
  template<class...>struct voider{using type=void;};
  template<class...Ts>using void_t=typename voider<Ts...>::type;

  template<template<class...>class Z, class, class...Ts>
  struct can_apply:
    std::false_type
  {};
  template<template<class...>class Z, class...Ts>
  struct can_apply<Z, void_t<Z<Ts...>>, Ts...>:
    std::true_type
  {};
}
template<template<class...>class Z, class...Ts>
using can_apply=details::can_apply<Z,void,Ts...>;

You can then simply write properties:

template <class T, class Index>
using subscript_t = decltype(std::declval<T>()[std::declval<Index>()]);

template <class T, class Index>
using has_subscript = can_apply<subscript_t, T, Index>;
Frayda answered 9/7, 2015 at 0:34 Comment(16)
@Yakk you may have converted me to this approach.Frayda
I'm curious, why the extra level of indirection with voider? Why not just template<class ...> using void_t = void?Mask
@NirFriedman Some compilers won't compile the void_t alias correctly.Frayda
Interesting. Another fun thing can do is instead of bothering to define has_subscript, you can just go straight to defining a template boolean variable has_subscript_v as can_apply<...>::value.Mask
Heh, you copy @Yakk's boilerplate. I link std::experimental::is_detected.Innards
Sorry, not following why the template variable would not work?Mask
@Innards Potato, potahto :)Frayda
@Frayda I tried with a variable template, has_subscript_v, and it worked perfectly (requires 14, of course).Mask
@Frayda coliru.stacked-crooked.com/a/364e728aa09880d5. It'll be nice never having to type ::value or typename ::type again. (see also: open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3854.htm)Mask
@NirFriedman Oh obviously you can do that. I thought you meant without defining subscript_tFrayda
Ah, ok, I was quite surprised when you wrote that initially and I thought maybe there was some extremely subtle point that I was missing. It's good though, caused me to go through your code carefully and figured out why it needs to be structured that way.Mask
I actually type it out each time, because that forces me to tidy it up each time. The first 20 times you write a metaprogram it sucks.Barong
@NirFriedman Why would you want a compile time bool, when you could have a std::integral_constant<bool,?>? A type that is true is stronger than a value that is compile-time true in my experience.Barong
@Yakk Well ideally you want both. Compile time bool is necessary for enable if and conditional. Type is better for tagged dispatch. That's why they're proposing to have both in the standard.Mask
@nir but {} turns typed bool to compile time bool already, same cgaracter count as _v. Both conditional and enable if taking a compile time bool and not a type is convention. I guess horrible syntax of doing compile time calculations as types does suck.Barong
@Yakk I thought it was (). It's the same character count but _v is before <> instead of after. Check out the proposal. I don't know why enable if and conditional don't also take a type, if memory serves in boost they accepted both. But it's not likely changing, so _v is useful.Mask
S
3

With C++20 Concepts:

template<class T, class I = std::size_t>
concept indexible = requires (T& t, const I& i) {
  {t[i]};
};

Usage:

static_assert(not indexible<std::list<int>>);
static_assert(    indexible<std::string>);
static_assert(    indexible<std::vector<bool>>);

static_assert(not indexible<std::map<std::string, int>>);
static_assert(    indexible<std::map<std::string, int>, std::string>);
static_assert(not indexible<const std::map<std::string, int>, std::string>);

struct any {
  template<class T>
  operator const T&() const;
};

static_assert(    indexible<std::map<std::string, int>, any>);
static_assert(    indexible<std::map<char*, int>, any>);

struct Foo {
    int operator[](int i);
    int operator[](double i);
};

static_assert(not indexible<Foo, any>);

Live demo: https://godbolt.org/z/7GWK4PvM8

Note that the usage with any will fail if operator[] is overloaded with different parameter types.

Sweetsop answered 13/3, 2022 at 11:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.