Eyal Kamitchi
Eyal Kamitchi

Reputation: 195

static member utility-function vs Non-member utility-function

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:

  1. regular member function.
  2. static member function.
  3. free function taking an instance of the class.
  4. something else?

Upvotes: 4

Views: 869

Answers (3)

Debargha Roy
Debargha Roy

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

Manuel
Manuel

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

lubgr
lubgr

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

Related Questions