Using Arrays as Template Parameters With Default Values in C++17 and Earlier

178 Views Asked by At

I need to define a C++ template that accepts several 3D coordinates as their parameters.

Why do I need this? The motivation is to create an "accessor" template for doing implicit index transformations in 3D space based on several parameters already known at compile-time, which has several useful applications. For a hypothetical example, a template may support the following feature:

  1. Origin transformation: A 3D array may have been created with the origin centered around (0, 0, 0). But if some formulas or algorithms are designed to work with 1-based arrays, one can use a template with the offset parameter set to (-1, -1, -1). Similarly, if there's a need to recenter the origin at (20, 20, 20), one can use a template with the offset parameter set to (20, 20, 20).

  2. Padding: Sometimes, the dimension of the 3D space must be a multiple of a certain size, in this case, one can use a template with the padding parameter set to (4, 4, 4).

  3. Blocking: Sometimes, it's more efficient to store values in the 3D space in a tiled or blocked format, so a blocking parameter can be used to specify the block size.

To create such a template, my first attempt is to declare it as:

template <
    int offset_i = 0,
    int offset_j = 0,
    int offset_k = 0,
    int padding_i = 0,
    int padding_j = 0,
    int padding_k = 0,
    int blocking_i = 0,
    int blocking_j = 0,
    int blocking_k = 0
>
class Space;

But as you see, when all dimensions of these coordinates are defined as separate integer variables, the parameter list would become exceedingly long - 3 coordinates need 9 parameters. This makes it hard to use, especially when the full type name of the template must be typed out with a lot of typing. For example, if a member function needs to return a 3D space, its signature would look like this:

Space<
    offset_i,
    offset_j,
    offset_k,
    padding_i,
    padding_j,
    padding_k,
    blocking_i,
    blocking_j,
    blocking_k
> member_function(void);

Thus, it's highly desirable to declare the templates in a way to use compile-time arrays, instead of compile-time integers. For convenience, it's the best if their default values can be declared directly at the location of the template declaration as values, rather than as variable names that are declared elsewhere.

In C++20, the following solution became possible:

template <
    std::array<int, 3> offset = std::array<int, 3>{0, 0, 0},
    std::array<int, 3> padding = std::array<int, 3>{0, 0, 0},
    std::array<int, 3> blocking = std::array<int, 3>{0, 0, 0}
>
class Space;

This has a much higher readability. It also simplifies the signature of member functions to:

Space<offset, padding, blocking> member_function(void);

A proposed C++ standard change would simplify it even further, to:

template <
    std::array<int, 3> offset={0, 0, 0},
    std::array<int, 3> padding={0, 0, 0},
    std::array<int, 3> blocking={0, 0, 0}
>
class Space;

At this point, the original problem has already been solved in a very satisfactory manner.

However, this solution requires C++20, which may be unavailable in systems with older compiler versions.

Question

Is there an alternative and more compatible way to achieve the same code readability goal using other techniques already available in earlier C++ versions, such as C++17? Using std::array is not necessary, any other methods will similar readability improvement are acceptable.

3

There are 3 best solutions below

0
On BEST ANSWER

Use type parameters that have non-type parameters.

template <int x_, int y_, int z_> struct array3 {
  static constexpr int x = x_;
  static constexpr int y = y_;
  static constexpr int z = z_;
};

using zeros3 = array3<0,0,0>;

template <typename offset = zeros3, 
          typename padding = zeros3, 
          typename blocking = zeros3>
struct Space
{
};

Space<array3<1,2,3>> withOffsets;

You can constrain template parameters of Space to be instantiations of array3 using SFINAE if you want.

static_assert(is_array3_v<offset>, "Offset must be an array3");
static_assert(is_array3_v<padding>, "Padding must be an array3");
static_assert(is_array3_v<blocking>, "Blocking must be an array3");

where is_array3_v is defined as

template <typename> struct is_array3 : public std::false_type {};
template <int x, int y, int z> struct is_array3<array3<x,y,z>> : public std::true_type {};
template <typename t> static constexpr bool is_array3_v = is_array3<t>::value;
1
On

Is there an alternative and more compatible way to achieve the same code readability goal using other techniques already available in earlier C++ versions, such as C++17?

Since class type non-type template parameter are only allowed from C+20, you can use built-in array as the template parameter as shown below:

const int defaultArr[3]={};
//c++17 compatible
template <const int (&offsetArr)[3] = defaultArr, 
          const int (&paddingArr)[3] = defaultArr, 
          const int (&blockingArr)[3] = defaultArr>   
class Space{
    void member_function(); 
};
template <const int (&offsetArr)[3], 
          const int (&paddingArr)[3], 
          const int (&blockingArr)[3]>
void Space<offsetArr, paddingArr, blockingArr>::member_function(){
     
} 

Working demo

0
On

You may use a constexpr function which returns a closure (lambda are able to be used in constexpr functions since C++17). For example:

#include <array>
#include <iostream>

using std::array;

constexpr auto make_accessor(array<int, 3> offset = {0, 0, 0}) {
  return [=](array<int, 3> point) {
    const array<int, 3> offset_point{
      point[0] + offset[0],
      point[1] + offset[1],
      point[2] + offset[2],
    };
    return offset_point;
  };
}

void show_point(const array<int, 3>& p) {
  std::cout << "(" << p[0] << ", " << p[1] << ", " << p[2] << ")\n";
}

int main() {
  constexpr auto accessor = make_accessor({1, 2, 3});

  const array<int, 3> p1 = accessor({4, 5, 6});
  const array<int, 3> p2 = accessor({7, 8, 9});
  const array<int, 3> p3 = accessor({10, 11, 12});

  show_point(p1); show_point(p2); show_point(p3);
}

Behind the scenes, each call to make_accessor will effectively create an instance of an anonymous class with the parameters as constexpr members (if make_accessor is itself called in a constexpr context).