8

I would like a type trait to get the element type of either a std::array or a plain old C-style array, eg. it should return char when provided with either std::array<char, 3> or char[3].

The mechanisms to do this appear to be only partially in place... I can use ::value_type on the std::array, and std::remove_all_extents on the plain array, but I can't find a single type trait that combines both and I'm unable to write one myself.

I've got as far as this:

#include <array>
#include <type_traits>

template <class T>
using element_type =  typename std::conditional<
    std::is_array<T>::value,
    typename std::remove_all_extents<T>::type,
    typename T::value_type
>::type;

It works just fine for std::array, of course:

int main()
{
    static_assert(
        std::is_same<char, element_type<std::array<char, 3>>>::value,
        "element_type failed");
}

but breaks when I pass it a plain array, because obviously plain arrays don't have a ::value_type.

static_assert(std::is_same<char, element_type<char[3]>>::value, "element_type failed");

just gives errors like "'T': must be a class or namespace when followed by '::'", as you'd expect.

If I were writing a function, I'd use std::enable_if to hide the offending template instantiation, but I don't see how this approach can be used in a type trait.

What is the correct way to solve this problem?

Rook
  • 5,734
  • 3
  • 34
  • 43

3 Answers3

11

For a very generic solution which supports any type of container/array supported by std::begin:

template<typename T>
using element_type_t = std::remove_reference_t<decltype(*std::begin(std::declval<T&>()))>;

std::begin as you may know returns an iterator. Dereferencing it gives you the value of it, which you can get the type of using decltype. The std::remove_reference_t is necessary because iterators return references to the element they are pointing at. As a result, this works for every single type for which std::begin has an overload.

Rakete1111
  • 47,013
  • 16
  • 123
  • 162
  • 1
    I think you take the prize on this one, for that clever use of `std::begin`. – Rook Jun 13 '17 at 13:39
  • You can improve this a little bit. There are some corner cases where the range-based for finds `begin/end` functions but this alias template won't. Define a function `auto begin_adl_helper(T && t){ using namespace std; return begin(t);}` and change your alias to use this function. Then types which overload `begin` in a namespace other than std will work too. – SirGuy Jun 13 '17 at 13:40
  • @SirGuy Really? I always thought that ranged for uses begin and end internally. Can you give an example? – Rakete1111 Jun 13 '17 at 13:43
  • @Rakete1111 I hit enter too soon on my comment, I had intended to explain myself in the same comment :P. The difference is that range for loops use ADL when checking for a free function called `begin` where as your alias does not use ADL. However, it seems that range-based for loops have a special handling of C-style arrays that allow them to work because `begin(c_array)` is not found via ADL. – SirGuy Jun 13 '17 at 13:49
  • @SirGuy Yes, true. `begin(c_array)` jus calls `std::begin`, nothing special. But does ADL really matter here? I mean, `std::begin` calls the member function `begin` if it exists. You don't need ADL like you would for say `std::swap`, which does not call any member function if it is not specialized. – Rakete1111 Jun 13 '17 at 13:58
  • Does ADL really matter here? In practise I doubt it, 99.9% of the time somebody creating a custom range will add `begin/end` functions inside their class definition. My point was that since you could make a change to make this type trait work in all cases that the range-based for works, I felt that you might as well. Especially since you don't know who's going to come along and read this answer with their own crazy constraints getting in the way of a simpler solution. – SirGuy Jun 13 '17 at 14:14
  • @SirGuy It already works for every type that has begin/end functions. There is nothing to change. Let's say `Foo` has those 2 functions. Calling `std::begin(Foo{})` will call the custom member functions, because the only valid overload is the one taking a type and calling `begin` on it. – Rakete1111 Jun 13 '17 at 14:34
7

What are the functions/operators you can call on both std::array and C-style arrays? operator[] of course:

template <class Array>
using array_value_type = decay_t<decltype(std::declval<Array&>()[0])>;

This will work for anything that supports looking up by integer, including std::vector, std::map<std::size_t, T>, etc.

If you want to distinguish between what you get from a const array vs a non-cost array you might want to create 2 type traits named something along the lines of:

template <class Array>
using array_element_t = decay_t<decltype(std::declval<Array>()[0])>;

template <class Array>
using array_value_t   = remove_reference_t<decltype(std::declval<Array>()[0])>;

The second trait here preserves the constness of the Array type passed in while the first one strips it. There are certainly use cases for both of these.

SirGuy
  • 10,660
  • 2
  • 36
  • 66
  • I very much like this approach, but I had to wrap it in a `std::remove_reference` in order to get something that I could work with (eg. pass the `static_assert` tests in the original question) – Rook Jun 13 '17 at 13:18
  • My previous comment was wrong: For containers with `const` elements, `std::decay` will strip the const out. `std::remove_reference_t` should be used instead, as it does not remove any cv-qualifiers. – Rakete1111 Jun 13 '17 at 13:59
  • @Rakete1111 That depends on whether the user wants a modifiable copy or wants to create a reference to it (where `decay_t` would then cause an error) – SirGuy Jun 13 '17 at 14:17
4

A way to do this is to dispatch to specialized templates

template<typename>
struct arr_trait;

template<typename T, size_t N>
struct arr_trait<T[N]> {using type = T;};

template<typename T, size_t N>
struct arr_trait<std::array<T, N>> {using type = T;};

template<typename T>
struct arr_trait<T&> : arr_trait<T> {};

template<typename T>
struct arr_trait<T&&> : arr_trait<T> {};

template<typename T>
using element_type = typename arr_trait<T>::type;

Live

The reason std::conditional is failing is because it doesn't support (and neither can it as far as I know) support short-circuiting, and both types will be evaluated.

Passer By
  • 19,325
  • 6
  • 49
  • 96
  • As it happens, I was just reading https://stackoverflow.com/a/22713242/1450890 and coming to the conclusion that I'd have to use the very approach you'd just suggested. Is it true to say that _no_ type trait can do the sort of short-circuiting I wanted? – Rook Jun 13 '17 at 13:02
  • @Rook As far as I know, nope – Passer By Jun 13 '17 at 13:03
  • Also, thanks for reminding me of the `foo` syntax, which was a much better way to handle plain array template parameters than the ones I was messing with. Silly, because I've been reading the docs for using `unique_ptr` on plain arrays, which uses exactly that. – Rook Jun 13 '17 at 13:05