0

This question is a follow on from my previous question Multi patterned varadic templates in C++ to which I received the solution:

#include <array>
#include <iostream>
#include <type_traits>

template <typename T, std::size_t N>
class Vec;

template <std::size_t, typename ...>
struct dimVec;

// ground case for no Vecs: unimplemented for SFINAE failure !
template <>
struct dimVec<0U>;

// ground case with one or more Vecs: size fixed
template <std::size_t N>
struct dimVec<N> : public std::integral_constant<std::size_t, N>
 { };

// first Vec: size detected
template <std::size_t N, typename T, typename ... Ts>
struct dimVec<0U, Vec<T, N>, Ts...> : public dimVec<N, Ts...>
 { };

// another Vec of same size: continue
template <std::size_t N, typename T, typename ... Ts>
struct dimVec<N, Vec<T, N>, Ts...> : public dimVec<N, Ts...>
 { };

// another Vec of different size: unimplemented for SFINAE failure !
template <std::size_t N1, std::size_t N2, typename T, typename ... Ts>
struct dimVec<N1, Vec<T, N2>, Ts...>;

// a not-Vec type: continue
template <std::size_t N, typename T, typename ... Ts>
struct dimVec<N, T, Ts...> : public dimVec<N, Ts...>
 { };

template <typename ... Args>
static constexpr auto dimVecV { dimVec<0U, Args...>::value };

template <std::size_t I, typename T, std::size_t N>
constexpr auto extrV (Vec<T, N> const & v)
 { return v[I]; }

template <std::size_t I, typename T>
constexpr auto extrV (T const & v)
 { return v; }

template <typename T, std::size_t N>
class Vec
 {
   private:
      std::array<T, N> d;

   public:
      template <typename ... Ts>
      Vec (Ts ... ts) : d{{ ts... }}
       { }

      T & operator[] (int i)
       { return d[i]; }

      T const & operator[] (int i) const
       { return d[i]; }
 };


template <std::size_t I, typename F, typename ... Args>
auto applyH2 (F && f, Args ... as)
 { return f(extrV<I>(as)...); }

template <std::size_t ... Is, typename F, typename ... Args>
auto applyH1 (std::index_sequence<Is...> const &, F && f, Args ... as)
   -> Vec<decltype(applyH2<0U>(f, as...)), sizeof...(Is)>
 { return { applyH2<Is>(f, as...)... }; }

template <typename F, typename ... Args, std::size_t N = dimVecV<Args...>>
auto apply (F && f, Args ... as)
 { return applyH1(std::make_index_sequence<N>{}, f, as...); }

long foo (int a, int b)
 { return a + b + 42; }

int main ()
 {
   Vec<int, 3U>  v3;
   Vec<int, 2U>  v2;

   auto r1 { apply(foo, v2, v2) };
   auto r2 { apply(foo, v3, v3) };
   auto r3 { apply(foo, v3, 0)  };

   static_assert( std::is_same<decltype(r1), Vec<long, 2U>>{}, "!" );
   static_assert( std::is_same<decltype(r2), Vec<long, 3U>>{}, "!" );
   static_assert( std::is_same<decltype(r3), Vec<long, 3U>>{}, "!" );

   // apply(foo, v2, v3); // compilation error
   // apply(foo, 1, 2);   // compilation error

 }

now lets say I also have a template class template <typename T, size_t N, size_t M> class mat; which represents a NxM matrix with the previous Vec's being equivalent to columns (M) for broadcasting, now I want to extend apply so it can also work with the Mat class with out breaking handling of just vec's.

Rules:

  • if the argument list contains a NxM matrix then all matrices NxM and all vectors must be M, and the function will return a NxM matrix
  • otherwise rules as previously for vectors, if the argument list contains a N vector then all vectors must be N, and the function will return a N vector

As far as I can tell since the function f() isn't implemented for matrices then the applyH2() should fail (if not then it needs to), in its current form, thus apply would fail, hence it should be possible to implement another apply() which accepts multiple sizes M and N in the same way, which will be used if the argument list contains a matrix, and then invoke extrV<I,J>(as) which for vectors should be mapped as extrV<I,J>(as) -> extrV<J>(as) this part seams pretty straight forward as dose implementing dimMatM and dimMatN in the same way as dimVec and such that dimMatM maps to dimVec for vectors.

However I'm not sure how to go about getting the two versions on apply() to work together with out error.

Optional: This part isn't really important by lets say that later I have a class tensor<T, N...> with extends the concept on vec and mat to N dimensions, would it be possible to extend apply to work such that broadcasting occurs over the leading dimensions i.e.

double f(float x, float y);
int main() {
   tensor<float, 2, 4, 6> f246;
   tensor<int, 4, 6> i46;
   tensor<float, 2, 4> f24;

   auto r1 = apply(f, f246, i46); // returns tensor<double, 2, 4, 6>
   // apply(f, f24, f246); // Compile ERROR: dimensions don't match
}

EDIT:

I just thought up a possible solution or at least part of it see below

template <size_t ...N>
struct tensor_helper {
template <size_t I, typename T, size_t ...M> static tensorGet(const tensor<T, M...>& V) { return V.v[I % product<M...>::value]; }
template <size_t I, typename T> static tensorGet(const T& V) { return V; }
template <std::size_t I, typename F, typename ...Args>
static auto applyH2 (F && f, Args ...as) -> decltype(f(std::declval<typename base_type<Args>::type>()...))
 { return f(tensorGet<I>(as)...); }
template <size_t ... Is, typename F, typename ... Args>
static auto applyH1(std::index_sequence<Is...> const &..., F && f, Args ... as) -> tensor<decltype(f(std::declval<typename base_type<Args>::type>()...)), N...>
    { return make_tensor<decltype(f(std::declval<typename base_type<Args>::type>()...)), N...>({applyH2<Is>(f, as...)... }); }
}

template <typename F, typename ...Args, size_t ...N = tensorSize<Args...> /* How do I get a list here? */>
auto apply (F && f, Args ... as)
 { return tensor_helper<N...>::applyH1(std::make_index_sequence<product<N...>>{}..., f, as...); }

the only problem is how to get tensorSize() to provide a list of dims, all though I assume this could be manually expanded if just implemented for vectors and matrices, rather than using the parameter packs for dims.

EDIT 2: I belive I can use constexpr array to variadic template, to solve the N list problem, from my above code. But I'm too tried too actualy try implementing that concept tonight will have a try tomorrow.

max66
  • 65,235
  • 10
  • 71
  • 111
Glen Fletcher
  • 644
  • 5
  • 21
  • 2
    Can you show us what you tried, and how it failed? No offense meant, but it sounds like you are just trying to get us to write this for you. –  May 14 '18 at 02:06
  • @Frank, I have no problem extending the helpers to matrices I explained how I'd go about that I my question, my problem is say I have two version of the apply function the original that only takes vectors and scalar and returns a vector, and a new one that takes matrices, vectors and scalars and returns a matrix. I don't know how to get the compiler to select the correct version, i.e. it must chose the one that matches the highest dimension-ed argument, or if an argument is a matrix chose the matrix version otherwise the vector version. – Glen Fletcher May 14 '18 at 06:50
  • As far as I can see their call signature would be the same one simply accepts an extra dimension, and I lack the familiarly with template meta programing to make sure the compiler chooses the correct version. – Glen Fletcher May 14 '18 at 06:52

2 Answers2

2

Conceptually is very similar but, with two dimensions, become a nightmare.

Instead of dimVec, I propose a dimMat with two dimensions (N and M) and SFINAE failure for unacceptable cases (different sizes vecs or mats, vect with size incompatible with mat); not sure that all case are managed but should be something as

template <std::size_t, std::size_t, typename ...>
struct dimMat;

// ground case for no Vecs and no Mats: unimplemented for SFINAE failure
template <>
struct dimMat<0U, 0U>;

// ground case with one or more Vecs and/or Mats: sizes fixed
template <std::size_t N, std::size_t M>
struct dimMat<N, M>
 { 
   static constexpr std::size_t valN { N };
   static constexpr std::size_t valM { M };
 };

// first Vec: M detected
template <std::size_t M, typename T, typename ... Ts>
struct dimMat<0U, 0U, Vec<T, M>, Ts...> : public dimMat<0U, M, Ts...>
 { };

// first Mat: N and M detected
template <std::size_t N, std::size_t M, typename T, typename ... Ts>
struct dimMat<0U, 0U, Mat<T, N, M>, Ts...> : public dimMat<N, M, Ts...>
 { };

// first Mat after a correct Vect: N detected
template <std::size_t N, std::size_t M, typename T, typename ... Ts>
struct dimMat<0U, M, Mat<T, N, M>, Ts...> : public dimMat<N, M, Ts...>
 { };

// another Vec of correct size: continue
template <std::size_t N, std::size_t M, typename T, typename ... Ts>
struct dimMat<N, M, Vec<T, M>, Ts...> : public dimMat<N, M, Ts...>
 { };

// another Mat of correct sizes: continue
template <std::size_t N, std::size_t M, typename T, typename ... Ts>
struct dimMat<N, M, Mat<T, N, M>, Ts...> : public dimMat<N, M, Ts...>
 { };

// another Vec of different size: unimplemented for SFINAE failure
template <std::size_t N, std::size_t M1, std::size_t M2, typename T,
          typename ... Ts>
struct dimMat<N, M1, Vec<T, M2>, Ts...>;

// another Mat of different sizes: unimplemented for SFINAE failure
template <std::size_t N1, std::size_t N2, std::size_t M1, std::size_t M2,
          typename T, typename ... Ts>
struct dimMat<N1, M1, Mat<T, N2, M2>, Ts...>;

// a not-Vec, not-Mat type: continue
template <std::size_t N, std::size_t M, typename T, typename ... Ts>
struct dimMat<N, M, T, Ts...> : public dimMat<N, M, Ts...>
 { };

The helper template variables are simple

template <typename ... Args>
static constexpr auto dimMatN { dimMat<0U, 0U, Args...>::valN };

template <typename ... Args>
static constexpr auto dimMatM { dimMat<0U, 0U, Args...>::valM };

For apply(), renamed applyM(), I suppose something as

template <typename F, typename ... Args, std::size_t N = dimMatN<Args...>,
          std::size_t M = dimMatM<Args...>>
auto applyM (F && f, Args ... as)
 { return applyMH1(std::make_index_sequence<N>{},
                   std::make_index_sequence<M>{}, 
                   f, as...); }

For applyMH1() two cases: if N is zero, so the first argument is an empty index list, we have the vec case

// Vec case: the first index list is empty: call applyH2()
template <std::size_t ... Js, typename F, typename ... Args>
auto applyMH1 (std::index_sequence<> const &,
               std::index_sequence<Js...> const &, F && f, Args ... as)
   -> Vec<decltype(applyH2<0U>(f, as...)), sizeof...(Js)>
 { return { applyH2<Js>(f, as...)... }; }

otherwise the mat case

template <std::size_t ... Is, std::size_t ... Js, typename F,
          typename ... Args>
auto applyMH1 (std::index_sequence<Is...> const &,
               std::index_sequence<Js...> const & js, F && f, Args ... as)
   -> Mat<decltype(applyMH3<0U, 0U>(f, as...)), sizeof...(Is), sizeof...(Js)>
 { return {{{ applyMH2<Is>(js, f, as...) ... }}}; }

Now applyMH2()

template <std::size_t I, std::size_t ... Js, typename F, typename ... Args>
auto applyMH2 (std::index_sequence<Js...> const &, F && f, Args ... as)
   -> std::array<decltype(applyMH3<0U, 0U>(f, as...)), sizeof...(Js)>
 { return {{ applyMH3<I, Js>(f, as...)... }}; }

and applyMH3()

template <std::size_t I, std::size_t J, typename F, typename ... Args>
auto applyMH3 (F && f, Args ... as)
 { return f(extrM<I, J>(as)...); }

given a extrM() based on extrV()

template <std::size_t I, std::size_t J, typename T,
          std::size_t N, std::size_t M>
constexpr auto extrM (Mat<T, N, M> const & v)
 { return v[I][J]; }

template <std::size_t I, std::size_t J, typename T>
constexpr auto extrM (T const & v)
 { return extrV<J>(v); }

The following is a full compiling example

#include <array>
#include <iostream>
#include <type_traits>

template <typename T, std::size_t N>
class Vec;

template <typename T, std::size_t N, std::size_t M>
struct Mat;

template <std::size_t, std::size_t, typename ...>
struct dimMat;

// ground case for no Vecs and no Mats: unimplemented for SFINAE failure
template <>
struct dimMat<0U, 0U>;

// ground case with one or more Vecs and/or Mats: sizes fixed
template <std::size_t N, std::size_t M>
struct dimMat<N, M>
 { 
   static constexpr std::size_t valN { N };
   static constexpr std::size_t valM { M };
 };

// first Vec: M detected
template <std::size_t M, typename T, typename ... Ts>
struct dimMat<0U, 0U, Vec<T, M>, Ts...> : public dimMat<0U, M, Ts...>
 { };

// first Mat: N and M detected
template <std::size_t N, std::size_t M, typename T, typename ... Ts>
struct dimMat<0U, 0U, Mat<T, N, M>, Ts...> : public dimMat<N, M, Ts...>
 { };

// first Mat after a correct Vect: N detected
template <std::size_t N, std::size_t M, typename T, typename ... Ts>
struct dimMat<0U, M, Mat<T, N, M>, Ts...> : public dimMat<N, M, Ts...>
 { };

// another Vec of correct size: continue
template <std::size_t N, std::size_t M, typename T, typename ... Ts>
struct dimMat<N, M, Vec<T, M>, Ts...> : public dimMat<N, M, Ts...>
 { };

// another Mat of correct sizes: continue
template <std::size_t N, std::size_t M, typename T, typename ... Ts>
struct dimMat<N, M, Mat<T, N, M>, Ts...> : public dimMat<N, M, Ts...>
 { };

// another Vec of different size: unimplemented for SFINAE failure
template <std::size_t N, std::size_t M1, std::size_t M2, typename T,
          typename ... Ts>
struct dimMat<N, M1, Vec<T, M2>, Ts...>;

// another Mat of different sizes: unimplemented for SFINAE failure
template <std::size_t N1, std::size_t N2, std::size_t M1, std::size_t M2,
          typename T, typename ... Ts>
struct dimMat<N1, M1, Mat<T, N2, M2>, Ts...>;

// a not-Vec, not-Mat type: continue
template <std::size_t N, std::size_t M, typename T, typename ... Ts>
struct dimMat<N, M, T, Ts...> : public dimMat<N, M, Ts...>
 { };

template <typename ... Args>
static constexpr auto dimMatN { dimMat<0U, 0U, Args...>::valN };

template <typename ... Args>
static constexpr auto dimMatM { dimMat<0U, 0U, Args...>::valM };

template <std::size_t, typename ...>
struct dimVec;

// ground case for no Vecs: unimplemented !
template <>
struct dimVec<0U>;

// ground case with one or more Vecs: size fixed
template <std::size_t N>
struct dimVec<N> : public std::integral_constant<std::size_t, N>
 { };

// first Vec: size detected
template <std::size_t N, typename T, typename ... Ts>
struct dimVec<0U, Vec<T, N>, Ts...> : public dimVec<N, Ts...>
 { };

// another Vec of same size: continue
template <std::size_t N, typename T, typename ... Ts>
struct dimVec<N, Vec<T, N>, Ts...> : public dimVec<N, Ts...>
 { };

// another Vec of different size: unimplemented !
template <std::size_t N1, std::size_t N2, typename T, typename ... Ts>
struct dimVec<N1, Vec<T, N2>, Ts...>;

// a not-Vec type: continue
template <std::size_t N, typename T, typename ... Ts>
struct dimVec<N, T, Ts...> : public dimVec<N, Ts...>
 { };

template <typename ... Args>
static constexpr auto dimVecV { dimVec<0U, Args...>::value };

template <std::size_t I, typename T, std::size_t N>
constexpr auto extrV (Vec<T, N> const & v)
 { return v[I]; }

template <std::size_t I, typename T>
constexpr auto extrV (T const & v)
 { return v; }

template <std::size_t I, typename T, std::size_t N, std::size_t M>
constexpr auto extrV (Mat<T, N, M> const & v)
 { return 0; }

template <std::size_t I, std::size_t J, typename T,
          std::size_t N, std::size_t M>
constexpr auto extrM (Mat<T, N, M> const & v)
 { return v[I][J]; }

template <std::size_t I, std::size_t J, typename T>
constexpr auto extrM (T const & v)
 { return extrV<J>(v); }


template <typename T, std::size_t N>
class Vec
 {
   private:
      std::array<T, N> d;

   public:
      template <typename ... Ts>
      Vec (Ts ... ts) : d{{ ts... }}
       { }

      T & operator[] (int i)
       { return d[i]; }

      T const & operator[] (int i) const
       { return d[i]; }
 };

template <typename T, std::size_t N, std::size_t M>
struct Mat
 {
   std::array<std::array<T, M>, N> m;

   auto & operator[] (int i)
    { return m[i]; }

   auto const & operator[] (int i) const
    { return m[i]; }
 };


template <std::size_t I, typename F, typename ... Args>
auto applyH2 (F && f, Args ... as)
 { return f(extrV<I>(as)...); }

template <std::size_t ... Is, typename F, typename ... Args>
auto applyH1 (std::index_sequence<Is...> const &, F && f, Args ... as)
   -> Vec<decltype(applyH2<0U>(f, as...)), sizeof...(Is)>
 { return { applyH2<Is>(f, as...)... }; }

template <typename F, typename ... Args, std::size_t N = dimVecV<Args...>>
auto apply (F && f, Args ... as)
 { return applyH1(std::make_index_sequence<N>{}, f, as...); }

template <std::size_t I, std::size_t J, typename F, typename ... Args>
auto applyMH3 (F && f, Args ... as)
 { return f(extrM<I, J>(as)...); }

template <std::size_t I, std::size_t ... Js, typename F, typename ... Args>
auto applyMH2 (std::index_sequence<Js...> const &, F && f, Args ... as)
   -> std::array<decltype(applyMH3<0U, 0U>(f, as...)), sizeof...(Js)>
 { return {{ applyMH3<I, Js>(f, as...)... }}; }

// Vec case: the first index list is empty: call applyH2()
template <std::size_t ... Js, typename F, typename ... Args>
auto applyMH1 (std::index_sequence<> const &,
               std::index_sequence<Js...> const &, F && f, Args ... as)
   -> Vec<decltype(applyH2<0U>(f, as...)), sizeof...(Js)>
 { return { applyH2<Js>(f, as...)... }; }

template <std::size_t ... Is, std::size_t ... Js, typename F,
          typename ... Args>
auto applyMH1 (std::index_sequence<Is...> const &,
               std::index_sequence<Js...> const & js, F && f, Args ... as)
   -> Mat<decltype(applyMH3<0U, 0U>(f, as...)), sizeof...(Is), sizeof...(Js)>
 { return {{{ applyMH2<Is>(js, f, as...) ... }}}; }

template <typename F, typename ... Args, std::size_t N = dimMatN<Args...>,
          std::size_t M = dimMatM<Args...>>
auto applyM (F && f, Args ... as)
 { return applyMH1(std::make_index_sequence<N>{},
                   std::make_index_sequence<M>{}, 
                   f, as...); }

long foo (int a, int b)
 { return a + b + 42; }

int main ()
 {
   Vec<int, 3U>      v3;
   Vec<int, 2U>      v2;
   Mat<int, 2U, 3U>  m23;
   Mat<int, 2U, 4U>  m24;

   auto r1 { applyM(foo, v2, v2) };
   auto r2 { applyM(foo, v3, v3) };
   auto r3 { applyM(foo, v3, 0)  };
   auto r4 { applyM(foo, v3, m23) };
   auto r5 { applyM(foo, m24, 0) };

   static_assert( std::is_same<decltype(r1), Vec<long, 2U>>{}, "!" );
   static_assert( std::is_same<decltype(r2), Vec<long, 3U>>{}, "!" );
   static_assert( std::is_same<decltype(r3), Vec<long, 3U>>{}, "!" );
   static_assert( std::is_same<decltype(r4), Mat<long, 2U, 3U>>{}, "!" );
   static_assert( std::is_same<decltype(r5), Mat<long, 2U, 4U>>{}, "!" );

   //applyM(foo, v2, v3);   // compilation error
   //applyM(foo, 1, 2);     // compilation error
   //applyM(foo, v2, m23);  // compilation error
   //applyM(foo, m24, m23); // compilation error
 }
max66
  • 65,235
  • 10
  • 71
  • 111
  • most of that I could infer from your original answer, what I really want here is a way to overload apply so it works with mat or vec. not having apply and applyM as separate functions, as that is simply adding an extra dimension. What I have no idea how to do is ensure the compiler will select the correct version of apply. – Glen Fletcher May 14 '18 at 06:45
  • The idea behind my example is that a list of `Args...` containing at least a `Mat` should follows the more generic path (`applyM()` -> `applyMH1()` -> `applyMH2()` -> `applyMH3()`, returning a `Mat`) where a list of `Args...` containig no `Mat`s, but only `Vec`s and scalars, should automagically follows the simplified `Vec`s path (`applyM()` -> `applyMH1()` (version with empty first list) -> `applyH2()`, returning a `Vec`). In this moment I have no time but in some hours I count to modify my example to make it clearer. – max66 May 14 '18 at 13:47
  • @glenflet - answer improved: as you can see, `applyM()` substitute the old `apply()`. – max66 May 14 '18 at 21:59
  • @glenflet - as you can see, this solution is a nightmare; maybe the variadic `tensor<>` way can be more interesting. Maybe one of the next day I'll try something. – max66 May 14 '18 at 22:02
0

If you're still interested in a variadic N dimensional tensor<> based solution... well... it's a nightmare but a nightmare of different type.

First of all, I've prepared the following recursive wrapper for std::array

template <typename T, std::size_t ...>
struct tensor;

template <typename T, std::size_t N, std::size_t ... Ns>
struct tensor<T, N, Ns...>
 { 
   using nextT = std::conditional_t<(sizeof...(Ns) > 0U),
                    tensor<T, Ns...>, T>;

   std::array<nextT, N>  value;

   auto const & operator[] (std::size_t i) const
    { return value[i]; }

   auto & operator[] (std::size_t i)
    { return value[i]; }
 };

The extrV() function is now recursive and accept the list of index in a std::index_sequence

// scalar case: return value
template <std::size_t ... Is, typename T>
constexpr auto extrV (std::index_sequence<Is...> const &, T const & t)
 { return t; }

// tensor case with lower dimension: skip the first requested index 
template <std::size_t I0, std::size_t ... Is,
          typename T, std::size_t ... Js>
constexpr auto extrV
   (std::index_sequence<I0, Is...> const &, tensor<T, Js...> const & t,
    std::enable_if_t<(sizeof...(Is) >= sizeof...(Js))> * = nullptr)
 { return extrV(std::index_sequence<Is...>{}, t); }

// tensor case with exact dimension: use the first requested index 
template <std::size_t I0, std::size_t ... Is,
          typename T, std::size_t ... Js>
constexpr auto extrV
   (std::index_sequence<I0, Is...> const &, tensor<T, Js...> const & t,
    std::enable_if_t<(sizeof...(Is)+1U == sizeof...(Js))> * = nullptr)
 { return extrV(std::index_sequence<Is...>{}, t[I0]); }

Now the custom type traits that, given a variadic list of Args... types, extract the longest common sequence of indexes (if any) and return it inside a std::index_sequence (in case)

template <typename ...>
struct commonDims;

// ground case: define type as surviving parameter
template <std::size_t I0, std::size_t ... Is>
struct commonDims<std::index_sequence<I0, Is...>>
 { using type = std::index_sequence<I0, Is...>; };

// no tensor type: continue
template <typename IS, typename T0, typename ... Ts>
struct commonDims<IS, T0, Ts...> : public commonDims<IS, Ts...>
 { };

// tensor type: continue with bigger common index list (if any)
template <std::size_t ... Is, typename T, std::size_t ... Js,
          typename ... Ts>
struct commonDims<std::index_sequence<Is...>, tensor<T, Js...>, Ts...>
   : public commonDims<greaterSeqT<std::index_sequence<Is...>,
                                   std::index_sequence<Js...>,
                                   (sizeof...(Is) > sizeof...(Js))>, Ts...>
 { };

template <typename ... Ts>
using commonDimsT = typename commonDims<std::index_sequence<>, Ts...>::type;

As you can see, it uses a greaterSeq helper type traits

template <typename, typename, bool>
struct greaterSeq;

template <typename T1, typename T2>
struct greaterSeq<T1, T2, true> : public gsHelper<T1, T1, T2>
 { };

template <typename T1, typename T2>
struct greaterSeq<T1, T2, false> : public gsHelper<T2, T2, T1>
 { };

template <typename T1, typename T2, bool B>
using greaterSeqT = typename greaterSeq<T1, T2, B>::type;

that uses another gsHelper helper type traits

template <typename, typename, typename, typename = std::true_type>
struct gsHelper;

// sequences of different lengths: skipp the first index in longest
template <typename T, std::size_t I0, std::size_t ... Is, std::size_t ... Js>
struct gsHelper<T, std::index_sequence<I0, Is...>,
          std::index_sequence<Js...>,
          std::integral_constant<bool, (sizeof...(Is) >= sizeof...(Js))>>
   : public gsHelper<T, std::index_sequence<Is...>,
                     std::index_sequence<Js...>>
 { };

template <typename T, typename IS>
struct gsHelper<T, IS, IS>
 { using type = T; };

Now the apply() function

template <typename F, typename ... Args, typename IS = commonDimsT<Args...>>
auto apply (F && f, Args const & ... as)
 { return applyH1(std::index_sequence<>{}, IS{}, f, as...); }

that start a recursion of call between applyH1() and applyH2() functions

template <std::size_t ... Is, std::size_t ... Js, std::size_t ... Ks,
          typename F, typename ... Args>
auto applyH2 (std::index_sequence<Is...> const &,
              std::index_sequence<Js...> const &,
              std::index_sequence<Ks...> const & ks,
              F && f, Args const & ... as)
   -> tensor<
         decltype(call(std::index_sequence<(Is, 0U)..., 0U, (Ks, 0U)...>{},
                       f, as...)), sizeof...(Js), Ks...>
 { return {{{ applyH1(std::index_sequence<Is..., Js>{},
                      ks, f, as...) ... }}}; }

template <typename IS, typename F, typename ... Args>
auto applyH1 (IS const & is, std::index_sequence<> const &,
              F && f, Args const & ... as)
 { return call(is, f, as...); }

template <typename IS, std::size_t J0, std::size_t ... Js,
          typename F, typename ... Args>
auto applyH1 (IS const & is, std::index_sequence<J0, Js...> const &,
              F && f, Args const & ... as)
 { return applyH2(is, std::make_index_sequence<J0>{},
                  std::index_sequence<Js...>{}, f, as...); }

that terminate with a call to call()

template <typename IS, typename F, typename ... Args>
auto call (IS const & is, F && f, Args const & ... as)
 { return f(extrV(is, as)...); }

The following is a full compiling example

#include <array>
#include <string>
#include <iostream>

template <typename T, std::size_t ...>
struct tensor;

template <typename T, std::size_t N, std::size_t ... Ns>
struct tensor<T, N, Ns...>
 { 
   using nextT = std::conditional_t<(sizeof...(Ns) > 0U),
                    tensor<T, Ns...>, T>;

   std::array<nextT, N>  value;

   auto const & operator[] (std::size_t i) const
    { return value[i]; }

   auto & operator[] (std::size_t i)
    { return value[i]; }
 };

// scalar case: return value
template <std::size_t ... Is, typename T>
constexpr auto extrV (std::index_sequence<Is...> const &, T const & t)
 { return t; }

// tensor case with lower dimension: skip the first requested index 
template <std::size_t I0, std::size_t ... Is,
          typename T, std::size_t ... Js>
constexpr auto extrV
   (std::index_sequence<I0, Is...> const &, tensor<T, Js...> const & t,
    std::enable_if_t<(sizeof...(Is) >= sizeof...(Js))> * = nullptr)
 { return extrV(std::index_sequence<Is...>{}, t); }

// tensor case with exact dimension: use the first requested index 
template <std::size_t I0, std::size_t ... Is,
          typename T, std::size_t ... Js>
constexpr auto extrV
   (std::index_sequence<I0, Is...> const &, tensor<T, Js...> const & t,
    std::enable_if_t<(sizeof...(Is)+1U == sizeof...(Js))> * = nullptr)
 { return extrV(std::index_sequence<Is...>{}, t[I0]); }

template <typename, typename, typename, typename = std::true_type>
struct gsHelper;

// sequences of different lengths: skipp the first index in longest
template <typename T, std::size_t I0, std::size_t ... Is, std::size_t ... Js>
struct gsHelper<T, std::index_sequence<I0, Is...>,
          std::index_sequence<Js...>,
          std::integral_constant<bool, (sizeof...(Is) >= sizeof...(Js))>>
   : public gsHelper<T, std::index_sequence<Is...>,
                     std::index_sequence<Js...>>
 { };

template <typename T, typename IS>
struct gsHelper<T, IS, IS>
 { using type = T; };

template <typename, typename, bool>
struct greaterSeq;

template <typename T1, typename T2>
struct greaterSeq<T1, T2, true> : public gsHelper<T1, T1, T2>
 { };

template <typename T1, typename T2>
struct greaterSeq<T1, T2, false> : public gsHelper<T2, T2, T1>
 { };

template <typename T1, typename T2, bool B>
using greaterSeqT = typename greaterSeq<T1, T2, B>::type;

template <typename ...>
struct commonDims;

// ground case: define type as surviving parameter
template <std::size_t I0, std::size_t ... Is>
struct commonDims<std::index_sequence<I0, Is...>>
 { using type = std::index_sequence<I0, Is...>; };

// no tensor type: continue
template <typename IS, typename T0, typename ... Ts>
struct commonDims<IS, T0, Ts...> : public commonDims<IS, Ts...>
 { };

// tensor type: continue with bigger common index list (if any)
template <std::size_t ... Is, typename T, std::size_t ... Js,
          typename ... Ts>
struct commonDims<std::index_sequence<Is...>, tensor<T, Js...>, Ts...>
   : public commonDims<greaterSeqT<std::index_sequence<Is...>,
                                   std::index_sequence<Js...>,
                                   (sizeof...(Is) > sizeof...(Js))>, Ts...>
 { };

template <typename ... Ts>
using commonDimsT = typename commonDims<std::index_sequence<>, Ts...>::type;

template <typename IS, typename F, typename ... Args>
auto call (IS const & is, F && f, Args const & ... as)
 { return f(extrV(is, as)...); }

template <std::size_t ... Is, std::size_t ... Js, std::size_t ... Ks,
          typename F, typename ... Args>
auto applyH2 (std::index_sequence<Is...> const &,
              std::index_sequence<Js...> const &,
              std::index_sequence<Ks...> const & ks,
              F && f, Args const & ... as)
   -> tensor<
         decltype(call(std::index_sequence<(Is, 0U)..., 0U, (Ks, 0U)...>{},
                       f, as...)), sizeof...(Js), Ks...>
 { return {{{ applyH1(std::index_sequence<Is..., Js>{},
                      ks, f, as...) ... }}}; }

template <typename IS, typename F, typename ... Args>
auto applyH1 (IS const & is, std::index_sequence<> const &,
              F && f, Args const & ... as)
 { return call(is, f, as...); }

template <typename IS, std::size_t J0, std::size_t ... Js,
          typename F, typename ... Args>
auto applyH1 (IS const & is, std::index_sequence<J0, Js...> const &,
              F && f, Args const & ... as)
 { return applyH2(is, std::make_index_sequence<J0>{},
                  std::index_sequence<Js...>{}, f, as...); }

template <typename F, typename ... Args, typename IS = commonDimsT<Args...>>
auto apply (F && f, Args const & ... as)
 { return applyH1(std::index_sequence<>{}, IS{}, f, as...); }

long foo (int a, int b)
 { return a + b + 42; }

int main ()
 { 
   tensor<int, 2, 3, 4, 5>  t0;

   t0[0][0][0][0] = 1;

   using t1 = commonDimsT<tensor<int, 1>, long, tensor<long, 3, 2, 1>, int>;

   static_assert(std::is_same<t1, std::index_sequence<3, 2, 1>>{}, "!");

   auto r1 { apply(foo, tensor<int, 3, 2, 1>{}, 0) };
   auto r2 { apply(foo, tensor<int, 3, 2, 1>{}, tensor<int, 3, 2, 1>{}) };
   auto r3 { apply(foo, tensor<int, 3, 2, 1>{}, tensor<int, 2, 1>{}) };
   auto r4 { apply(foo, tensor<int, 3, 2, 1>{}, tensor<int, 1>{}) };
   auto r5 { apply(foo, 0, tensor<int, 1>{}) };

   static_assert(std::is_same<decltype(r1), tensor<long, 3, 2, 1>>{}, "!");
   static_assert(std::is_same<decltype(r2), tensor<long, 3, 2, 1>>{}, "!");
   static_assert(std::is_same<decltype(r3), tensor<long, 3, 2, 1>>{}, "!");
   static_assert(std::is_same<decltype(r4), tensor<long, 3, 2, 1>>{}, "!");
   static_assert(std::is_same<decltype(r5), tensor<long, 1>>{}, "!");

   // compilation errors (no common tensor)
   //apply(foo, 0, 0);
   //apply(foo, tensor<int, 3, 2, 1>{}, tensor<int, 2>{});
 }
max66
  • 65,235
  • 10
  • 71
  • 111