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.