This is a follow-up question for A Summation Function For Boost.MultiArray in C++. Besides the summation operation of all elements, I am trying to focus on the element-wise operation here. The main idea of this question is to implement an element_wise_add
function for Boost.MultiArray. The purpose of this element_wise_add
function is to perform element-wise add operation on two boost::multi_array
s. The function element_wise_add
has two input parameters input1
and input2
for element-wise add operation and the return type is the element-wised result.
template<class T> requires is_summable<T>
auto element_wise_add(const T& input1, const T& input2)
{
return input1 + input2;
}
// Deal with the two input case
template<class T, std::size_t Dims> requires is_summable<T>
auto element_wise_add(const boost::detail::multi_array::const_sub_array<T, Dims>& input1, const boost::detail::multi_array::const_sub_array<T, Dims>& input2)
{
boost::multi_array<T, Dims> output(reinterpret_cast<boost::array<size_t, Dims> const&>(*input1.shape()));
for (typename boost::detail::multi_array::const_sub_array<T, Dims>::index i = 0; i < input1.shape()[0]; i++)
{
output[i] = element_wise_add(input1[i], input2[i]);
}
return output;
}
// Deal with the two input case
template<class T, std::size_t Dims> requires is_summable<T>
auto element_wise_add(const boost::detail::multi_array::sub_array<T, Dims>& input1, const boost::detail::multi_array::sub_array<T, Dims>& input2)
{
boost::multi_array<T, Dims> output(reinterpret_cast<boost::array<size_t, Dims> const&>(*input1.shape()));
for (typename boost::detail::multi_array::sub_array<T, Dims>::index i = 0; i < input1.shape()[0]; i++)
{
output[i] = element_wise_add(input1[i], input2[i]);
}
return output;
}
// Deal with the two input case
template<class T, std::size_t Dims> requires is_summable<T>
auto element_wise_add(const boost::multi_array<T, Dims>& input1, const boost::multi_array<T, Dims>& input2)
{
if (*input1.shape() != *input2.shape()) // if shape is different
{
return input1; // unable to perform element-wise add operation
}
boost::multi_array<T, Dims> output(reinterpret_cast<boost::array<size_t, Dims> const&>(*input1.shape()));
for (typename boost::multi_array<T, Dims>::index i = 0; i < input1.shape()[0]; i++)
{
output[i] = element_wise_add(input1[i], input2[i]);
}
return output;
}
The used is_summable
concept:
template<typename T>
concept is_summable = requires(T x) { x + x; };
The test of this element_wise_add
function is as follows.
// Create a 3D array that is 3 x 4 x 2
typedef boost::multi_array<double, 3> array_type;
typedef array_type::index index;
array_type A(boost::extents[3][4][2]);
// Assign values to the elements
int values = 0;
for (index i = 0; i != 3; ++i)
for (index j = 0; j != 4; ++j)
for (index k = 0; k != 2; ++k)
A[i][j][k] = values++;
for (index i = 0; i != 3; ++i)
for (index j = 0; j != 4; ++j)
for (index k = 0; k != 2; ++k)
std::cout << A[i][j][k] << std::endl;
auto DoubleA = element_wise_add(A, A);
for (index i = 0; i != 3; ++i)
for (index j = 0; j != 4; ++j)
for (index k = 0; k != 2; ++k)
std::cout << DoubleA[i][j][k] << std::endl;
All suggestions are welcome.
Which question it is a follow-up to?
What changes has been made in the code since last question?
The previous question is the implementation of a summation function for Boost.MultiArray and the main idea of this question is to implement an
element_wise_add
function for Boost.MultiArray.Why a new review is being asked for?
The similar usage of three types overload function for
boost::multi_array
,boost::detail::multi_array::sub_array
andboost::detail::multi_array::const_sub_array
appears again. I know maybe this is not a good idea. However, there is no any better way coming up in my mind. Moreover, the exception handling for the "shape is different" situation is not perfect. In the case ofboost::detail::multi_array::sub_array
andboost::detail::multi_array::const_sub_array
, I am not sure what's the appropriate thing to return back. I have ever tried something withstd::optional
like this:template<class T, std::size_t Dims> requires is_summable<T> auto element_wise_add(const boost::detail::multi_array::const_sub_array<T, Dims>& input1, const boost::detail::multi_array::const_sub_array<T, Dims>& input2) { std::optional<boost::detail::multi_array::const_sub_array<T, Dims>> final_output; if (*input1.shape() != *input2.shape()) // if shape is different { final_output = std::nullopt; } else { boost::multi_array<T, Dims> output(reinterpret_cast<boost::array<size_t, Dims> const&>(*input1.shape())); for (typename boost::detail::multi_array::const_sub_array<T, Dims>::index i = 0; i < input1.shape()[0]; i++) { output[i] = element_wise_add(input1[i], input2[i]); } final_output = output; } return final_output; }
And this:
template<class T, std::size_t Dims> requires is_summable<T> auto element_wise_add(const boost::detail::multi_array::sub_array<T, Dims>& input1, const boost::detail::multi_array::sub_array<T, Dims>& input2) { std::optional<boost::detail::multi_array::sub_array<T, Dims>> final_output; if (*input1.shape() != *input2.shape()) // if shape is different { final_output = std::nullopt; } else { boost::multi_array<T, Dims> output(reinterpret_cast<boost::array<size_t, Dims> const&>(*input1.shape())); for (typename boost::detail::multi_array::sub_array<T, Dims>::index i = 0; i < input1.shape()[0]; i++) { output[i] = element_wise_add(input1[i], input2[i]); } final_output = output; } return final_output; }
However, this is so complex to use (the usage of
.value()
or.value_or()
function is needed to access the content instd::optional<>
structure) and a little horrible I think. If there is any suggestion or possible improvement, please tell me!
1 Answer 1
Consider turning this into an operator+()
Since element-wise addition is quite a common and natural operation (for example, the STL supports this for std::valarray
), it might be more intuitive to overload operator+()
instead of creating a function element_wise_add()
.
See this question for a possible implementation that also extends this easily to other operators.
Another advantage of making this an operator+()
is that it makes a boost::multi_array
itself satisfy is_summable
, so without adding explicit support for recursive containers, the following would then work:
boost::multi_array<boost::multi_array<double, 2>, 3> array1, array2;
auto array3 = array1 + array2;
Handling mismatched array dimensions
I would indeed not use std::optional
to signal errors for mathematical operations. I see two ways:
Ensure the dimensions of the returned array are the maximums of the dimensions of the two input arrays. So if I add
{{1}, {2}}
to{{3, 5}}
the result will be{{4, 5}, {2, 0}}
.Throw a
std::logic_error
, in the assumption that it is a programming error to add two mismatched errors.
Avoiding repetition
You are basically writing the same thing thrice, the only variation is whether the inputs are regular boost::multi_array
s, sub_array
s or const_sub_array
s. To avoid this, you want to make the input types templated, and to ensure that it only matches boost::multi_array
related types, just write a concept for that. Again, you can use expressions that you already use for this:
template<T>
concept is_multi_array = requires(T x) {
x.shape();
boost::multi_array(x);
};
This will test that the type of x
has a shape()
member function and that a boost::multi_array
can be copy-constructed from it. Then just write:
template<class T> requires is_multi_array<T>
auto element_wise_add(const T& input1, const T& input2)
{
if (*input1.shape() != *input2.shape())
{
throw std::logic_error("array shape mismatch");
}
boost::multi_array output(input1);
for (decltype(+input1.shape()[0]) i = 0; i < input1.shape()[0]; i++)
{
output[i] = element_wise_add(input1[i], input2[i]);
}
return output;
}
The drawback here is that the whole input1
array is copied into output
for no good reason, except that I don't see how to construct a new multi_array
more efficiently without going into the type hell that comes with boost::multi_array
. Perhaps this can be outsourced to another template function.
You'll also notice I changed the way the type for i
is determined: I use decltype()
, but since that keeps the const
-ness, I have to cast that away. There are various ways to do that, I used the unary +
trick here.
Handling more combinations
What if I want to add an sub_array
to a regular multi_array
of the same size? With your approach, you would have to handle all possible combinations separately, but with the single function example I showed, you can just write:
template<class T1, class T2> requires (is_multi_array<T1> && is_multi_array<T2>)
auto element_wise_add(const T1& input1, const T2& input2)
{
...
}
However, also consider that you might want to add an array of doubles to an array of integers. That could be made to work, if you also write:
template<typename T1, typename T2>
concept is_summable = requires(T1 x, T2 y) { x + y; };
template<class T1, class T2> requires is_summable<T1, T2>
auto element_wise_add(const T1& input1, const T2& input2)
{
return input1 + input2;
}
Although to make it truly useful, you want the type of output
to have a value type that matches the result of adding the value types of the two input arrays.