3
\$\begingroup\$

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_arrays. 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?

    A Summation Function For Boost.MultiArray in C++

  • 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 and boost::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 of boost::detail::multi_array::sub_array and boost::detail::multi_array::const_sub_array, I am not sure what's the appropriate thing to return back. I have ever tried something with std::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 in std::optional<> structure) and a little horrible I think. If there is any suggestion or possible improvement, please tell me!

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

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:

  1. 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}}.

  2. 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_arrays, sub_arrays or const_sub_arrays. 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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.