Я хотел бы разработать класс 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;
}
Я вижу две проблемы: присваивание против инициализации и константность.
В вашем main
у вас есть:
Matrix<int, 3, 3> C = A + B;
Для этого требуется конструктор Matrix
, который принимает const RHS& exp
. Измените его на:
Matrix<int, 3, 3> C;
C = A + B;
Ваш 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, поэтому дизайн моего класса все еще довольно наивен. Ваши заметки невероятно полезны.