Проектирование класса Matrix — проблемы с шаблоном выражения

Я хотел бы разработать класс Matrix, который использует шаблоны выражений и избегает временных. Я хотел бы начать с чего-то очень простого, поэтому я решил сначала просто реализовать сложение матриц.

Я создал класс AddExp для представления составного выражения и перегрузил оператор (), который поможет. И оператор присваивания копии Matrix& Matrix::operator=(AddExp& addExp) запускает оценку, рекурсивно вызывая () для каждого из элементов в этом выражении.

Однако компилятор жалуется, что не может преобразовать AddExp<Matrix<int,3,3>,Matrix<int,3,3>> в Matrix<int,3,3>. Меня озадачивает, почему оператор копирования-присваивания класса Matrix не может этого сделать.

Обратите внимание, что если я заменю Matrix C на auto C, код будет выполняться нормально, но важно иметь возможность писать выражения в форме Matrix C = <something>.

Обозреватель компиляторов - C++ (x86-64 gcc 12.2)

Я провожу довольно много времени над этим. Я хотел бы попросить о помощи; если вы, ребята, играли с шаблонами выражений или обнаружили что-то принципиально неправильное в моем коде, было бы неплохо исправить это и извлечь из этого уроки.

#include <array>
#include <iostream>

/**
 * Compile time fixed-size matrix.
 */

template <typename LHS, typename RHS, typename T>
class AddExp {
   public:
    AddExp(LHS& lhsExp, RHS& rhsExp) : m_lhsExp{lhsExp}, m_rhsExp{rhsExp} {}

    T operator()(int i, int j) { return m_lhsExp(i, j) + m_rhsExp(i, j); }

    template <typename R>
    AddExp<AddExp<LHS, RHS, T>, R, T> operator+(R& exp) {
        return AddExp<AddExp<LHS, RHS, T>, R, T>(*this, exp);
    }

    // friend class Exp<AddExp,T>;
   private:
    LHS& m_lhsExp;
    RHS& m_rhsExp;
};

template <typename T = double, int ROWS = 3, int COLS = 3>
class Matrix {
   public:
    Matrix() : m_rows{ROWS}, m_cols{COLS}, data{} {}
    Matrix(std::initializer_list<std::initializer_list<T>> lst)
        : m_rows{ROWS}, m_cols{COLS}, data{} {
        int i{0};
        for (auto& l : lst) {
            int j{0};
            for (auto& v : l) {
                data[m_rows * i + j] = v;
                ++j;
            }
            ++i;
        }
    }

    T& operator()(int i, int j) { return data[ROWS * i + j]; }

    template <typename RHS>
    AddExp<Matrix<T, ROWS, COLS>, RHS, T> operator+(RHS& exp) {
        return AddExp<Matrix<T, ROWS, COLS>, RHS, T>(*this, exp);
    }

    template <typename RHS>
    Matrix<T, ROWS, COLS>& operator=(const RHS& exp) {
        for (int i{}; i < ROWS; ++i) {
            for (int j{}; j < COLS; ++j) {
                (*this)(i, j) = exp(i, j);
            }
        }

        return (*this);
    }

    // friend class Exp<Matrix,T>;
   private:
    std::array<T, ROWS * COLS> data;
    int m_rows;
    int m_cols;
};

int main() {
    Matrix A{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};

    Matrix B{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};

    Matrix<int, 3, 3> C = A + B;

    return 0;
}
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
59
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Я вижу две проблемы: присваивание против инициализации и константность.

назначение против инициализации

В вашем main у вас есть:

Matrix<int, 3, 3> C = A + B;

Для этого требуется конструктор Matrix, который принимает const RHS& exp. Измените его на:

Matrix<int, 3, 3> C;
C = A + B;

const-корректность

Ваш T operator()(int i, int j) должен быть const, так как вы не хотите менять выражение или данные при выполнении суммы.

Проверьте это на Godbolt.

Если вы объявляете переменную и назначаете ее в том же операторе, фактически используется конструктор, т.е.

Matrix<int, 3, 3> C = A + B;

не использует operator= из Matrix<int, 3, 3>, но ищет конструктор, которому может быть передано выражение A+B.

Однако вы реализовали только оператор присваивания. Вам также необходимо реализовать конструктор a, принимающий ссылку на «матричный» тип, чтобы компилятор мог принять это выражение. Вы можете просто сделать это, используя оператор присваивания в теле конструктора.

Некоторые дополнительные примечания:

  • Вы используете структуру данных фиксированного размера для хранения данных элемента; кроме того, у вас нет никакой логики для изменения размера матрицы, что было бы возможно только в очень ограниченных случаях, поэтому я рекомендую избавиться от переменных-членов и просто определить статические constexpr переменные. Эти переменные могут помочь вам избежать матриц несовместимых размеров вместе.

  • operator= и operator() несовместимы; индексы в реализации operator= необходимо поменять местами.

    Рассмотрим случай, когда матрица имеет 100 строк и 1 столбец. Самый внутренний цикл будет использовать (*this)(99, 0), который вернет data[ROWS * 99 + 0], то есть data[9801], который явно выходит за пределы.

  • Вы не проверяете, совместимы ли размеры матрицы; кроме того, вы можете автоматически определить тип элемента AddExp на основе LHS и RHS.

  • Вы можете реализовать operator+ в пространстве имен, чтобы избежать повторения реализации для каждого типа выражения; вам просто нужно ограничить возможные параметры шаблона чем-то «матричным», используя концепции (или SFINAE).

  • Если вы берете параметр operator= как ссылку на константу, operator() для параметра должна иметь реализацию operator(), которая работает для константных объектов.

Ниже приведена обновленная версия с учетом этих замечаний (требуется C++ 20 из-за использования концепции):

#include <algorithm>
#include <array>
#include <iostream>
#include <type_traits>
#include <stdexcept>

using IndexType = unsigned;

template<class T, IndexType rows, IndexType columns>
struct MatrixType
{
    using value_type = T;
    static constexpr IndexType Rows = rows;
    static constexpr IndexType Columns = columns;
};

template<class T>
concept MatrixLike = requires(IndexType row, IndexType column, T const mat)
{
    mat(row, column);
    std::is_base_of_v<MatrixType<typename T::value_type, T::Rows, T::Columns>, std::remove_cvref_t<T>>;
    {T::Rows} -> std::convertible_to<IndexType>;
    {T::Columns} -> std::convertible_to<IndexType>;
};

template<class T, class TargetValueType, IndexType rows, IndexType columns>
concept SizedMatrixLike = MatrixLike<T> && std::is_convertible_v<typename T::value_type, TargetValueType> && (T::Rows == rows) && (T::Columns == columns);

template<class M1, class M2>
using ValueAddResult_t = decltype(std::declval<typename M1::value_type>() + std::declval<typename M2::value_type>());

/**
 * Compile time fixed-size matrix.
 */
template <MatrixLike LHS, MatrixLike RHS>
class AddExp : public MatrixType<ValueAddResult_t<LHS, RHS>, LHS::Rows, LHS::Columns> {
public:
    static_assert(LHS::Rows == RHS::Rows, "row dimension mismatch");
    static_assert(LHS::Columns == RHS::Columns, "column dimension mismatch");

    AddExp(LHS const& lhsExp, RHS const& rhsExp)
        : m_lhsExp{ lhsExp }, m_rhsExp{ rhsExp }
    {
    }

    decltype(auto) operator()(IndexType i, IndexType j) const
    {
        return m_lhsExp(i, j) + m_rhsExp(i, j);
    }

private:
    LHS const& m_lhsExp;
    RHS const& m_rhsExp;
};

template <typename T = double, IndexType ROWS = 3, IndexType COLS = 3>
class Matrix : public MatrixType<T, ROWS, COLS> {
public:

    Matrix() : m_data{} {}

    Matrix(std::initializer_list<std::initializer_list<T>> lst)
        : m_data{}
    {
        if (lst.size() > ROWS)
        {
            throw std::runtime_error("too many rows provided");
        }
        IndexType i{ 0 };
        for (auto& l : lst) {
            if (l.size() > COLS)
            {
                throw std::runtime_error("one of the rows has to many values");
            }
            std::copy(l.begin(), l.end(), m_data.begin() + (ROWS * i));
            ++i;
        }
    }

    // copying the array directly is probably cheaper than using the () operator for each element
    Matrix(Matrix const&) = default;
    Matrix& operator=(Matrix const&) = default;

    template<SizedMatrixLike<T, ROWS, COLS> U>
    Matrix(U const& other)
    {
        *this = other;
    }

    T& operator()(IndexType column, IndexType row)
    {
        return m_data[ROWS * column + row];
    }

    T const& operator()(IndexType column, IndexType row) const
    {
        return m_data[ROWS * column + row];
    }

    template <SizedMatrixLike<T, ROWS, COLS> RHS>
    Matrix& operator=(const RHS& other) {
        auto out = m_data.begin();
        for (IndexType c = 0; c != COLS; ++c)
        {
            for (IndexType r = 0; r != ROWS; ++r)
            {
                *out = other(c, r);
                ++out;
            }
        }
        return *this;
    }

private:
    std::array<T, ROWS * COLS> m_data;
};

template<MatrixLike T, MatrixLike U>
requires (T::Rows == U::Rows) && (T::Columns == U::Columns)
AddExp<T, U> operator+(T const& s1, U const& s2)
{
    return AddExp(s1, s2);
}

int main()
{
    Matrix A{ {1, 0, 0}, {0, 1, 0}, {0, 0, 1} };

    Matrix B{ {1, 0, 0}, {0, 1, 0}, {0, 0, 1} };

    Matrix<int, 3, 3> C = A + B;
}

Я исхожу из фона С++ 98 и изучаю С++ 20 с нуля. Я еще не делал STL, поэтому дизайн моего класса все еще довольно наивен. Ваши заметки невероятно полезны.

Quasar 15.04.2023 16:00

Другие вопросы по теме