2
\$\begingroup\$

I have created a class Complex which represents a complex number of any arbitrary arithmetic type. Overloads are given for the 4 common functions, addition, subtraction, multiplication, and division.

The overloads take two different template arguments so as to allow operating on complex numbers of different types together, and the decltype(Ty_a() + Ty_b()) is used to automatically determine the return type from the template parameters. How could I improve this code?

#include <type_traits>
#include <iostream>
template<
 typename Ty,
 typename std::enable_if<std::is_arithmetic<Ty>::value, int>::type = 0
>
class Complex
{
public:
 Complex() = default;
 Complex(const Ty &r, const Ty &i) noexcept :
 r(r), i(i)
 {}
 Ty real() const noexcept {return r;}
 Ty &real() noexcept {return r;}
 Ty imag() const noexcept {return i;}
 Ty &imag() noexcept {return i;}
private:
 Ty r, i;
};
template<typename Ty_a, typename Ty_b>
Complex<decltype(Ty_a() + Ty_b())> operator+ (const Complex<Ty_a> &a, const Complex<Ty_b> &b) noexcept
{
 return {a.real() + b.real(), a.imag() + b.imag()};
}
template<typename Ty_a, typename Ty_b>
Complex<decltype(Ty_a() - Ty_b())> operator- (const Complex<Ty_a> &a, const Complex<Ty_b> &b) noexcept
{
 return {a.real() - b.real(), a.imag() - b.imag()};
}
template<typename Ty_a, typename Ty_b>
Complex<decltype(Ty_a() * Ty_b())> operator* (const Complex<Ty_a> &a, const Complex<Ty_b> &b) noexcept
{
 typedef decltype(Ty_a() * Ty_b()) result_t;
 result_t real = a.real() * b.real() - a.imag() * b.imag();
 result_t imag = a.real() * b.imag() + b.real() * a.imag();
 return {real, imag};
}
template<typename Ty_a, typename Ty_b>
Complex<decltype(Ty_a() / Ty_b())> operator/ (const Complex<Ty_a> &a, const Complex<Ty_b> &b) noexcept
{
 typedef decltype(Ty_a() / Ty_b()) result_t;
 result_t denominator = b.real() * b.real() + b.imag() * b.imag();
 result_t real = (a.real() * b.real() + a.imag() * b.imag()) / denominator;
 result_t imag = (a.imag() * b.real() - a.real() * b.imag()) / denominator;
 return {real, imag};
}
template<typename Ty>
std::ostream &operator<< (std::ostream &stream, const Complex<Ty> &num)
{
 return stream << num.real() << '+' << num.imag() << 'i';
}
int main(int argc, char **argv)
{
 Complex<double> a(1.2, 2.25);
 Complex<int> b(2, -1);
 
 Complex<double> y = a / b;
 std::cout << y << std::endl;
}
Toby Speight
87.1k14 gold badges104 silver badges322 bronze badges
asked Sep 2, 2022 at 10:17
\$\endgroup\$
2
  • \$\begingroup\$ What does this do that std::complex doesn't? \$\endgroup\$ Commented Sep 2, 2022 at 15:20
  • \$\begingroup\$ @TobySpeight e.g., mixed complex types arithmetic \$\endgroup\$ Commented Sep 2, 2022 at 16:21

2 Answers 2

1
\$\begingroup\$

Is encapsulation useful?

There's an important question to ask about this class: is there any benefit to keeping the data members private? As a thought experiment, how would the following struct be used differently than what you wrote?

template<
 typename Ty,
 typename std::enable_if<std::is_arithmetic<Ty>::value, int>::type = 0
>
struct Complex
{
 Ty real, imag;
};
  1. Instead of Complex<double> a(1.2, 2.25), you would write Complex<double> a{1.2, 2.25}.
  2. Instead of a.real() and a.imag(), you would write a.real and a.imag.

In your class, the methods Ty Complex::real() const and Ty& Complex::real() (likewise for imag()) give complete, unrestricted access to the underlying variable. I am not saying this is a bad design. If you want the users of this class to freely modify the real and imaginary parts of the complex number, then what you wrote is correct. However, what I wrote above achieves the same result with much less code.

Effectively, you are encapsulating the innards of your Complex class by making the data members private and then breaking encapsulation by providing the non-const reference methods. Using a struct is simpler and achieves the same effect.

Making encapsulation useful

There's another way forward: make the class immutable by deleting all non-const methods.

template<
 typename Ty,
 typename std::enable_if<std::is_arithmetic<Ty>::value, int>::type = 0
>
class Complex
{
public:
 Complex() = default;
 Complex(const Ty &r, const Ty &i) noexcept :
 r(r), i(i)
 {}
 Ty real() const noexcept {return r;}
 Ty imag() const noexcept {return i;}
private:
 Ty r, i;
};

With this class, once a Complex number is created, it can never be modified. Immutable data types are easy to reason about since you never have to worry about the value of a variable changing. If the user needs a modified version of a Complex number, they can create a new one. Instead of

auto z = Complex(1, 1);
// ... more code ...
z.imag() = 2;

users can write

auto z = Complex(1, 1);
// ... more code ...
auto z2 = Complex(z.real(), 2);

One could argue that the second better expresses the intent of the programmer: "I want a Complex value with the real part of this other Complex number and an imaginary part equal to 2."

Which path you take depends on how you want the class to be used. As with most engineering questions, the correct answer is, "It depends."

Default Constructor

Your default constructor will leave both r and i uninitialized for the simple types of Ty--int, double, etc. I would delete the default constructor to force users to create a valid value for every constructed Complex number.

Templating

You've probably noticed how long the lines containing template functions can get. That's why there are helper functions for getting the ::type and ::value results. std::enable_if_t<> is equivalent to std::enable_if<>::type and std::is_arithmetic_v<> is equivalent to std::is_arithmetic<>::value.

template<
 typename Ty,
 typename std::enable_if_t<std::is_arithmetic_v<Ty>, int> = 0
>

Although, I would state the requirement more directly using static_assert.

template<typename Ty>
class Complex
{
 static_assert(std::is_arithmetic_v<Ty>, "Complex requires an arithmetic type.");
public:
 // etc.
};

Type juggling

In your operator overloads, you specify the return value as Complex<decltype(Ty_a() + Ty_b())> and similary for the other operations. A simpler name for the return type uses std::common_type.

template<typename Ty_a, typename Ty_b>
Complex<std::common_type_t<Ty_a, Ty_b>> operator+ (const Complex<Ty_a>& a, const Complex<Ty_b>& b) noexcept
{
 return {a.real() + b.real(), a.imag() + b.imag()};
}

Now, that's a little unwieldy to repeat for all the operations, so you can declare it once and reuse it.

template<typename Ty_a, typename Ty_b>
using op_return_type = Complex<std::common_type_t<Ty_a, Ty_b>>;
template<typename Ty_a, typename Ty_b>
op_return_type<Ty_a, Ty_b> operator+ (const Complex<Ty_a>& a, const Complex<Ty_b>& b) noexcept
{
 return {a.real() + b.real(), a.imag() + b.imag()};
}

Using auto to make life easier

You can use the auto keyword to let the compiler figure out what types should be.

template<typename Ty_a, typename Ty_b>
op_return_type<Ty_a, Ty_b> operator/ (const Complex<Ty_a>& a, const Complex<Ty_b>& b) noexcept
{
 auto denominator = b.real() * b.real() + b.imag() * b.imag();
 auto real = (a.real() * b.real() + a.imag() * b.imag()) / denominator;
 auto imag = (a.imag() * b.real() - a.real() * b.imag()) / denominator;
 return {real, imag};
}

Putting everything together

Here's what you code looks like using all of my suggestions (using the immutable option for Complex):

#include <type_traits>
#include <iostream>
template<typename Ty>
class Complex
{
 static_assert(std::is_arithmetic_v<Ty>, "Complex requires an arithmetic type.");
public:
 Complex(const Ty& r, const Ty& i) noexcept :
 r(r), i(i)
 {}
 Ty real() const noexcept { return r; }
 Ty imag() const noexcept { return i; }
private:
 Ty r, i;
};
template<typename Ty_a, typename Ty_b>
using op_return_type = Complex<std::common_type_t<Ty_a, Ty_b>>;
template<typename Ty_a, typename Ty_b>
op_return_type<Ty_a, Ty_b> operator+ (const Complex<Ty_a>& a, const Complex<Ty_b>& b) noexcept
{
 return {a.real() + b.real(), a.imag() + b.imag()};
}
template<typename Ty_a, typename Ty_b>
op_return_type<Ty_a, Ty_b> operator- (const Complex<Ty_a>& a, const Complex<Ty_b>& b) noexcept
{
 return {a.real() - b.real(), a.imag() - b.imag()};
}
template<typename Ty_a, typename Ty_b>
op_return_type<Ty_a, Ty_b> operator* (const Complex<Ty_a>& a, const Complex<Ty_b>& b) noexcept
{
 auto real = a.real() * b.real() - a.imag() * b.imag();
 auto imag = a.real() * b.imag() + b.real() * a.imag();
 return {real, imag};
}
template<typename Ty_a, typename Ty_b>
op_return_type<Ty_a, Ty_b> operator/ (const Complex<Ty_a>& a, const Complex<Ty_b>& b) noexcept
{
 auto denominator = b.real() * b.real() + b.imag() * b.imag();
 auto real = (a.real() * b.real() + a.imag() * b.imag()) / denominator;
 auto imag = (a.imag() * b.real() - a.real() * b.imag()) / denominator;
 return {real, imag};
}
template<typename Ty>
std::ostream& operator<< (std::ostream& stream, const Complex<Ty>& num)
{
 return stream << num.real() << '+' << num.imag() << 'i';
}
int main(int argc, char** argv)
{
 Complex<double> a(1.2, 2.25);
 Complex<int> b(2, -1);
 Complex<double> y = a / b;
 std::cout << y << std::endl;
}
answered Sep 2, 2022 at 14:16
\$\endgroup\$
7
  • 1
    \$\begingroup\$ requires is the modern, more readable alternative to std::enable_if. static_assert() is different, because it doesn't stop the function existing (which can be important with overloads). For the template itself, the short form is even simpler (template<arithmetic_type T>) if you're willing to define the arithmetic_type concept yourself (it's not provided in standard library). \$\endgroup\$ Commented Sep 2, 2022 at 16:26
  • 1
    \$\begingroup\$ I believe, const references are redundant in the constructor. Probably, other references may be removed too because Complex wraps only two arithmetic variables. \$\endgroup\$ Commented Sep 2, 2022 at 16:26
  • 1
    \$\begingroup\$ @Mark, member functions real() and imag() are consistent with std::complex, so there's an argument for keeping the familiar interface. \$\endgroup\$ Commented Sep 2, 2022 at 16:33
  • 1
    \$\begingroup\$ @finlaymorrison Encapsulation is a very good thing. Hiding the nitty-gritty details of a class simplifies the code that uses that class. The point of the first two sections was to get you to think about how ill-chosen methods can break encapsulation and how to either fix it (the immutable class) or do without it (the struct). I prefer the immutable class version. \$\endgroup\$ Commented Sep 2, 2022 at 16:45
  • 1
    \$\begingroup\$ @TobySpeight That's true about requires. I wasn't sure what version of C++ OP was using, so I stuck with C++11 constructs. I think static_assert works here because it stops compilation on a bad type and prints a useful error message. \$\endgroup\$ Commented Sep 2, 2022 at 16:47
1
\$\begingroup\$

std::is_arithmetic<> might be too restrictive. This prevents making complex numbers of bignum types, for example.

It's probably a good idea to define a Concept that requires the arithmetic operations you want to support, and use that. Something like (untested)

template<typename T>
concept arithmetic_for_complex =
 requires(T a, T b) {
 a + b;
 a * b;
 /* etc. */
 };
template<arithmetic_for_complex Ty>
class Complex;
answered Sep 2, 2022 at 16:31
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.