LTD: Non-member VS Memeber functions

Started by
5 comments, last by SmkViper 9 years, 2 months ago

I am currently rounding out some functionality for my libraries and I'm trying to make them as complete / professional as possible.

And recently due to learning about operator overloading, I learned you can have member and non member functions. So I am wondering are there any basic guidelines when a function should be a non-member vs a member function?

Would it be a good idea to have both types for a class? Where the non-member directly manipulates / changes the object verse a member function that does the same thing but returns a modified copy of said object?

E.G:

A TransposeMatrix(Matrix a) function that as a non-member directly changes the input matrix, where as the member version (a.TransposeMatrix()) returns the transpose version of the matrix it was called on


 
class Matrix
{
public:
 
//Return a transposed version of the input matrix, while still retaining the original matrix.
//The return matrix is a transposed copy of the input matrix
Matrix TransposeMatrix(const Matrix &a);
 
};
 
//Directly modify / set the matrix used as input
TransposeMatrix(const Matrix &a);
 
Advertisement
For that example I would clearly favor two member functions: 'Matrix transposed() const' returns a modified copy while 'void transpose()' modifies the matrix itself.

The closest to a general rule I could think of would be: if it would take a single Class parameter (perhaps with some other non-Class parameters) it's probably better off as a member functions. That rule is just sketched into sand though, not hammered into granite. Generous helpings of common sense and experience are advised.
A memorable quote: "Functions want to be free." - Herb Sutter

The general theory is that everything which can be a non-member function should be a non-member function. There are various reasons, ranging from avoiding class bloat (does your matrix.h file really need to include all 50 functions that operate on matrices?), avoiding dependencies (does your matrix.h need to include quat.h and vec4.h and vec3.h and any other type it might interact with?), 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), and language features.

This means that you wouldn't want both a member and a non-member function. Make them _both_ non-member functions. Since the matrix is just a dumb holder of data in this scenario and the data is publicly accessible, the mutating non-member function takes its value by lvalue-reference while the non-mutating version takes it by const-reference. You'd probably want to give them different names, but this is _way_ better than the member version having one semantic and the non-member having another (the name should tell you the semantic, not its member-ness; e.g. transpose_copy and transpose_mutate). Another option to consider is to just not having the mutating version: copying a matrix (even a 4x4 matrix) is a lot cheaper than you think and can in some cases make life easier for the optimizer, resulting in even faster code. In other words, prefer value types that have the same sorts of usage semantics as the built-in math types like int and float.

The language features are an interesting case. Consider a function like abs(). This is a non-member function for integers, floats, etc. If you make it a member function for your vector types, you can no longer write generic code that works both for your types and for built-in types! If you instead make a non-member overload of abs() for your types, you can now write a generic function that can take the absolute value of any math type. The same goes for other mathematical operations.

The argument for member functions mostly boils down to getting code-completion (Intellisense) to work for your operations. ... that's really about it. An easy work-around is to put all your math types in their own namespace and then put the free functions in those namespaces. And the added benefit is that you can put your data types in one header and your functions in another header so you only have to include the minimal data type header from within other headers, improving compile times. See the motivating arguments at http://bitsquid.blogspot.com/2012/11/bitsquid-foundation-library.html for using this approach for not only math types, but your STL replacement/companion library as well.

A future C++ (maybe even C++17) might have a feature that also allows non-member functions to be called with the dot notation for Intellisense. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4174.pdf and http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4165.pdf propose slightly different variants of this feature (and more).

TL;DR: prefer non-member functions for everything possible.

Sean Middleditch – Game Systems Engineer – Join my team!

Sean did a really good job explaining, I can't really think of any more details to add.

In general, you want the fewest member functions that you can reasonably get away with. 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. The reasons are many, but at the end of the day, the one you really care about is that it just makes your codebase less coupled and more cohesive -- in short, easier to use and extend.

The implications for generic code are also important, for an example of why and how, you need look no further than the non-member begin()/end() functions added in C++11 to the standard containers. This really speaks highly of the non-member approach, since its hard to think of anything that you'd think was more a "member" than begin() and end() of a container.

Keep in mind that a non-member, non-friend function that's in the same namespace as the class its related to is exactly as much a part of that class's interface as its member functions are -- they are not second-class citizens unless they were to exist in another namespace. Koenig Lookup ensures this relationship, and that overloads and such are resolved correctly.

throw table_exception("(? ???)? ? ???");

This sounds a little bad, but I still feel confused sad.png

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

There are various reasons, ranging from avoiding class bloat (does your matrix.h file really need to include all 50 functions that operate on matrices?), avoiding dependencies (does your matrix.h need to include quat.h and vec4.h and vec3.h and any other type it might interact with?)


Are you saying I should not do this? And that the non member function should go in there own Header?
Does that not still keep the "problem" of having the includes when needing to use Matrix2?


class Matrix2
{
	public:
	Matrix();
	
	/*Other stuff*/
	
	float data[4];
	
	private:
};

/*Non member functions*/
Matrix2 operator+(const Matrix2 &a, const Matrix2 &b);
Matrix2 Transpose(const Matrix2 &a);
Matrix2 Inverse(const Matrix2 &a);

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


This might be a side note, but taking the above example (Matrix2)
Lets say I have Matrix2, Matrix3, Matrix4, and etc
They all have non-member methods that allow the user who includes matrix.h (all of these objects are in the same header) to do things like +, -, *, and use functions that return the Matrix* version of the Transpose, inverse, or etc.

So then say we come to the function Identity, which you can guess, gives the Matrix*'s identity matrix in some form. This is where I see a problem.

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?).

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?

Do I just live with the wrongness and make it a member funciton? Or do I make it a non member function and just pass in a param for the sake of the compiler, which will then know what Identify function to use. Ooor do I make it a static member function, which then makes me wonder if all of the previous functions [transpose, inverse, etc] should also become static member functions?



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.

throw table_exception("(? ???)? ? ???");

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.


I believe I can elaborate on the "second class citizen" comment. Pretty sure the idea is that:


Object.Function();
"Feels" first-class, while:


Function(Object);
"Feels" second-class.

Plus most IDEs will not help you with the second one because they have nothing to key off of when you want to see what functions operate on an object (unlike the '.' character for member functions).

As Sean pointed out, there are a couple of C++17 proposals that will eliminate this problem, however, should they be approved smile.png

This topic is closed to new replies.

Advertisement