4

I want to write a templatized function which takes either an array<int, 3> or an int[3]. I'm trying to capture that in an enable_if:

template<typename T>
enable_if_t<is_array_v<T> && extent_v<T> == 3U || !is_array_v<T> && tuple_size<T>::value == 3U> foo(const T& param) {}

Unfortunately for an int[3], tupple_size is not defined, which causes the template to fail to compile, before short circuiting is evaluated.

I have also tried to do this using a conditional but that has the same problem of ensuring both options are valid for T before considering the condition.

I know that I can do this by specializing. But the code is the exact same in the body of the function. I hate the fact that I'm specializing when the implementation is the same.

Is there a way I can force the short circuit before evaluating the conditions?

Jonathan Mee
  • 37,899
  • 23
  • 129
  • 288
  • Typos: `extent_v` and `const T& param`. You could of course use specialization to define a trait to control the SFINAE, which avoids repeating the function body. – aschepler Aug 29 '18 at 14:41
  • @aschepler Good call on the typos. I just typed it from memory rather than copying :( Always a bad plan. Your trait comment may be what I'm looking for. Could you elaborate? – Jonathan Mee Aug 29 '18 at 15:01
  • The `my_is_array` or `array_size_or_zero` pieces from bolov's answer are the sort of thing I meant by the "define a trait" part. – aschepler Aug 29 '18 at 22:52

3 Answers3

8

Taking advantage of the fact that extent<T> for non-array types is zero and hence falsy, and disjunction derives from the first truthy type in the list with short circuiting:

template<typename T>
enable_if_t<disjunction<extent<T>, tuple_size<T>>::value == 3U> foo(const T& param) {}

This is probably too clever. Note that you can't use disjunction_v here.


conditional should work just fine too. The trick is to not ask for ::value until you've picked the right type:

template<typename T>
enable_if_t<conditional_t<is_array_v<T>, extent<T>, tuple_size<T>>::value == 3U> 
    foo(const T& param) {}
T.C.
  • 133,968
  • 17
  • 288
  • 421
  • @aschepler if I upvote your comment can we count that as an extra vote for TC? – bolov Aug 29 '18 at 14:48
  • I had to read through [the Notes section of `disjunction`](https://en.cppreference.com/w/cpp/types/disjunction#Notes) to understand this. But it appears `disjunction` will continue to evaluate subsequent arguments till it finds one that does not cast to 0. And more importantly, *when it find one that doesn't cast to 0, it stops evaluating arguments.* – Jonathan Mee Aug 29 '18 at 14:59
  • Wow, so help me understand here then, `tuple_size` isn't valid... why doesn't the compiler care unless I use the `_v`? – Jonathan Mee Aug 29 '18 at 15:07
  • @JonathanMee As noted in the answer, this is the code you asked for but probably not the code you should write. The "write your own metafunction" answers are orders of magnitude easier to understand in the final code. – Max Langhof Aug 29 '18 at 15:09
  • 2
    @JonathanMee Just naming `tuple_size` is perfectly valid. What's not valid is asking for its member `value`. – T.C. Aug 29 '18 at 15:12
  • @MaxLanghof Hmmm... am I missing something? What advantages does the trait have over the `conditional` or `disjunction` here? – Jonathan Mee Aug 29 '18 at 15:12
  • @JonathanMee That you don't have to read through a notes section on a reference site to understand how/why it works. Also, giving the condition you are looking to use a name is as good as (or better than) finding some terse combination of library tools to express the same thing. However, you might combine the two: `template inline constexpr bool isArray3 = (conditional_t, extent, tuple_size>::value == 3U);` – Max Langhof Aug 29 '18 at 15:32
4

In short no, the template substitutions always have to be valid. It would probably be easier to just define a specific template to match the arrays:

template <typename T>
struct IsArrayInt3 { enum: bool { value = false }; };

template <>
struct IsArrayInt3<int[3]> { enum: bool { value = true }; };

template <>
struct IsArrayInt3<std::array<int, 3>> { enum: bool { value = true }; };
Karl
  • 161
  • 4
  • 4
    Simpler to inherit `std::false_type` or `std::true_type`, plus that matches what all the library traits do. – aschepler Aug 29 '18 at 14:55
3

I would suggest a alternate approach: 2 overloads (always prefer overloads to template specializations) that call a common function which contains the common code:

namespace detail
{
template <class T>
auto foo_impl(const T& a)
{
    // common code
}
}

template <class T>
auto foo(const std::array<T, 3>& a)
{
    detail::foo_impl(a);
}

template <class T>
auto foo(const T(&a)[3])
{
    detail::foo_impl(a);
}

This is clear, hassle-free and avoids code repetition.

An alternate is to create your own trait:

template <class T, std::size_t Size>
struct my_is_array : std::false_type
{};

template <class T, std::size_t Size>
struct my_is_array<std::array<T, Size>, Size> : std::true_type
{};

template <class T, std::size_t Size>
struct my_is_array<T[Size], Size> : std::true_type
{};

template<typename T>
std::enable_if_t<my_is_array<T, 3>::value> foo(const T& param) {}

or (I actually like this one better):

template <class T>
struct array_size_or_zero : std::integral_constant<std::size_t, 0>
{};

template <class T, std::size_t Size>
struct array_size_or_zero<std::array<T, Size>> : std::integral_constant<std::size_t, Size>
{};

template <class T, std::size_t Size>
struct array_size_or_zero<T[Size]> : std::integral_constant<std::size_t, Size>
{};

template<typename T>
std::enable_if_t<array_size_or_zero<T>::value == 3> foo(const T& param) {}

Careful!!: foo must have parameter by reference, otherwise the array decays to pointer.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
bolov
  • 72,283
  • 15
  • 145
  • 224