0

How would one best implement a single function that accepts two std::array<int, [size]> arguments, each with a size constrained by a corresponding set of values known at compile-time?

  • The function must only accept arrays with sizes derived from a given set (enum/macro/etc)
  • The sets of allowable array "sizes" may be changed in the future and may be large (effectively precluding function overloading)
  • The function itself should remain fixed regardless of changes to the sets of allowable array "sizes"

The question "Passing a std::array of unknown size to a function", while similar, doesn't appear to directly apply.

The following works in C++14 but seems unnecessarily redundant & messy:

#include <type_traits>
#include <array>

// Add legal/allowable sizes for std::array<> "types" here
// Note: Not married to this; perhaps preprocessor instead?
enum class SizesForArrayX : size_t { Three = 3, Four, Forty = 40 };
enum class SizesForArrayY : size_t { Two = 2, Three, EleventyTwelve = 122 };

// Messy, compile-time, value getter for the above enum classes
template <typename S>
constexpr size_t GetSizeValue(const S size)
{ return static_cast<std::underlying_type_t<S>>(size); }

// An example of the function in question; is Template Argument Deduction
//  possible here?
// Note: only arrays of "legal"/"allowable" sizes should be passable
template <SizesForArrayX SX, SizesForArrayY SY>
void PickyArrayHandler(
    const std::array<int, GetSizeValue(SX)>& x,
    const std::array<int, GetSizeValue(SY)>& y)
{
    // Do whatever
    for (auto& i : x) i = 42;
    for (auto& i : y) while (i --> -41) i = i;
}

Calling the above:

int main()
{
    // Declare & (value-)initialize some arrays
    std::array<int, GetSizeValue(SizesForArrayX::Forty)> x{};
    std::array<int, GetSizeValue(SizesForArrayY::Two>) y{};

    //PickyArrayHandler(x, y); // <- Doesn't work; C2672, C2783

    // This works & handles arrays of any "allowable" size but the required
    //  template params are repetitions of the array declarations; ick
    PickyArrayHandler<SizesForArrayX::Forty, SizesForArrayY::Two>(x, y);
}

...which is ugly, inelegant, slow-to-compile, and requires the declared array size match the explicit "size" passed to the PickyArrayHandler function template.

  1. For the specific example above: Is there a way for the PickyArrayHandler template to deduce the sizes of the passed arrays?

  2. Generally speaking: Is there a different, better approach?

7 Answers7

2

Since you don't seem to be picky about how the valid sizes are defined, you can use type traits

#include <array>

template <size_t N> struct valid_size1 { enum { value = false }; };
template <size_t N> struct valid_size2 { enum { value = false }; };

template <> struct valid_size1<3> { enum { value = true }; };
template <> struct valid_size1<4> { enum { value = true }; };
template <> struct valid_size1<40> { enum { value = true }; };

template <> struct valid_size2<2> { enum { value = true }; };
template <> struct valid_size2<122> { enum { value = true }; };

template <size_t TX, size_t TY>
void PickyArrayHandler(const std::array<int, TX> &x,
                       const std::array<int, TY> &y)
{
  static_assert(valid_size1<TX>::value, "Size 1 is invalid");
  static_assert(valid_size2<TY>::value, "Size 2 is invalid");
    // Do whatever
}

int main()
{
    // Declare & (value-)initialize some arrays
    std::array<int, 40> x{};
    std::array<int, 2> y{};

    PickyArrayHandler(x, y);
    PickyArrayHandler(std::array<int, 4>{}, std::array<int, 2>{});
    // PickyArrayHandler(std::array<int, 1>{}, std::array<int, 5>{}); // BOOM!
}

Here's a solution using an array:

#include <iostream>
#include <array>

constexpr size_t valid_1[] = { 3, 4, 40 };
constexpr size_t valid_2[] = { 2, 122 };

template <size_t V, size_t I=0> 
struct is_valid1 { static constexpr bool value = V==valid_1[I] || is_valid1<V,I+1>::value; };

template <size_t V, size_t I=0> 
struct is_valid2 { static constexpr bool value = V==valid_2[I] || is_valid2<V,I+1>::value; };

template <size_t V>
struct is_valid1<V, sizeof(valid_1)/sizeof(valid_1[0])>
{static constexpr bool value = false; };

template <size_t V>
struct is_valid2<V, sizeof(valid_2)/sizeof(valid_2[0])>
{static constexpr bool value = false; };

template <size_t TX, size_t TY>
void PickyArrayHandler(const std::array<int, TX> &x,
                       const std::array<int, TY> &y)
{
  static_assert(is_valid1<TX>::value, "Size 1 is invalid");
  static_assert(is_valid2<TY>::value, "Size 2 is invalid");
    // Do whatever
}
agbinfo
  • 793
  • 5
  • 17
  • This works, albeit with unref'd local var warnings for 'isValid'. Having never dabbled with type traits, could you please explain in more detail what occurs at `{ typename valid_size1::type isValid; }`? – quasinormalized May 16 '18 at 16:11
  • @quasinormalized In order to instantiate the template, it needs to be able to create a variable of type valid_size1::type. If the type valid_size1::type exists, it succeeds. Otherwise it doesn't. The variables are not used so you get the warning. – agbinfo May 16 '18 at 16:40
  • this is elegant, i like it :) – skeller May 16 '18 at 17:00
  • @agbinfo Cool, thanks! I am currently toying with a hybrid of this answer & Stephen's to see if I can eliminate the compiler warnings. Do you have any further insight? – quasinormalized May 16 '18 at 18:58
  • Updated to remove warnings – agbinfo May 16 '18 at 19:55
  • Nice. Although I think you may have omitted `enum { value = false };` inside the initial struct template braces? – quasinormalized May 16 '18 at 20:30
  • Another question: is it just a convention to explicitly template the unused `N`? `template struct valid_size1 { enum { value = false }; };` seems to work just fine. What is this syntax called so I can research? – quasinormalized May 16 '18 at 20:36
  • The `value = false` is not needed but doesn't hurt - you may get better diagnostics but I didn't check. If it's omitted, the lack of a `value` identifier will make the template fail. As for the unused `N`it's not any convention that I'm aware of. – agbinfo May 16 '18 at 20:51
  • @agbinfo Without `value = false`, the compilation barfs with _'undeclared identifier'_ at the template instead of failing at the `static_assert()` as intended. If you wouldn't mind that small update, I would accept this answer as complete. :) – quasinormalized May 16 '18 at 21:09
  • Updated with `value = false` in default template – agbinfo May 16 '18 at 21:38
  • I've added a solution with C-style array. I think it's better. – agbinfo May 16 '18 at 23:16
0

twiddled around a bit and got this reduced one working: maybe it helps:

enum SizesForArrayX : size_t { Three = 3, Four, Forty = 40 };
enum SizesForArrayY : size_t { Two = 2, EleventyTwelve = 122 };

template <size_t TX, size_t TY>
void PickyArrayHandler(
    const std::array<int, TX>& x,
    const std::array<int, TY>& y)
{
    // Do whatever
}

int main()
{
    // Declare & (value-)initialize some arrays
    std::array<int, SizesForArrayX::Forty> x{};
    std::array<int, SizesForArrayY::Two> y{};

    PickyArrayHandler(x, y); 

    return 0;
}
skeller
  • 1,151
  • 6
  • 6
0

Unfortunately, your enums are not continuous so you cannot simply iterate over the enum and you have to handle all cases individually. Since the sizes are known at compile-time you can static_assert for it.

#include <array>

enum SizesForArrayX : size_t { Three = 3, Four, Forty = 40 };
enum SizesForArrayY : size_t { Two = 2, EleventyTwelve = 122 };

template <size_t TX, size_t TY>
void PickyArrayHandler(const std::array<int, TX> &x,
                       const std::array<int, TY> &y)
{
    static_assert(TX == Three || TX == Four || TX == Forty,
                  "Size mismatch for x");
    static_assert(TY == Two || TY == EleventyTwelve, "Size mismatch for y");
    // Do whatever
}

int main()
{
    // Declare & (value-)initialize some arrays
    std::array<int, SizesForArrayX::Forty> x{};
    std::array<int, SizesForArrayY::Two> y{};

    PickyArrayHandler(x, y);
    PickyArrayHandler(std::array<int, 4>{}, std::array<int, 2>{});
  //PickyArrayHandler(std::array<int, 1>{}, std::array<int, 5>{}); // BOOM!
}
Henri Menke
  • 10,705
  • 1
  • 24
  • 42
0

Personally I would just manually type the allowable sizes into a static_assert inside PickyArrayHandler. If that's not an option because the sizes will be used in other parts of your program and you're adhering to the DRY principal then I'd use the preprocessor.

#define FOREACH_ALLOWABLE_X(SEP_MACRO) \
    SEP_MACRO(3)    \
    SEP_MACRO(4)    \
    SEP_MACRO(40)   \

#define FOREACH_ALLOWABLE_Y(SEP_MACRO) \
    SEP_MACRO(2)    \
    SEP_MACRO(3)    \
    SEP_MACRO(122)  \


#define COMMA_SEP(NUM) NUM,
#define LOGIC_OR_SEP_X(NUM) N1 == NUM ||
#define LOGIC_OR_SEP_Y(NUM) N2 == NUM ||
#define END_LOGIC_OR false

// some arrays with your sizes incase you want to do runtime checking
namespace allowable_sizes
{
    size_t x[] {FOREACH_ALLOWABLE_X(COMMA_SEP)};
    size_t y[] {FOREACH_ALLOWABLE_Y(COMMA_SEP)};
}

template <size_t N1, size_t N2>
void PickyArrayHandler(const std::array<int, N1>& x, const std::array<int, N2>& y)
{
    static_assert(FOREACH_ALLOWABLE_X(LOGIC_OR_SEP_X) END_LOGIC_OR);
    static_assert(FOREACH_ALLOWABLE_Y(LOGIC_OR_SEP_Y) END_LOGIC_OR);

    // do whatever
}

#undef FOREACH_ALLOWABLE_X
#undef FOREACH_ALLOWABLE_Y
#undef COMMA_SEP
#undef LOGIC_OR_SEP_X
#undef LOGIC_OR_SEP_Y
#undef END_LOGIC_OR

Some C++ purists will frown at it but it gets the job done.

Fibbs
  • 1,350
  • 1
  • 13
  • 23
0

The best way I see to solve this problem is writing a custom type trait:

template <std::underlying_type_t<SizesForArrayX> SX>
struct is_size_x {
    static constexpr bool value = false;
};

template <>
struct is_size_x<static_cast<std::underlying_type_t<SizesForArrayX>>(SizesForArrayX::Forty)>{

    static constexpr bool value = true;
};

I'd put these right under the enum class declarations, just so it's easy to check that you got them all. Somebody more clever than I could probably figure out a way to even do this with variadic templates so you only need one specialization.

While tedious, if you have a small set of values this should be fast enough and easy to put in unit tests. The other nice thing about this approach is that if you have multiple functions that need one of these special sizes, you don't have to copy/paste static_asserts around.

With the type traits, your function becomes trivial:

template <std::size_t SX, std::size_t SY>
void PickyArrayHandler(
    std::array<int, SX>& x,
    std::array<int, SY>& y)
{
    static_assert(is_size_x<SX>::value, "Invalid size SX");
    static_assert(is_size_y<SY>::value, "Invalid size SY");
    // Do whatever
    for (auto& i : x) i = 42;
    for (auto& i : y) while (i --> -41) i = i;
}

Lastly, you can make a type alias to avoid creating invalid arrays in the first place:

template <typename T, SizesForArrayX SIZE>
using XArray =
    std::array<T, static_cast<std::underlying_type_t<SizesForArrayX>>(SIZE)>;

template <typename T, SizesForArrayY SIZE>
using YArray =
    std::array<T, static_cast<std::underlying_type_t<SizesForArrayY>>(SIZE)>;

That'll prevent you from declaring an array if it's not an approved size:

XArray<int, SizesForArrayX::Forty> x{};
YArray<int, SizesForArrayY::Two> y{};
Stephen Newell
  • 7,330
  • 1
  • 24
  • 28
  • This excellent answer deserves more than my sub-15-repuation, invisible up-vote can offer. Any **hints for eliminating the superfluous specializations** while I experiment with a hybrid of this & @agbinfo's answer? The enum classes were simply my 1st stab at the problem & need not persist as long as the array sizes are restricted. – quasinormalized May 16 '18 at 18:54
  • I'm not sure how to remove specializations, since that's one of the tricks behind type traits. If you have many values to deal with, I'd suggest writing a short script (python would work well) that can generate the appropriate header file with the `enum`s and specializations. If I come up with a better solution, I'll update this answer. – Stephen Newell May 16 '18 at 20:07
0

You could have a is_of_size-like template that check the size of the array, and then use it to disable the template if one of the sizes does not match, something like:

#include <array>
#include <type_traits>

// Forward template declaration without definition.
template <class T, T N, T... Sizes>
struct is_one_of;

// Specialization when there is a single value: Ends of the recursion,
// the size was not found, so we inherit from std::false_type.
template <class T, T N>
struct is_one_of<T, N>: public std::false_type {};

// Generic case definition: We inherit from std::integral_constant<bool, X>, where X
// is true if N == Size or if N is in Sizes... (via recursion).
template <class T, T N, T Size, T... Sizes>
struct is_one_of<T, N, Size, Sizes... >: 
    public std::integral_constant<
        bool, N == Size || is_one_of<T, N, Sizes... >::value> {};

// Alias variable template, for simpler usage.
template <class T, T N, T... Sizes>
constexpr bool is_one_of_v = is_one_of<T, N, Sizes... >::value;

template <std::size_t N1, std::size_t N2,
          std::enable_if_t<
                (is_one_of_v<std::size_t, N1, 3, 4, 40> 
                && is_one_of_v<std::size_t, N2, 2, 3, 122>), int> = 0>
void PickyArrayHandler(
    const std::array<int, N1>& x,
    const std::array<int, N2>& y)
{
}

Then you can simply:

PickyArrayHandler(std::array<int, 3>{}, std::array<int, 122>{}); // OK
PickyArrayHandler(std::array<int, 2>{}, std::array<int, 3>{}); // NOK

In C++17, you could (I think) replace is_one_of with:

template <auto N, auto... Sizes>
struct is_one_of;

...and automatically deduce std::size_t.


In C++20, you could use a concept to have clearer error messages ;)

Holt
  • 36,600
  • 7
  • 92
  • 139
  • Not sure if I am yet sufficiently conversant with templates to follow your code; but +1 for `NOK` which enters my lexicon today & forevermore. – quasinormalized May 16 '18 at 19:41
  • @quasinormalized I've added some information - Basically you have a template recursion that check if the first size you pass (`N`) is one of the of the other `Sizes`. I made a generic template, which is why there is a `T`, but you could have a simpler template only for `std::size_t`. – Holt May 16 '18 at 19:46
  • @quasinormalized The idea is quite simple to understand if you are familiar with recursion. The basic case is when `Sizes...` is empty (first specialization), in which case the result is false (`std::false_type`), otherwize, you check if the first size `Size` matches `N`, or you recursively instantiate the template with the remaining sizes `Sizes...`. – Holt May 16 '18 at 19:47
0

Using static_assert for invalid sizes is not a good solution because it doesn't play well with SFINAE; i.e., TMP facilities like std::is_invocable and the detection idiom will return false positives for calls that in fact always yield an error. Far better is to use SFINAE to remove invalid sizes from the overload set, resulting in something resembling the following:

template<std::size_t SX, std::size_t SY,
         typename = std::enable_if_t<IsValidArrayXSize<SX>{} && IsValidArrayYSize<SY>{}>>
void PickyArrayHandler(std::array<int, SX> const& x, std::array<int, SY> const& y) {
    // Do whatever
}

First we need to declare our valid sizes; I don't see any benefit to stronger typing here, so for a compile-time list of integers, std::integer_sequence works just fine and is very lightweight:

using SizesForArrayX = std::index_sequence<3, 4, 40>;
using SizesForArrayY = std::index_sequence<2, 3, 122>;

Now for the IsValidArraySize traits... The straightforward route is to make use of C++14's relaxed-constexpr rules and perform a simple linear search:

#include <initializer_list>

namespace detail {
    template<std::size_t... VSs>
    constexpr bool idx_seq_contains(std::index_sequence<VSs...>, std::size_t const s) {
        for (auto const vs : {VSs...}) {
            if (vs == s) {
                return true;
            }
        }
        return false;
    }
} // namespace detail

template<std::size_t S>
using IsValidArrayXSize
  = std::integral_constant<bool, detail::idx_seq_contains(SizesForArrayX{}, S)>;
template<std::size_t S>
using IsValidArrayYSize
  = std::integral_constant<bool, detail::idx_seq_contains(SizesForArrayY{}, S)>;

Online Demo

However if compile times are at all a concern, I suspect the following will be better, if potentially less clear:

namespace detail {
    template<bool... Bs>
    using bool_sequence = std::integer_sequence<bool, Bs...>;

    template<typename, std::size_t>
    struct idx_seq_contains;

    template<std::size_t... VSs, std::size_t S>
    struct idx_seq_contains<std::index_sequence<VSs...>, S>
      : std::integral_constant<bool, !std::is_same<bool_sequence<(VSs == S)...>,
                                                   bool_sequence<(VSs, false)...>>{}>
    { };
} // namespace detail

template<std::size_t S>
using IsValidArrayXSize = detail::idx_seq_contains<SizesForArrayX, S>;
template<std::size_t S>
using IsValidArrayYSize = detail::idx_seq_contains<SizesForArrayY, S>;

Online Demo

Whichever implementation route is chosen, using SFINAE in this way enables very nice error messages – e.g. for PickyArrayHandler(std::array<int, 5>{}, std::array<int, 3>{});, current Clang 7.0 ToT yields the following, telling you which array's size is invalid:

error: no matching function for call to 'PickyArrayHandler'
    PickyArrayHandler(std::array<int, 5>{}, std::array<int, 3>{});
    ^~~~~~~~~~~~~~~~~

note: candidate template ignored: requirement 'IsValidArrayXSize<5UL>{}' was not satisfied [with SX = 5, SY = 3]
    void PickyArrayHandler(std::array<int, SX> const& x, std::array<int, SY> const& y) {
         ^
ildjarn
  • 62,044
  • 9
  • 127
  • 211