16

I was looking at the C++ library <complex>, and noticed that functions such as std::conj and std::norm are free functions i.e. static functions not placed inside the std::complex class. Why is this the case?

I would've thought that, from a C++ OOP design perspective, it would've made more sense to have e.g. complex<T> complex<T>::conj() and complex<T> complex<T>::norm() as methods so that I can call auto norm = z.norm() instead of auto norm = std::norm(z).

Am I missing something about how the standard library is designed which justifies why these functions are free?

5
  • 4
    Have you tried compiling auto znorm = norm(z)? The result might surprise you.
    – Sneftel
    Aug 25 at 18:22
  • Related SO question: stackoverflow.com/q/5989734/1497199
    – Dave
    Aug 25 at 21:37
  • @Sneftel I have tried and it works just fine on my compiler. Am I missing something?
    – Gary Allen
    Aug 27 at 9:04
  • No, that's the point. Due to ADL, you don't need the std:: prefix.
    – Sneftel
    Aug 27 at 9:22
  • @Sneftel interesting! however I'd never right code like that, haha
    – Gary Allen
    Aug 27 at 11:42
40

Am I missing something about how the standard library is designed which justifies why these functions are free?

The C++ standard library does not exclusively follow the OO design paradigm.

Free functions, when combined with parameter overloading, play much nicer when you are writing templated code that should work with both class types and primitive types.

For example, suppose I have a list of values (either complex, as std::complex<float>, or real, as float) and I want to compare them on magnitude. Then I can write a comparison function like

template <class T>
bool magnitude_less(const T& lhs, const T& rhs)
{
  using std::abs;
  return abs(lhs) < abs(rhs);
}

Writing such a function would not be possible if abs for std::complex<T> would have been a member function based on OO design principles.

7
  • 19
    Argument-dependent lookup is a subtle C++ feature, but absolutely essential for writing generic code like this. And it only works with free functions. The alternative design C++ could have used would have been to add methods like abs() to primitives like floats and ints as well (the design that Rust later chose).
    – amon
    Aug 25 at 10:34
  • 25
    Also worth noting that the C++ Core Guidelines recommend to only use member functions if you need access to private members or need your function to be virtual. For std::complex, the public interface is "this class has a real and imaginary part", and all functionality can be implemented with only that public interface, so we don't need any more member functions. Aug 25 at 18:20
  • 2
    @SilvioMayolo - it's worth noting that many classes in the C++ standard library predate the hard-won design practices that were eventually learned and so don't follow this recommendation. The most often used example (because it is the most egregious offender) being std::string of course.
    – davidbak
    Aug 26 at 0:00
  • 5
    @davidbak: Always loved Herb's Monolith Unstrung for this. std::string is a monstrosity. Aug 26 at 6:48
  • 3
    Well, if we had a complex<T>::abs(), that just wraps std::abs(*this), then your magnitude_less() implementation would still work, but something like z.abs() would also be possible.
    – Kaiserludi
    Aug 26 at 16:42
9

There is a school of though that prefers "non-member non-friend" functions, and justifies it in terms of improving code cohesion. I believe the original source for the idea is Effective C++ by Scott Meyers. The example in Bart's answer is a concrete demonstration of how this plays out in practice.

8

std::complex represents complex numbers, which is a mathematical concept. In mathematics it is customary to write f(x) instead of x.f(). So for more numerical or mathematical code, it is more natural to write norm(z) instead of z.norm(). (Also, there was and is lots of numerical code in C and Fortran, where the syntax goes norm(z).)

(This is in addition to the reasons stated in other answers.)

1
  • 1
    Yeah, it makes things consistent with how C++ treats basic numeric types like int and double: You write abs(n), not n.abs().
    – dan04
    Aug 27 at 0:28
1

For most users it would be nicer if C++ did the OOP thing(including giving methods to int, float..., but without making everything inherit from some MagicRootObject).

Unfortunately there is a strong dislike for member functions(Herb Sutter even had a GOTW where he bashes 100+ string member functions, as if millions of developers that use string have to implement them instead of use them).

For one example of this usability horror you could check out How to trim a std::string? At least in C++23 string will get a contains member function, only 25 years too late.

Additionally your example is trivial since function has 1 argument, but you can see errors that explode with useless error spam as soon as you use something more complex.

Sure this is technically possible even with member functions, but compiler has an easier time giving you nicer error message since one argument(object instance) is known.

1

The basic argument here is that cohesion and reusability is improved if you don't create member functions that don't require access to private data. i.e. can be implemented efficiently using only member functions.

Personally, I think C++ took that philosophy a bit too far, but I also think most libraries take it a bit too far the other way.

For example, consider finding the complex number with the largest magnitude from a list of complex numbers. You can't make this a member function of Complex because it operates on a list. You can make max a member function of List, but it doesn't know how to compare complex numbers. So you have some sort of Ordering interface or type class. You can't just make Complex implement this interface, because of the ISP, but also because you might want to also be able to order a Complex by real component or imaginary component sometimes instead of magnitude.

So where do you put all the Ordering implementations for Complex? Now you have a place that's separate from the Complex class, but still "belongs" to the Complex class.

Most OO programmers don't find that separation strange, because they were syntactically forced into it. But they don't think twice about clumping things together when they aren't forced to separate them, even when there might be a different, more cohesive grouping.

1
  • 1
    There is also only one place class-members can be added, namely in the class-definition, but any number of places the interface can be added to with free functions. Aug 27 at 14:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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