Как мне реализовать конструктор копирования и оператор присваивания для матричного класса?

У меня есть матричный класс с такими полями:

template <typename T>
class Matrix
{
private:
    T **matrix = nullptr;
    int rows; 
    int cols; 

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

Matrix(const Matrix &matrix_) : rows(matrix_.rows), cols(matrix_.cols)
        {
            matrix = static_cast<T **>(new T *[rows]);

            for (int i = 0; i < rows; i++)
            {
                matrix[i] = static_cast<T *>(new T[cols]);
            }
            for (int i = 0; i < rows; i++)
            {
                for (int j = 0; j < cols; j++)
                {
                    matrix[i][j] = matrix_[i][j];
                }
            }
        }

        Matrix &operator=(const Matrix &matrix_)
        {
            if (&matrix == this)
            {
                return *this;
            }
            clean();

            rows = matrix_.rows;
            cols = matrix_.cols;

            matrix = static_cast<T **>(new T *[rows]);

            for (int i = 0; i < rows; i++)
            {
                matrix[i] = static_cast<T *>(new T[cols]);
            }
            for (int i = 0; i < rows; i++)
            {
                for (int j = 0; j < cols; j++)
                {
                    matrix[i][j] = matrix_[i][j];
                }
            }
        }

        void clean()
        {
            if (cols > 0)
            {
                for (int i = 0; i < rows; i++)
                {
                    delete[] matrix[i];
                }
            }
            if (rows > 0)
            {
                delete[] matrix;
            }
        }

По условию задания запрещено использовать STL-контейнеры, я должен сам реализовать элементы управления

Добавлена ​​семантика перемещения

Matrix(Matrix &&other) noexcept : rows(std::move(other.rows)), cols(std::move(other.cols)), data(new T(rows * cols))
{
    other.data = nullptr;
    rows = 0;
    cols = 0;
}

Matrix &operator=(Matrix &&other) noexcept
{

    if (&other == this)
    {
        return *this;
    }

    if (rows != other.rows && cols != other.cols)
    {
        std::cout << "Error assigning matrices of different sizes" << std::endl;
        exit(-1);
    }

    clean();

    std::swap(data, other.data);
    std::swap(rows, other.rows);
    std::swap(cols, other.cols);

    return *this;
}

Вам лучше использовать одномерный массив, к которому вы используете T& operator()(size_t row, size_t col) { return matrix[row * cols + col]; } для доступа. Копирование одного из них, вероятно, настолько быстро, насколько это возможно. Используйте vector<T> вместо matrix, и вы получите большую часть бесплатно.

Ted Lyngmo 15.05.2022 18:41

Вам действительно нужно менять столбцы и строки во время выполнения? Если нет, рассмотрите возможность их объявления в параметрах шаблона и используйте std::array, вам не понадобится настраивать конструктор копирования и оператор присваивания.

273K 15.05.2022 18:47

Есть ли у вас причина использовать ручное управление памятью в классе Matrix? Это какое-то задание (без каламбура) или что-то в этом роде? Если нет, то предложение @ted имеет много достоинств.

Paul Sanders 15.05.2022 18:48

Как и @ 273K, если на то пошло, но по другим причинам.

Paul Sanders 15.05.2022 18:49

Да, это задача. Преподаватель запретил использовать контейнеры из STL, приходится самому реализовывать управление ресурсами

Елизавета Тараненко 15.05.2022 18:52

Хорошо, тогда используйте указатель один. Вы даже можете создать небольшой класс интеллектуальных указателей для управления памятью.

Ted Lyngmo 15.05.2022 18:53

Почему один, а не двойной? А как насчет оператора присваивания и конструктора присваивания. Они должны быть очень похожи (включая дублирование кода)

Елизавета Тараненко 15.05.2022 18:54

Ваш конструктор копирования может вызвать ваш оператор присваивания (как operator=), если вы хотите, и если предварительные условия верны. В этом помогает инициализация ваших переменных-членов в момент их объявления.

Paul Sanders 15.05.2022 18:58

@ЕлизаветаТараненко Пожалуйста, проверьте мой ответ. Я думаю, это то, что вы хотите. Я отредактировал, чтобы использовать ручное управление памятью вместо std::vector Конечно, понадобятся конструкторы и деструкторы...

simre 15.05.2022 19:00
"Почему один, а не двойной?" — потому что это медленнее и гораздо более подвержено ошибкам.
Ted Lyngmo 15.05.2022 19:01

Не похоже, что эти static_cast нужны. Приведения должны заставить вас заподозрить, что вы делаете что-то не так.

Pete Becker 15.05.2022 19:01

Указана ли в присваивании семантика присваивания матриц для матриц разной размерности?

einpoklum 15.05.2022 19:40

@einpoklum, ты имеешь в виду не только квадратные матрицы? Если так, то да, матрицы могут быть разного размера

Елизавета Тараненко 15.05.2022 19:50

@ЕлизаветаТараненко: Я имею в виду, что должно произойти, если вы присвоите матрице размера 10x20 матрицу размера 1x1? Это должно потерпеть неудачу, или целевая матрица теперь должна стать матрицей 10x20?

einpoklum 15.05.2022 19:53

@ЕлизаветаТараненко Что должно произойти, если вы назначите матрицу 2x3 матрице 3x2?

Goswin von Brederlow 15.05.2022 19:53

Я еще не обработал эту ошибку, но я думаю, что ошибка должна быть поднята

Елизавета Тараненко 15.05.2022 19:59

Зачем вам использовать семантику перемещения с примитивами? Это не имеет никакого смысла.

jabaa 15.05.2022 20:58

Если вам требуются матричные назначения только между значениями с одинаковым количеством строк/столбцов, это сделает такие вещи, как помещение их в вектор, непрактичным, если только каждая матрица, помещенная в вектор, не имеет одинакового размера. Вы можете сделать размеры неизменными, поместив строки и столбцы в шаблон. Тогда все будет работать, так как разные размеры — это разные классы.

doug 15.05.2022 21:04

@Doug Этого недостаточно. Также требуется знать размер матрицы во время компиляции...

simre 15.05.2022 21:10

Существует явный сайт для проверки кода codereview.stackexchange.comНО обратите внимание, что сначала код должен работать. Поэтому, если вы можете протестировать свой код с помощью модульных тестов, и он работает должным образом, вы можете проверить его там. Хотя вы, вероятно, захотите просмотреть весь класс, а не только его часть.

Martin York 15.05.2022 21:23

@simre Меня беспокоило то, что без возможности назначать матрицы с разными строками / столбцами такие вещи, как std::swap(mat1, mat2);, не будут работать. И это используется вектором.

doug 15.05.2022 23:48
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать 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
21
88
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

Тогда используйте одномерный массив. Это будет намного чище, проще и быстрее, чем массив указателей на массивы... Вы можете сделать что-то вроде:

template <typename T>
class Matrix
{
private:
    // Note: Assuming T is a trivial type, most likely a fundamental type...

    T* data; // Flattened matrix with size = rows*cols. Be careful,
             // for really big matrices this multiplication could overflow!!!

    // You can use unsigned type since negative size is a nonsense...
    unsigned int rows;
    unsigned int cols;

public:
    Matrix(const Matrix& other) 
        : data(new T[other.rows*other.cols])
        , rows(other.rows)
        , cols(other.cols)
    {
        /// You can do this:
        /// for(int i = 0; i < rows*cols; ++i)
        ///    data[i] = other.data[i];
        /// or simply call the copy assign operator:
        operator=(other);
    }

    Matrix& operator=(const Matrix& other)
    {
        // This is only for matrix with the same dimensions.
        // Delete and allocate new array if the dimensions are different. 
        // This is up to the OP if he can have differently sized matrices or not...
        // Also assert will "disappear" if NDEBUG (ie. in release) is defined.
        assert(rows == other.rows);
        assert(cols == other.cols);
        for(int i = 0; i < rows*cols; ++i)
            data[i] = other.data[i];
        return *this;
    }
};

Примечание. Вам также нужно будет определить конструктор (ы) и деструктор, конечно... Я уверен, что вы не можете сделать это намного проще, чем это...

Спасибо! Но у меня есть небольшой вопрос, поскольку у нас есть только один указатель, то функция очистки (для деструктора и, возможно, будущего оператора перемещения конструктора перемещения) будет выглядеть так: if (nullptr != data) delete[] data;

Елизавета Тараненко 15.05.2022 19:15

@ЕлизаветаТараненко Вам не нужно проверять, является ли это nullptr. Просто delete[] data; В назначении перемещения вы можете просто std::swap указатели двух контейнеров. Не надо delete[].

Ted Lyngmo 15.05.2022 19:16

@ЕлизаветаТараненко delete[] ничего не будет делать на nullptr. Вы можете смело назвать это nullptr. @TedLyngmo Он/она не может использовать std::swap и я думаю...

simre 15.05.2022 19:17

@simre Вероятно, это правда, но понять, как это работает, и написать аналогичную функцию не так уж сложно.

Ted Lyngmo 15.05.2022 19:20

@ЕлизаветаТараненко Для clean, если он очищает весь объект, вы также должны сбросить cols и rows на 0. Не оставляйте объект в недопустимом состоянии. Это может вызвать головную боль позже довольно легко ...

simre 15.05.2022 19:23

Ваш оператор = будет волей-неволей копировать из матрицы n x 2m в матрицу 2n x m.

einpoklum 15.05.2022 19:35

@simre: Но это было просто указание на более глубокую проблему. Почему вы считаете, что матрицы разных размерностей не должны быть присваиваемыми, если размерности не являются константными элементами? ОП не использовал эту семантику.

einpoklum 15.05.2022 19:40

@simre Просто имейте в виду, что assert удаляются в релизных сборках (если NDEBUG определено). Если размеры не совпадают и размер выделенной вами памяти не тот же (или, возможно, просто меньше), вы можете просто delete[] выделить старую выделенную память и выделить то, что необходимо для копирования другой Matrix

Ted Lyngmo 15.05.2022 19:42

@einpoklum Если вы переместите матрицу 2x3 в матрицу 3x2, то каким должно быть состояние элементов, отсутствующих в исходной матрице? Что делать с элементами, которым нет места в целевой матрице?

Goswin von Brederlow 15.05.2022 19:44

@GoswinvonBrederlow: вы бы изменили размеры и при необходимости перераспределили. Это означает, что вы также можете назначить матрицу 10x20 матрице 1x1... конечно, это вопрос того, что было запрошено OP.

einpoklum 15.05.2022 19:48

@einpoklum Изменение размеров также возможно, и да, тогда вам придется перераспределить данные. Я понимаю Матрицу как математический объект Матрицы, и тогда я полностью согласен с утверждениями. Для двумерного массива без математического смысла все может быть по-другому.

Goswin von Brederlow 15.05.2022 19:52

«Я в полном порядке» - но учителя ОП могут быть не такими. Так что, по крайней мере, ваш ответ должен пояснить, что здесь должен быть сделан явный выбор дизайна.

einpoklum 15.05.2022 19:55

@simre: Твоя проблема в том, что ты думаешь как математик. Это StackExchange, сайт программирование, и OP получил назначение программирование. По вашей логике std::vector нельзя назначать друг другу, если их размеры различны.

einpoklum 15.05.2022 20:19

Для полноты я добавил реализацию семантики перемещения (для меня это новый мир). Можете ли вы оценить и сказать, что не так и что можно исправить?

Елизавета Тараненко 15.05.2022 20:58

@ЕлизаветаТараненко В конструкторе перемещения вам не нужно move, так как вы имеете дело с фундаментальными типами и указателями. Также вам не нужно выделять память в конструкторе перемещения. Вы просто «воруете». Копируешь с другой Матрицы, а потом другую ставишь как сделал. При назначении перемещения, если clean() устанавливает nullptr, а cols, rows=0, то вроде нормально. Также вы можете проверить std::exchange, но это может запутать некоторых людей...

simre 15.05.2022 21:02

Не понимаю, почему у этого -4 голоса. Это может быть не идеально, но делает то, что просил ОП.

Martin York 15.05.2022 21:10

@MartinYork Никто не знает. :D Только те, кто пытался убедить меня реализовать логическую ошибку... :D

simre 15.05.2022 21:11

@simre: это не дает сильной гарантии исключения. Он не использует стандартный шаблон копирования и подкачки. Это очень неэффективно, потому что каждый элемент в массиве создается, а затем присваивается (что еще важнее, T нетривиален). Но он работает так, как ожидалось.

Martin York 15.05.2022 21:15

Как правило, если вам нужен общий код между методами, лучший способ — разделить общий код на (частные) вспомогательные методы. Например, у вас может быть метод alloc_, который имеет цикл, вызывающий new, и free_, который вызывает delete. Тогда вы могли бы:

Matrix(const Matrix &a) : rows(a.rows), cols(a.cols) {
    alloc_();
    copy_(a);
}
Matrix &operator=(const Matrix &a) {
    free_();
    rows = a.rows;
    cols = a.cols;
    alloc_();
    copy_(a);
}
~Matrix() { free_(); }

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