Ravyne, on 16 Feb 2015 - 11:59 AM, said:
For me, this pretty much means I only have member functions that mutate the state of the object in some way and I usually even do this, for consistency, even when I might be able to implement the mutating member functions as free functions because the object leaves everything exposed
Are you talking about non-member friend functions here? Since this is the only case I can think of when a object is exposed like this
If you have a class that's a Plain-Old-Data type (or POD), it exposes all its members publicly, like the classic notion of a struct. You can implement essentially all its functions then as non-member-non-friend, because it doesn't hide anything. AFAIK, you'd still need to declare/define/default/delete its constructor inside the class though. Anyways, when I have a class like this -- like say a matrix or vector class -- I could just make everything non-member-non-friend, but I *still* prefer to make my mutating operators like *=. /=, +=, -=, =, etc to be members. It goes against the general advice of "make everything you possibly can a non-member, non-friend function", but I prefer the consistency of my choice, because most classes aren't POD types, and so you usually can't make the mutating operations non-members.
SeanMiddleditch, on 16 Feb 2015 - 11:17 AM, said:
consistency (if a user adds a new math function it must be a non-member, which makes it a second-class citizen compared to the member versions)
Can you expand on this. I'm a little lost when you say functions that are non-members are second class citizens
If I can be as bold as trying to answer for Sean...
Non-member-non-friend functions are *not* second-class citizens as long as they are in the same namespace as the class they relate too. Koenig Lookup ensures that these non-member, non-friend functions participate in things like overload resolution exactly as much as member functions. This means that to code that interfaces with your class, a member cuntion and a non-member, non-friend function in the same namespace are equals, neither one is secondary to the other. The only difference is that if your class has protected or private data or methods, then only the member function will have access to them -- however, you could also have a non-member friend function that would have access, just like the member. All of this is one of the larger reasons (but not the only reason) that the axiom is to prefer non-member, non-friend functions -- by doing this, you limit the amount of code that could potentially, by mistake, cause a mutation it didn't intend -- for example, by calling a private member function that appears innocuous, but silently mutates the state of the object.
Its an axiom in the same vein as making things const-correct -- by preferring things to be const whenever they can, or non-member-non-friend whenever they can, you prevent yourself (or those who come to your code after you) from making silly mistakes that could lead to some very difficult-to-track-down bugs, and it also documents the logical contract that you intend your class and its interface to uphold.
Getting/Setting the identity of a matrix is abstract. You don't need a matrix to work on, you just need to fill certain spots with a 1 or a 0. So with that being said, making the Identity function a member function kind of seems silly (do I really need to have a matrix object to return the identity of said matrix type?).
How else do you intend to represent the matrix?
That's a serious question, actually. If you want some other object to represent the identity matrix and you can make the algebra work out, you might have good reason to do so. But in general, you've already got a perfectly good representation of a matrix, so maybe just use that... Whether you do or don't is just another one of those considerations we have to balance. If I were to go down the path of alternate representation myself, never having done so before, I would probably look at an implementation involving a type_traits class, because I suspect that would give the abstract representation of an identity matrix the flexibility to specialize (optimize) specific operations without being beholden to implement an entire parallel algebra.
Now if you make it a non-member function then you run into an issue where the compiler does not know what to do on the return type alone (Identity function does not need a parameter in order to do its job). Now at this point to me, this is where it seems to fit the member function mold, but then again it feels super wrong to do this based on the above. So it leaves me kind of in the dark as to what to do?
Sure, you can make it a member function if you like, a static one, probably. Or, you could name your non-member, non-friend IdentityMatrix3x3f instead of Matrix3x3f.Identity. Type traits (as I put forward in the last part) can solve this problem perfectly well. Or a free-standing template function Identity<T>() can too, for that matter (this is really just a lighter-weight type-traits-style approach). Or, you could take a dummy parameter of the type you want, which is probably not what you want in this specific case, but is a useful pattern otherwise, and is known as tagged-dispatch.
The world is your oyster.
The general case of your problem-statement is "I have a function or data I want to associate with a type, but there's no place to name the type on it." When faced with that, there's no other way than to find a way to name the type -- you can do that by attaching it to the type (as by making it a member), or by giving the type explicitly to the compiler (as in type_traits or free-standing template), or via tagged dispatch which is similar to the template approach, except that it relies on overload resolution rather than template parameters.
Do I just live with the wrongness and make it a member funciton?
Just because some good advice is unassailable 98% of the time, doesn't mean you're bad for questioning it the other 2%. There's a reason good advice is repeated -- it works great most of the time. But the same advice in a different situation can be 'meh' or even terrible advice. If you ever find yourself cornered by a wolf, standing tall and trying to appear aggressive might be good advice; if you ever find yourself cornered by a large, angry man holding a lead pipe, probably not so much. Context matters.
If you try to do the right thing all the time, and manage to do the right thing most of the time, you're probably OK, and I wouldn't advise you to lose sleep over it at any rate.