Advertisement

Is this proper usage of non-member functions?

Started by January 17, 2024 04:58 AM
19 comments, last by JoeJ 10 months ago

I recently learned how I can simplify/improve code by using structs and non-member functions so to test this new discovery I refactored my camera code and I have a feeling I misused it.

I took every member function that wasn't 100% necessary out and I'm left with:

struct Camera {
	glm::vec3 position;
    glm::vec3 forward;
    glm::vec3 right;
    glm::vec3 up;

    float yaw;
    float pitch;
    float roll;

    float fov;
    float aspectRatio;
    float nearPlane;
    float farPlane;

    std::unique_ptr<CameraBehavior> behavior;

    void Update(float timestep, Input& input) {
        if (behavior) {
            behavior->Update(*this, timestep, input);
        }
    }
};

and made those member functions into non-member functions:

void Move(Camera& camera, const glm::vec3& direction, float speed, float timestep);
void Pivot(Camera& camera, float mouseXChange, float mouseYChange, float sensitivity);
void UpdateDirectionVectors(Camera& camera);

glm::mat4 ComputeViewMatrix(const Camera& camera);
glm::mat4 ComputeProjectionMatrix(const Camera& camera);

The first 3 functions probably won't ever be used by something that isn't a camera so I have a feeling those might be bad usages. The compute functions on the other hand the camera doesn't really need that it's just for the render and potentially other code so is this a better usage?

Your test is working fine I think 🙂

You can indeed argue that the first three functions mostly “belong” to a camera. That notion is indeed much less present with the 4t and 5th function. Whether making them independent functions is “good” or “bad” (for each case) is a decision you are learning about now. That notion however is not universal, different persons have different ideas about what “good” / “ bad” is.

In general, independent functions have varying degrees of “belonging” to something. Somewhere in the range from “completely belong to one of the parameters” to “belongs to none of them in particular” is the cutting point between member functions and independent functions. That point is different for everyone, your journey is now about finding that point for yourself.

My advice is go with the flow. Keep member functions when you think there is a strong relation, like you expressed in the question, and independent functions when you think that relation is not clear enough to make it a member function. You will probably find that decisions here will shift as you get more experience, and that's ok. It takes time to find your balance.

Advertisement

I've personally come to prefer member-functions, where possible.

camera.ComputeViewMatrix();

is to me preferable to

ComputeViewMatrix(camera);

syntax-wise, but that might be just personal preference. So I would not have split those functions you showed myself. I just don't think you get miuch value from splitting functions that then always take a “Camera" object as first parameter, into independant functions. Well, the one real benefit of free functions is that they might allow you to forward-declare the function, without having to include the header (which you cannot do with member-functions; and can save some inclusion-dependencies especially when used in another header).

Other than that, kind of agree with Alberth. If a bunch of function use “camera” but are not really an intricate functionality of the camera object, it could make sense to separate. I'd try to think along the terms of “If I removed those functions entirely, would Camera still be usable”? I think in terms of not being able to move, or compute matrices, then answer would be “no”, and I would further be discouraged from decoupling them.

I'd probably rather go for trying to extract generic algorithms. Make a “computeViewMatrx” that takes the member-variable of the camera as parameters, and use that internally in Camera::ComputeViewMatrix, if you care about reusability.

I'm pretty sure there will be someone making a case for having free functions about members, so millega might vary. Just pick what you think makes sense, try it out, and if you don't like it, change it.

Understanding WHY you are doing things is important, and it looks like you're well on your way for that.

Some questions:

Why are you treating a camera object different from other game objects? There are some possible reasons for it, but most major game engines treat cameras as an add-on (a behavior or component) that you can attach to a world object; most of the manipulation is done as a general object rather than as a camera. Moving things, following, pointing at, they're not unique to cameras as you pointed out, and they can be useful on many types of objects.

Do you have reasons for separating out behavior the way you did? Are there ways to have additional cameras? Some examples include VR-viewpoint cameras that often have two stereo cameras for the eyes plus a third camera for the screen, or for cinematic cameras with special effects, or for software-cameras like minimaps or picture-in-picture displays as the camera feeds to a texture.

There's no right or wrong answer, but there are tradeoffs for each implementation choice that can help with understanding why professional tools make the choices they did, and reasons people sometimes will customize away from those choices.

DSICLAIMER: Do not read this section. It's a bad proposal. ; )

ShapeSpheres said:
The compute functions on the other hand the camera doesn't really need that it's just for the render and potentially other code so is this a better usage?

Personally i came up with an unusual solution here. But there is a catch, so i'm open for proposals if anyone knows.

Instead camera i use a mesh for example:

namespace MeshProcessing
{
	class HEMesh
	{
	public:


// some data...

		
		typedef int32_t Index;
		
		// vertices
		std::vector<Index> mVertexEdge;
		std::vector<vec> mVertexPosition;
		
		
// ...

// some functions


		void SetVertexPos (const int vertex, const vec &pos)	{ mVertexPosition[(Index)vertex] = pos; }
		vec GetVertexPos (const int vertex)		const { return mVertexPosition[(Index)vertex]; }
		
		// ...

The functions are general stuff i need everywhere. Accessing the data, but also calculating a vertex normal or polygon area, basic things.

I could not add all specific functionality to the same mesh class. It would get too big.
I need mesh editing functions, visualization, serialization, conversation, and even more specific stuff like Delaunay triangulation.

So i use derived classes for each of those topics, for example:

namespace MeshProcessing
{
	class HEMesh_Modeling : public HEMesh
	{
	public:

		int AddPolygon (const int edge,
			const int atIndex = -1);

		int AddPolygonFromEdges (const int *edgeIndices, const int numEdges,
			const int atIndex = -1);

		int AddPolygonFromVertices (const int *vertexIndices, const int numVertices,
			const int atIndex = -1);

		void RemovePolygon (const int poly);
		
		// ...

So i have all my editing functions grouped on one derived class to keep things organized by topic.

To use the derive class, i do a cast:

HEMesh mesh;
((HEMesh_Serialization&) mesh).LoadFromFile("...");
((HEMesh_Modeling&) mesh).AddPolygon(17);
((HEMesh_Vis&) mesh).RenderPolygonsFilled();

Maybe this looks a bit awkward, but it's better than giving a mesh as parameter to all those specific functions while avoiding the derived class.
It's actually nice for things which have a lot of functionality associated to it. But surely overkill to a Camera.

But the catch is: The derived classes must not introduce any new member data to the base class. Otherwise i could not cast one to the other just to ‘switch topics’.
And sadly C++ has no way to enforce a derived class can only add functions but no data, afaict.
If it had, my proposal here would make much more sense.

Alternatively, we could dump all functionality to the header file of mesh class definition, but use multiple C++ files to group by multiple topics. But then our header file becomes bloated with lots of inlined functions we need only rarely, which is bad for many reasons.

So i really wonder what other programmers do here. There should be a better, standard way to do group class functionality, but i could not find any. And actually i feel like misusing the concept of derived classes for a different purpose.

ShapeSpheres said:
I recently learned how I can simplify/improve code by using structs and non-member functions so to test this new discovery I refactored my camera code and I have a feeling I misused it.

Ha, it's actually coincidence i've used the word ‘misuse’ as well. : )

But coming from C, to me it's member functions which was the new discovery.
Still, today i do not use global functions anymore. I use namespaces to group them by some topic, so they are no longer global but technically still the same.
Maybe you can do the same to make non-member functions feel less alien.

Example:

namespace TextureFilter
{
	// box filter

	template <typename T>
	static T SampleTextureBox (const T *texture,
		const int *gridI0, const float *gridF0, 
		const int *gridI1, const float *gridF1, 
		const int *size);
	
	template <typename T>
	static T SampleTiledTextureBox (const T *texture,
		const int *gridI0, const float *gridF0, 
		const int *gridI1, const float *gridF1, 
		const int *size,
		const bool tileH, const bool tileV);
		
	// ...
};

Those functions give me similar functionality then a texture lookup on GPU. It has lots of functions, so it's convenient to group them all by a namespace.

I could use a struct or class without any member data for the same purpose, but by using a namespace it's obviously clear there is no member data right from the start to anybody. So the namespace is clearly better.

However, sometimes it is useful to have private functions internal to the topic as well, to reduce visible complexity to the outside.
Then i use a class instead a namespace. Again i would like to make clear the class has no member data.

Following this, you could end up with something like this:

Camera cam;
cam.Update(...);
auto m = CameraTools::ComputeViewMatrix(cam); // CameraTools would be a namespace / class / struct to group related functionality

But as others said, this as a matter of finding your personal preferences with time, or the ability to adopt conventions from the code base you are working on with others.

JoeJ said:
Maybe this looks a bit awkward, but it's better than giving a mesh as parameter to all those specific functions while avoiding the derived class.

God, sorry to say, but this is horrendeous. This is in now way better than passing a mesh to the parameters.

You are relying on c-style casts here, which not only don't enforce the condition with data-members you mentioned, butr can literally cast anything. Not only will it cast const away quitely, but you could pass it any type of mismatching object, including primitive types like “int”, and it would quietly accept it, try to call the function on that mismatching object and crash. Please, for the love of god, never recommend this to anyone. I'm not one to judge preferenced, but this is plainly bad. At least use a static_cast:

static_cast<HEMesh_Serialization&>(mesh).LoadFromFile("...");

That will enforce const, and that “mesh” actually is a base-object of HEMesh_Serialization. However, in reality, if you really want to avoid passing a paramter, because you have like 50 different functions that all would take “mesh” as the first param, this is a much better solution:

struct HEMesh_Modeling

{

HEMesh_Modeling(HEMesh& mesh) : m_mesh(mesh) {}

// use m_mesh internally

int AddPolygon (const int edge,

const int atIndex = -1);

private:

HEMesh& m_mesh;

}

HEMesh_Modeling(mesh).AddPolygon();

Can't wrap it in code-tags, sorry. But this uses the same amount of code on the using-side (actually less syntax), requires no passing of parameters to the functions, and doesn't rely on any rigid fake inheritance-structure.

EDIT: Didn't even mention that what you do is UB, and is not guaranteed to work at all. Due to the standard, you are not allowed to cast an object to a derived class, if it is not part of that objects created inheritance structure. While it will work on a lot of compilers, if you compile gcc/clang with stric-aliasing, it will probably just not execute the calls at all.

Advertisement

Juliean said:
Please, for the love of god, never recommend this to anyone. I'm not one to judge preferenced, but this is plainly bad. At least use a static_cast:

Haha, well i have posted this actually hoping you would reply to correct me : )

I must admit i never use static_cast. Good practice is not worth the additional code here to me, but idk about eventual performance differences.

And sadly i do not like your second proposal either. Again more complexity just to be nice to the language.

I have no problems with clang either, but will pay attention to strict aliasing settings.
The UB argument worries me ofc. I did not know that.

It sucks, but i'll keep going this way, hoping future C++ opens a path i will like. And i'll avoid to use this for new code. I think the mesh is the only thing where i used this bad practice. : /

But thanks anyway! I can still change my mind…

JoeJ said:
I must admit i never use static_cast. Good practice is not worth the additional code here to me, but idk about eventual performance differences. And sadly i do not like your second proposal either. Again more complexity just to be nice to the language.

Performance is the same, any type of cast is a no-op essentially. It's not about “being nice to the language” eigther (its actually more of the language being nice to you to even allow that - c++ is the only language where a constructor like yours could exist), but just not disabling all form of type-checking. Which, aside from UB, is actually what you are doing. What you are doing is essentially the equivalent of saying "I don't want to overload a function for different arguments, so I pass them as void* alongside the typeid:

void badMethod(void* data, std::type_info type)
{
if (type == typeid(int))
*(int*)data;
else if (type == typeid(bool))
*(bool*)data;
}

badMethod(&int, typeof(int));
badMethod(&bool, typeof(int)); // yes, the mismatch is on purpose


That's how your example looks to me. You are of course free to do as you please, I mainly wanted to add in case of other people reading this and getting any funny ideas. No offence/don't take this personal, but the less code like the one you posted here exists, the better :D Many things are personal preference, that is just a very cut and dry case of bad code/design with no benefits and just downsides. The only “added complexity” in my example is, idk, having to add a constructor and member variable? This is well worth not being an actual landmine waiting to explode on any type of refactoring.

EDIT: You wouldn't even need to add a constructor if you used curly-bracket syntax:

struct HEMesh_Modeling
{

// use m_mesh internally

int AddPolygon (const int edge,

const int atIndex = -1);

HEMesh& mesh;

}

HEMesh_Modeling{mesh}.AddPolygon();

At that point, you'd really have to explain to me what the “additional complexity” compared to your example would be, because it's exactly the same amount of statements and less to type overall :D

EDIT2:

JoeJ said:
It sucks, but i'll keep going this way, hoping future C++ opens a path i will like.

What would even be a path you'd “like”? Doing weird fake down-casts would not work anywhere that's not c++, so do you have something in mind that a different language offers? In C#, you'd probably have partial classes, but other than that, you'd also eigther have to pass the class as a parameter or store it in a field.

Use member functions in the following cases:

  • When you need to access private or protected members of a class (or expect you might need to in the future).
  • When you want to make the member function itself private or protected.
  • When you need to make a member function virtual (or expect you might need to in the future).
  • For some special functions that only work as members (i.e. constructor, destructor, some operators).

All other functions can and should be non-members. This includes the Update function in the original post.

Juliean said:
HEMesh_Modeling{mesh}.AddPolygon();

At that point, you'd really have to explain to me what the “additional complexity” compared to your example would be, because it's exactly the same amount of statements and less to type overall :D

I like this curly version. I would not have thought to use this to do such nice one liner.

The complexity i meant is technically the need to create an object where no one is needed, and semantically the need to care about the member data which should not be needed either.

I can assure there is no potential landmine with my method. I use it for years already, it went through a lot of refactoring, but i never had any related problems, nor did i accidentally cast things to the wrong type. I wanted something easier to maintain, and it still is.

Thinking about it during the day, i concluded the best option would be to move modeling functionality to the base class. The other things are not so frequently used, and giving the mesh as parameter would be fine.

But then i thought i probably misunderstood your UB argument:

Juliean said:
Due to the standard, you are not allowed to cast an object to a derived class, if it is not part of that objects created inheritance structure.

I do not understand how a derived object could be NOT a part of the inheritance structure? It's the base class after all?

Juliean said:
c++ is the only language where a constructor like yours could exist

I do not use a constructor anywhere. And i can't follow your snippet to illustrate bad practice. To much keywords i don't know.

However, i do not try to write ‘proper C++’. I'm just a C programmer who uses some C++ features. Casting stuff is no security hole to me. It's something i did all the time, and i see no point to change my habits if they never cause me problems.

Juliean said:
I mainly wanted to add in case of other people reading this and getting any funny ideas. No offence/don't take this personal, but the less code like the one you posted here exists, the better :D

Yeah no problem. Accepted and agreed. I won't do it again.

But i want to do some self defense. The key question is: Why not giving the mesh as parameter?

The answer has to do with the half edge data structure and its usage patterns.
For every little thing we need to traverse the data structure, since there are no index lists. (Getting rid of index lists is the primary motivation to use half edge - it makes life much easier.)
Writing those traversals is no problem, but reading them is difficult and requires much more focus than we want.
Adding a ‘mesh.*’ to every line of code adds visual clutter, making the reading noticeable harder. It increases the time needed to maintain the code. Not only to me, but to anybody, i'm sure.

And that's the point where i prefer easier maintenance over good practice or quality of code.
That's no mistake. But if UB is a justified concern, sadly i have to find another way.

This topic is closed to new replies.

Advertisement