Reputation: 195
I've read some other questions here and did not find any definitive answer.
background
for analogy, assume there is a node class and that I'd want to get a graph of the connected nodes from this one. For this I'd need a function get_graph()
.
first try:
the function is a regular member function of the Node
class:
class Node
{
//impl...
GraphObj get_graph();
}
I don't like this one since a graph is not a functionality of the node class.
It's rather a query upon some instances of Node
.
second try:
the function is a static member function of the Node
class:
class Node
{
//impl...
static GraphObj get_graph();
}
this is better because now I've decoupled the function from an instance. But still, I don't feel comfortable that this function is part of the class.
third try:
the function is a free function in the same namespace/TU:
class Node
{
//impl...
}
GraphObj get_graph(Node* graph_source);
Now it's really decoupled! I don't want the user of this class to miss this functionality. Also, this function can only act on this class, so can it be too much decoupled?
tl;dr
how should you decide how to expose a function related to a class? particularly exposing the function as:
Upvotes: 4
Views: 869
Reputation: 2698
You have a nice question, and I must say that analogy is good as well. I'll try explaining when to use what, and then at the end, we'll take a look into what fits into this use case.
When to use Regular Member Functions?
These are the methods that work at the object level. Typical examples are the getters and setters, that are used to get/set class attribute values. The constructor also works at the object level (to initialize it with a value), and thus they are also at the class level and non-static in nature.
When to use Static Member Functions?
Given a class A
, if the method is needed only by the objects of class A
, they should be defined as static members. Let's say there's an isValid()
method. It takes an object of A only and returns a boolean value depending on if the object has all valid values. Such methods should be defined as static methods in the class, as the definition of validity may go well beyond the NULL and empty checks. An example is shown below
class A {
public: int n1; // <-- Must be greater than 0
static bool isValid(A& ob) {
if (ob.n1 > 0) return true;
return false;
}
};
When to use Non-Member Methods?
If you have a method that can work on objects of multiple classes, then they should be defined as non-member methods. An example of this would be getValueOrDefault()
method, which may work as defined below
template<typename T>
T getValueOrDefault(T n1, T n2) {
return n1 == NULL ? n2 : n1;
}
int main () {
int num1 = NULL, num2 = 2;
std::cout << getValueOrDefault(num1, 10); // <-- Will return 10 as num1 is NULL
std::cout << getValueOrDefault(num2, 10); // <-- Will return 2 as num2 is NOT NULL
}
Getting back to your graph analogy.
It would be good to move the method out of the node class and maybe create another Graph
class that may have a method getGraph
in it. You can generalize the method to work with not just the Node
class but any linked-list type of class if you follow certain standards across all the classes.
Otherwise, even the static method approach should work fine, but it may decrease the cohesion in the class.
Upvotes: 2
Reputation: 2554
I would do it with template friend.
In any case, if you want something generic you need to have some kind of common interface, be it same member names or access functions.
main.hh (or get_graph.hh)
//
#include <iostream>
template<typename T>
T& get_graph (T& obj)
{
std::cout << obj.getInternalData() << std::endl;
// this could be obj.data directly if the member names are always the same
return obj;
}
main.cc
#include "main.hh"
class INode
{
private:
virtual int getInternalData () = 0;
};
class Node : private INode {
public:
Node(int i) : data (i) {}
int getValue () { return data; }
private:
int data;
int getInternalData () { return data; }
template<typename T>
friend T& get_graph (T&);
};
class NodeB : private INode {
public:
NodeB(int i) : someother (i) {}
int getValue () { return someother; }
private:
int someother;
int getInternalData () { return someother; }
template<typename T>
friend T& get_graph (T&);
};
int main()
{
Node obj (999);
Node & n = get_graph(obj);
NodeB obk (111);
NodeB & m = get_graph(obk);
std::cout << "Data: " << n.getValue() << std::endl;
std::cout << "Data: " << m.getValue() << std::endl;
}
In this case, the functions to access private members are private so you isolate that from external users and they can simply use it without any knowledge.
The template allows you to handle any node that complies with the INode interface.
If you add relevant operators to the Node
classes you don't even need the interface: every "graph getable" class implementing the operators will work with the template get_graph
.
Upvotes: 1
Reputation: 38267
Scott Meyer's article on member- vs. free functions is highly relevant here;
If you're writing a function that can be implemented as either a member or as a non-friend non-member, you should prefer to implement it as a non-member function. That decision increases class encapsulation.
Checkout the article for the explanation.
The question whether member functions should be static
or not is another issue. Generally, make them static
if they aren't coupled to a particular instance. Examples are utility functions for setting up the state of the object depending on constructor arguments, e.g.
A::A(int someArg) :
dataMember{computeInitialState(someArg)}
Here, computeInitialState
should be static
. Another example are named constructors, e.g. Point::cartesian(double x, double y)
- this should be static
, too.
Upvotes: 2