операторы класса шаблона c ++ - PullRequest
1 голос
/ 01 сентября 2011

Я учу себя на С ++, создав свой собственный класс структуры данных (точнее, матрицу), и я изменил его на шаблонный тип типа <T>, использующий только double. Перегруженные матричные операторы были довольно стандартными

    // A snippet of code from when this matrix wasn't a template class
    // Assignment
    Matrix& operator=( const Matrix& other );

    // Compound assignment
    Matrix& operator+=( const Matrix& other ); // matrix addition
    Matrix& operator-=( const Matrix& other ); // matrix subtracton
    Matrix& operator&=( const Matrix& other ); // elem by elem product
    Matrix& operator*=( const Matrix& other ); // matrix product

    // Binary, defined in terms of compound
    Matrix& operator+( const Matrix& other ) const; // matrix addition
    Matrix& operator-( const Matrix& other ) const; // matrix subtracton
    Matrix& operator&( const Matrix& other ) const; // elem by elem product
    Matrix& operator*( const Matrix& other ) const; // matrix product

    // examples of += and +, others similar
    Matrix& Matrix::operator+=( const Matrix& rhs )
    {
        for( unsigned int i = 0; i < getCols()*getRows(); i++ )
        {
            this->elements.at(i) += rhs.elements.at(i);
        }
        return *this;
    }

    Matrix& Matrix::operator+( const Matrix& rhs ) const
    {
        return Matrix(*this) += rhs;
    }

Но теперь, когда у Matrix может быть тип, у меня возникли проблемы с определением, какая из ссылок на матрицу должна иметь тип <T> и каковы будут последствия. Должен ли я позволить разным типам работать друг на друга (например, Matrix <foo> a + Matrix <bar> b действителен)? Я также немного размышляю о том, как

Одна из причин, по которой меня интересуют разнородные типы, - это упрощение использования комплексных чисел в будущем. Я новичок в C ++, но я рад окунуться в учебу. Если вы знакомы с любыми бесплатными онлайн-ресурсами, которые занимаются этой проблемой, я нашел бы это наиболее полезным.

Редактировать: неудивительно, что никто не думал, что в этом есть смысл, все мои угловые скобки в теле рассматриваются как теги! Я не могу понять, как их избежать, поэтому я встроу их в код.

Ответы [ 5 ]

2 голосов
/ 01 сентября 2011

Я полагаю, что мне следует проиллюстрировать мой комментарий о параметризации размеров матрицы, поскольку вы, возможно, не видели эту технику раньше.до 1.0, как:

Matrix<float, 2, 4> testArray(1.0);

Обратите внимание, что не требуется, чтобы хранилище находилось в куче (то есть, используя operator new), так как размер фиксирован.Вы можете разместить это в стеке.

Вы можете создать еще пару матриц int s:

Matrix<int, 2, 4> testArrayIntA(2);
Matrix<int, 4, 2> testArrayIntB(100);

Для копирования размеры должны совпадать, хотя типы не совпадают:

Matrix<float, 2, 4> testArray2(testArrayIntA); // works
Matrix<float, 2, 4> testArray3(testArrayIntB); // compile error
// No implementation for mismatched dimensions.

testArray = testArrayIntA; // works
testArray = testArrayIntB; // compile error, same reason

Умножение должно иметь правильные размеры:

Matrix<float, 2, 2> testArrayMult(testArray * testArrayIntB); // works
Matrix<float, 4, 4> testArrayMult2(testArray * testArrayIntB); // compile error
Matrix<float, 4, 4> testArrayMult2(testArrayIntB * testArray); // works

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

Изменение размера

Если вы не знаете размеры вашей матрицы во время компиляции, но должны подождать до времени выполнения, этот код может оказаться бесполезным.Вам нужно написать класс, который будет внутренне хранить измерения и указатель на фактические данные, и он должен будет делать все во время выполнения.Подсказка: напишите свой operator [], чтобы рассматривать матрицу как измененный вектор 1xN или Nx1, и используйте operator () для выполнения многоиндексных обращений.Это связано с тем, что operator [] может принимать только один параметр, но operator () такого ограничения не имеет.Легко выстрелить себе в ногу (по крайней мере, заставить оптимизатор сдаться), пытаясь поддерживать синтаксис M[x][y].

При этом, если есть какое-то стандартное изменение размера матрицы, которое вы делаете дляизмените размер одного Matrix на другой , учитывая, что все измерения известны во время компиляции , тогда вы можете написать функцию для изменения размера.Например, эта шаблонная функция преобразует любой Matrix в вектор-столбец:

template<class T, size_t NRows, size_t NCols>
Matrix<T, NRows * NCols, 1> column_vector(const Matrix<T, NRows, NCols>& original)
{   Matrix<T, NRows * NCols, 1> result;

    for(size_t i = 0; i < NRows; ++i)
        for(size_t j = 0; j < NCols; ++j)
            result.data[i * NCols + j][0] = original.data[i][j];

    // Or use the following if you want to be sure things are really optimized.
    /*for(size_t i = 0; i < NRows * NCols; ++i)
        static_cast<T*>(result.data)[i] = static_cast<T*>(original.data)[i];
    */
    // (It could be reinterpret_cast instead of static_cast. I haven't tested
    // this. Note that the optimizer may be smart enough to generate the same
    // code for both versions. Test yours to be sure; if they generate the
    // same code, prefer the more legible earlier version.)

    return result;
}

... ну, я думаю, что это вектор-столбец, в любом случае.Надеюсь, очевидно, как это исправить, если нет.В любом случае оптимизатор увидит, что вы возвращаете result, и удалит лишние операции копирования, в основном построив результат именно там, где его хочет видеть вызывающий объект.

Проверка корректности измерения во время компиляции

Допустим, мы хотим, чтобы компилятор остановился, если размерность равна 0 (обычно это приводит к пустому Matrix).Я слышал трюк под названием «Утверждение времени компиляции», который использует специализацию шаблонов и объявляется как:

template<bool Test> struct compiler_assert;
template<> struct compiler_assert<true> {};

Это позволяет вам писать код, такой как:

private:
    static const compiler_assert<(NRows > 0)> test_row_count;
    static const compiler_assert<(NCols > 0)> test_col_count;

Основная идея состоит в том, что, если условие true, шаблон превращается в пустой struct, который никто не использует, и молча отбрасывается.Но если это false, компилятор не может найти определение для struct compiler_assert<false> (просто объявление , которого недостаточно) и выдает ошибки.

Лучше версия Андрея Александреску (из его книги ), которая позволяет использовать объявленное имя объекта утверждения в качестве сообщения об импровизированной ошибке:

template<bool> struct CompileTimeChecker
{ CompileTimeChecker(...); };
template<> struct CompileTimeChecker<false> {};
#define STATIC_CHECK(expr, msg) { class ERROR_##msg {}; \
    (void)sizeof(CompileTimeChecker<(expr)>(ERROR_##msg())); }

Для чего вы заполняетеmsg должен быть действительным идентификатором (только буквы, цифры и символы подчеркивания), но это не так уж важно.Затем мы просто заменим конструктор по умолчанию:

Matrix()
{   // `data` gets its default constructor, which for simple types
    // like `float` means uninitialized, just like C.
    STATIC_CHECK(NRows > 0, NRows_Is_Zero);
    STATIC_CHECK(NCols > 0, NCols_Is_Zero);
}

И вуаля, компилятор остановится, если мы по ошибке установим одно из измерений на 0.О том, как это работает, см. Стр. 25 Книга Андрея .Обратите внимание, что в случае true сгенерированный код отбрасывается до тех пор, пока у теста нет побочных эффектов, поэтому нет раздувания.

1 голос
/ 01 сентября 2011
Matrix<double> x = ...;
Matrix<int> y = ...;
cout << x + y << endl; // prints a Matrix<double>?

ОК, это выполнимо, но проблема быстро усложняется.

Matrix<double> x = ...
Matrix<complex<float>> y = ...
cout << x + y << endl; // Matrix<complex<double>>?

Скорее всего, вы будете счастливее, если вам потребуется, чтобы ваши бинарные операторы использовали операнды подобного типа и заставляли сборщиков приложенийявно привести тип их значений.Для последнего случая:

cout << ((Matrix<complex<double>>) x) + ((Matrix<complex<double>>) y) << endl;

Вы можете предоставить конструктор шаблона элемента (или оператор преобразования типа) для поддержки преобразований.

template <typename T>
class Matrix {
   ...
public:
   template <typename U>
   Matrix(const Matrix<U>& that) { 
       // initialize this by performing U->T conversions for each element in that
   }
   ...
};

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

1 голос
/ 01 сентября 2011

Я не уверен, что понимаю, о чем вы спрашиваете.

Но я хотел бы отметить, что объявления вашего оператора неверны и / или неполны.

Во-первых, оператор присваивания долженвернуть тот же тип, что и его параметр;а именно:

const Matrix & operator = (const Matrix & src);

Во-вторых, бинарные операторы возвращают новый объект , поэтому вы не можете вернуть ссылку.Все бинарные операторы должны быть объявлены следующим образом:

Matrix operator+( const Matrix& other ) const; // matrix addition
Matrix operator-( const Matrix& other ) const; // matrix subtracton
Matrix operator&( const Matrix& other ) const; // elem by elem product
Matrix operator*( const Matrix& other ) const; // matrix product

На самом деле считается, что лучше объявить и реализовать бинарные операторы как глобальные функции-друзья:

class Matrix { ... };

inline Matrix operator+(const Matrix& lhs,const Matrix& rhs)
{ return Matrix(lhs)+=rhs; }

Надеюсь, это поможет.


Теперь я понимаю, о чем вы спрашиваете.

Предположительно, ваша реализация различных операторов в этом случае будет состоять из операций над составными типами.Поэтому на самом деле вопрос о том, имеет ли значение Matrix op Matrix, зависит от того, имеет ли значение строка op int (и может ли такая вещь быть полезной).Вам также необходимо определить тип возвращаемого значения.

Если предположить, что возвращаемый тип совпадает с операндом LHS, объявления будут выглядеть примерно так:

template <typename T>
class Matrix
{
    template <typename U>
    Matrix<T>&  operator+=(const Matrix<U>& rhs);
};

template <typename T,typename U>
Matrix<T> operator+(const Matrix<T>& lhs,const Matrix<U>& rhs)
{ return Matrix<T>(lhs)+=rhs; }
0 голосов
/ 01 сентября 2011

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

template <typename T> struct Foo
{
  Foo<T> bar(const Foo<T> &);
  Foo bar2(const Foo *);       // same
};

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

temlate <typename T> class Matrix
{
  template <typename U> Matrix(const Matrix<U> &);  // construct from another matrix
  /*...*/
};

Используя этот конструктор преобразования, вы можете смешивать матрицы в своих операторах, поскольку Matrix<T>::operator+(Matrix<U>) будет использовать преобразование для создания аргументавведите Matrix<T>, а затем вы используете уже реализованный оператор.

В C ++ 11 вы можете добавить static_assert(std::is_convertible<U, T>::value, "Boo"); в конструктор преобразования, чтобы дать вам полезную диагностику во время компиляции, если вы вызываете его с помощьюнесовместимый тип.

0 голосов
/ 01 сентября 2011

Прежде всего, оператор присваивания копии не должен иметь const Matrix& в качестве типа возврата; ваш интерфейс правильный.

Предложение Гранта о том, как реализованы бинарные операторы, является общепринятым способом сделать это.

Это хорошее упражнение, но быстро понятно, почему начинать линейную алгебру в C ++ - плохая идея для начала. Операции типа A+B и A*B действительны, только если размеры матриц совпадают.

...