3
\$\begingroup\$

Suppose I have a class of complex numbers called Complex and I wish to implement a class of generic matrices with transpose operation.

doubles and ints don't require special care, but you have to calculate the conjugate of complex numbers to transpose. Therefore, I implemented a "specialized transpose()".

template <class T> 
Matrix<T> Matrix<T>::transpose() const
{
    Matrix<T> matrix(_cols, _rows);

    unsigned int i, j;

    for (i = 0; i < _rows ; ++i)
    {
        for (j = 0; j < _cols; ++j)
        {
            matrix(j, i) = (*this)(i, j);
        }
    }

    return matrix;
}

template <> 
Matrix<Complex> Matrix<Complex>::transpose() const
{
    Matrix<Complex> matrix(_cols, _rows);

    unsigned int i, j;

    for (i = 0; i < _rows ; ++i)
    {
        for (j = 0; j < _cols; ++j)
        {
            matrix(j, i) = (*this)(i, j).conj();
        }
    }

    return matrix;
}

As you can see, there's a severe code repetition. Is there any way I can deal with it (like using helper methods)?

References from Wikipedia:

\$\endgroup\$
4
  • 4
    \$\begingroup\$ Transpose and conjugate transpose are two different operations. The normal transposition of a complex matrix works just like normal transposition for a real/int matrix. \$\endgroup\$
    – Mat
    Commented Sep 7, 2015 at 12:48
  • \$\begingroup\$ Do you really need to make a new matrix and return it every time you do this operation? This seems very memory inefficient if you are dealing with large matrices. The other option is to simply transpose the class-matrix. \$\endgroup\$
    – Winther
    Commented Sep 7, 2015 at 13:27
  • \$\begingroup\$ Yes, in the school excerice we're reuqired not to change the object but return a new one. \$\endgroup\$
    – Alex Goft
    Commented Sep 7, 2015 at 13:53
  • \$\begingroup\$ By the way, specialized method - where to i implement them and how? (for example, declare in hpp and implement in cpp)? \$\endgroup\$
    – Alex Goft
    Commented Sep 7, 2015 at 13:56

2 Answers 2

1
\$\begingroup\$

Conjugate ints and doubles as well

Instead of having a generic template that does not conjugate its elements, and a specialization for Complex that does conjugate, just calculate the conjugate for all types. Of course, int and double don't have a .conj() member function, but this can be solved by creating an out-of-class conj() function that calls .conj() for Complex, and just returns the original value for other types (by assuming those are real numbers).

template <class T>
T conj(T value) {
    return value;
}

template <>
Complex conj(Complex value) {
    return value.conj();
}

template <class T>
auto Matrix<T>::transpose() const
{
    Matrix<T> matrix(_cols, _rows);

    for (unsigned i = 0; i < _rows; ++i) {
        for (unsigned j = 0; j < _cols; ++j) {
            matrix(j, i) = conjugate((*this)(i, j));
        }
    }

    return matrix;
}

Now there is no code duplication anymore, and you can even add support to conjugate other types, without having to change the transpose() function. You can also make it work like std::begin(), and have it call .conj() for all types who have that member function, and perhaps limit the version that just returns the input to the built-in number types. This is easy with C++20's concepts:

template <typename T>
concept conjugatable = requires (T x) {
    x.conj();
};

template <conjugatable T>
T conj(T value) {
    return value.conj();
}

template <typename T> requires std::is_arithmetic_v<T>
T conj(T value) {
     return value;
}

Note that there is also std::conj(), but it only works for std::complex. Overloading functions from the std:: namespace should not be done, but you can pull std::conj() into the global namespace by writing:

using std::conj;

And then calling conj() without std:: will then also work for std::complex, and thus your transpose() function will also be able to work with that type.

All the above can be combined with Toby Speight's answer.

\$\endgroup\$
5
  • 1
    \$\begingroup\$ I guess this is taking the other assumption to me (i.e. that Complex isn't an alias of some std::complex<> type). I'm not sure why anyone wouldn't use the standard library for a complex number, but this does show how to use the same technique for the non-standard version. BTW, it's probably worth mentioning that the using std::conj; can be local to the transpose() implementation - I've seen a propensity for declaring these aliases at file scope here on CR. \$\endgroup\$ Commented Dec 1, 2022 at 8:41
  • 1
    \$\begingroup\$ If using std::conj;, do we need to remove our std::is_arithmetic version, to avoid an ambiguous match? \$\endgroup\$ Commented Dec 1, 2022 at 8:43
  • 1
    \$\begingroup\$ @TobySpeight No. std::conj() always returns a std::complex, even if the input is a scalar. That's not what we want here. And the match will not be ambiguous; our own version will take precedence over the one we bring in with using. \$\endgroup\$
    – G. Sliepen
    Commented Dec 1, 2022 at 9:10
  • \$\begingroup\$ Ah, I missed that the return type isn't the same as the argument type! And thanks for confirming the lack of ambiguity - I really wasn't sure about that. \$\endgroup\$ Commented Dec 1, 2022 at 9:13
  • 1
    \$\begingroup\$ As for bringing the using into transpose(), that's a good point. But I think that would only make sense if our own definitions for conj() were also inside transpose(). If you don't want a ::conj() to be visible outside transpose(), maybe everything should be put into a detail namespace. \$\endgroup\$
    – G. Sliepen
    Commented Dec 1, 2022 at 9:13
1
\$\begingroup\$

It's hard to review this without the whole definition of Matrix, but I'll have a stab.

Firstly, I'd distinguish between transpose() and transposed(). The former would mutate an existing Matrix with no extra memory overhead, and the latter more like what we have here, creating a new Matrix without modifying the original:

template <class T> 
Matrix<T> Matrix<T>::transposed() const
{
    Matrix m{*this};
    m.transpose();
    return m;
}

The in-place transpose() would then just be

template<class T> 
auto& Matrix<T>::transpose() noexcept(std::is_nothrow_swappable_v<T>)
{
    for (std::size_t i = 0;  i < _rows;  ++i) {
        for (std::size_t j = i;  j < _cols;  ++j) {
            std::swap((*this)(j, i), (*this)(i, j));
        }
    }

    return *this;
}

I would implement the conjugation as a separate pass, and make it more generic, as there are other functions we'd like to apply to all elements:

template<class T>
template<typename Func>
auto& Matrix<T>::transform(Func f)
{
    std::ranges::transform(data, data.begin(), f);
}

Then we can use transform(std::conj) to form the complex conjugate of all the matrix elements (assuming that Complex is an alias of some instantiation of std::complex<>). And remember that std::conj() is well-defined for integer and floating-point types, so you could consider using it always¹.


¹ It's not as simple as that, because std::conj(IntegerOrDouble) returns a std::complex<double>, but we need the same type as the argument. See G. Sliepen's answer for a workable alternative.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.