3

This snippet is a small example from a code base. It is a free function to multiply matrices with each other. The Matrix itself is templatized on ROWS and COLUMNS which, like the std::array, makes it a bit painful to use in function interfaces.

The full type name becomes Matrix<ROWS, COLUMNS> which is fine on its own, but when repeated three times (for the return value and two arguments) it really harms the readability of the function interface.

What syntax or strategies are available in modern C++ to make the repeated type name less noisy?

template <uint8_t ROWS, uint8_t COLUMNS>
Matrix<ROWS, COLUMNS> operator*(
    const Matrix<ROWS, COLUMNS>& lhs, 
    const Matrix<ROWS, COLUMNS>& rhs) noexcept 
{
    Matrix<ROWS, COLUMNS> result;
    for (uint8_t row = 0; row < ROWS; ++row) 
    {
        for (uint8_t col = 0; col < COLUMNS; ++col) 
        {
            for (uint8_t i = 0; i < COLUMNS; ++i)
            {
                result(row, col) += lhs(row, i) * rhs(i, col);
            }
        }
    }
    return result;
}

Requirements:

  • all Matrixes (arguments and return value) must have the same dimensions
  • The Matrix knows its own size (.columns(), .rows()), so we don't need to use the template arguments in those loops.
  • the Matrix also offers a ::size_type so the ideal solution would let us use that (cleanly) instead of hardcoding uint8_t in the loop.
JeJo
  • 30,635
  • 6
  • 49
  • 88
ulfben
  • 151
  • 8
  • 2
    `auto operator*(const Matrix& lhs, decltype(lhs) rhs)` – 273K Feb 05 '23 at 08:20
  • 2
    Define a `matrix` concept, then something like `template RHS> LHS operator*(const LHS& lhs, const RHS& rhs)`. – 康桓瑋 Feb 05 '23 at 08:32
  • ^That, and have the dimensions be static members. Then you write `for(LHS::size_type row = 0; row < lhs.rows; row++) ...`. Also, using `uint8_t` as indices sounds like an overflow waiting to happen. – Passer By Feb 05 '23 at 08:48
  • you could make it a friend and define it inside the class template – 463035818_is_not_an_ai Feb 05 '23 at 10:06
  • Simplest, if you implement this inside the class itself, since you could simply leave out the template parameters in this case. You don't even need to use a specific C++ standard for this to work. `template class Matrix{... friend Matrix operator*(Matrix const& lhs, Matrix const& rhs) { ... } };`, see (2) here: https://en.cppreference.com/w/cpp/language/friend – fabian Feb 05 '23 at 11:58

2 Answers2

3

What syntax or strategies are available in modern C++ to make the repeated type name less noisy?

The easiest way of achieving this is to make the operator* be a friend function and implementation is done inside the class itself.

However, if you insist having outside the class, the noisy part of the shown code can be eliminated by having an abbreviated function template (Since ).

#include <type_traits> // std::decay_t

auto operator*(const auto& lhs, const auto& rhs) noexcept 
{
    // static assert for type check!
    static_assert(std::is_same_v<std::decay_t<decltype(lhs)>
                               , std::decay_t<decltype(rhs)>>, "are not same type!");

    std::decay_t<decltype(lhs)> result;
    for (auto row = 0u; row < lhs.rows(); ++row) {
        for (auto col = 0u; col < lhs.columns(); ++col) {
            for (auto i = 0u; i < lhs.columns(); ++i) {
                // calculate result
            }
        }
    }
    return result;
}

That been said, now the operator* accepts any parameters whose types are same. But, we need only for Matrix<ROWS, COLUMNS> type. A type trait, which checks, passed type, is of the type Matrix helps here.

#include <type_traits> // std::false_type, std::true_type

template <typename> struct is_Matrix final : std::false_type{};
template <std::size_t ROWS, std::size_t COLUMNS>
struct is_Matrix<Matrix<ROWS, COLUMNS>> final : std::true_type {};

// or variable template
// template <typename> inline constexpr bool is_Matrix = false;
// template <std::size_t ROWS, std::size_t COLUMNS>
// inline constexpr bool is_Matrix<Matrix<ROWS, COLUMNS>> = true;

Using the trait now, you can restrict (i.e. SFINAE or static_assert or concept ing) the operator* to be used only for Matrix type.

See live example code

JeJo
  • 30,635
  • 6
  • 49
  • 88
0

One alternative is to use auto return type, and change the template arguments to the full type. Something like this:

template <class Matrix>
constexpr auto operator*(const Matrix& lhs, const Matrix& rhs) noexcept {
    using size_type = Matrix::size_type;
    Matrix result;
    for (size_type row = 0; row < Matrix::HEIGHT; ++row) {
        for (size_type col = 0; col < Matrix::WIDTH; ++col) {
            for (size_type i = 0; i < Matrix::HEIGHT; ++i) {
                result(row, col) += lhs(row, i) * rhs(i, col);
            }
        }
    }
    return result;
}

This is certainly more readable, and I believe it will still enforce that both arguments must be the same type / size.

One downside of this might be that the template becomes too promiscious. It could be instantiated (and fail) for non-Matrix types too. Is there a simple way (concepts?) to limit the template to just Matrix-like classes?

Update: With the help of JeJo's answer I was able to piece this constraint together:

template<typename> constexpr bool isMatrix = false;
template <std::size_t ROWS, std::size_t COLUMNS>
constexpr bool isMatrix<Matrix<ROWS, COLUMNS>> = true;

using Matrix4 = Matrix<4, 4>;   

static_assert(isMatrix<Matrix4>, "Constraint test failed. Matrix4 should be identified as a Matrix");
static_assert(!isMatrix<Tuple>, "Constraint test failed. Tuple shouldn't be identified as a Matrix.");

template <class Matrix>
    requires (isMatrix<Matrix>)
constexpr auto operator*(const Matrix& lhs, const Matrix& rhs) noexcept {
    using size_type = typename Matrix::size_type;
    Matrix result;
    for (size_type row = 0; row < Matrix::ROWS; ++row) {
        for (size_type col = 0; col < Matrix::COLUMNS; ++col) {
            for (size_type i = 0; i < Matrix::COLUMNS; ++i) {
                result(row, col) += lhs(row, i) * rhs(i, col);
            }
        }
    }
    return result;
}

The constraint seems to work. I'm not sure repitition has decreased much, but it is more readable than the forest of angle-brackets I had initialy.

ulfben
  • 151
  • 8