Home > database >  Why should I prefer a separate function over a static method for functional programming in c ?
Why should I prefer a separate function over a static method for functional programming in c ?

Time:12-05

In my other question, How to use a static method as a callback in c , people solved my practical problem, then suggested I take a different approach in general to follow best practice. I would like a better understanding of why.

This example uses a comparator, but I would really like to understand if there is a general rule for functional programming (for example perhaps also aggregator functions related to a class etc.)

Example use case for a comparator function:

#include <set>

int main() {
    std::set<MyClass, decltype(SOMETHING_HERE)> s(SOMETHING_HERE);
    return 0;
}

Below are solutions I am aware of, from most to least recommended (I believe) - the opposite order to what I would currently choose.

1.

Recommended way (I believe) with a functor:

struct MyComparator {
    bool operator() (MyClass a, MyClass b) const {
        return a > b; // Reversed
    }
};
std::set<MyClass, MyComparator)> s;

I don't like: verbose; not lexically tied to MyClass; I don't understand why s has no constructor argument. I am confused about the lack of argument because set seems to mix up/combine deciding the comparison algorithm at compile time / with the typesystem (as in this example), and at runtime / with a pointer. If it was just one way or the other I would be fine with "that's just how it is".

2.

A method that seems nicer to me:

auto const my_comparator = [](int a, int b) {
    return a > b;
};
std::set<int, decltype(my_comparator)> s(my_comparator);

It's terser. Intention is clear: it is a function, not a class that could be potentially added to. Passing the object, not just the type, makes sense to me (couldn't there be 2 different implementations).

3.

The method that makes most sense to me (based on other languages):

class MyClass {
public:
    
    // More code here
    
    static auto compare_reverse(MyClass a, MyClass b) {
        return a > b;
    }
};
std::set<int, decltype(&MyClass::compare_reverse)> s(&MyClass::compare_reverse);

It is clearly related (and lexically tied to) MyClass. Seems efficient (though I have been told it's not) and terse.


Any explanation why the recommended order is better, performance, bugs/maintainability, philosophical, very appreciated.

CodePudding user response:

Firstly, note that if you just want a > comparator, there are std::greater<> and std::greater<MyClass> (the former is usually superior).


Reviewing the options you listed:

(1)

struct MyComparator
{
    bool operator()(MyClass a, MyClass b) const
    {
        return a > b;
    }
};
std::set<MyClass, MyComparator)> s;

As you said, this is the recommended way.

not lexically tied to MyClass

As suggested in the comments, you can put it inside MyClass.

don't understand why s has no constructor argument

If you don't pass the comparator to the constructor, the comparator is default-constructed (if it's default-constructible, otherwise you get a compilation error).

You can pass MyComparator{} manually, but there is no point.

(2)

auto my_comparator = [](int a, int b)
{
    return a > b;
};
std::set<int, decltype(my_comparator)> s(my_comparator);

It's exactly equivalent to (1), except you're forced to pass my_comparator to the constructor. C 20 fixed this by making lambdas default-constructible (if they have no captures), making the argument unnecessary.

(3)

class MyClass
{
  public:
    static auto compare_reverse(MyClass a, MyClass b)
    {
        return a > b;
    }
};
std::set<int, decltype(&MyClass::compare_reverse)> s(&MyClass::compare_reverse);

This gives you an extra feature: you can choose the comparator at runtime, by passing different functions to the constructor.

This ability comes with an overhead: the set stores a pointer to the function (normally 4 or 8 bytes), and has to look at the pointer when making comparisons.

(3.5)

You can wrap your function in std::integral_constant. The result is equivalent to (1).

bool foo(int x, int y) {return x > y;}

std::set<int, std::integral_constant<decltype(&foo), foo>> s;
// Or:
using MyComparator = std::integral_constant<decltype(&foo), foo>;
std::set<int, MyComparator> s;

This is good if you're dealing with an existing type, that provides the comparator as a function, and you don't want the overhead of (3).


All of those (except (3)) are equivalent.

If std::greater<> is applicable, you should use it.

Otherwise I'd recommend (1) as the least confusing option.

  • Related