First, a way to map a projection into an ordering:
template<class F>
struct order_by_t {
F f;
using is_transparent = std::true_type;
template<class Lhs, class Rhs>
auto operator()(Lhs&& lhs, Rhs&& rhs)const
-> decltype (
static_cast<bool>(f(std::declval<Lhs>()) < f(std::declval<Rhs>())
)
{
return f(std::forward<Lhs>(lhs)) < f(std::forward<Rhs>(rhs));
}
};
template<class F>
order_by_t<std::decay_t<F>> order_by(F&& f) {
return {std::forward<F>(f)};
}
A projection takes a type X and "projects" it onto a type Y. The trick here is that the type Y
is the type of the field that we want to order our X
s by (in this case, a string, and the projection takes X
to the name of X
).
This means all we have to do is define the projection (the mapping from our type, to the part of the type we want to order it by), and then feed it to order_by_t
and it will generate an ordering function for us.
order_by_t
seems stateful, but it doesn't have to be. If F
is stateless, so can order_by_t
be! Stateless means we don't have to initialize the F
, and we can just use it, and also can lead to the compiler understanding the code better (tracking state is hard for compilers, stateless things are easy to optimize).
Or, in short, stateless is better than stateful. Here is a stateless type that wraps a function call:
template<class Sig, Sig* f>
struct invoke_func_t;
template<class R, class...Args, R(*f)(Args...)>
struct invoke_func_t<R(Args...), f> {
R operator()(Args...args)const {
return f(std::forward<Args>(args)...);
}
};
Example use:
void println( std::string const& s ) {
std::cout << s << '\n';
}
using printer = invoke_func_t< void(std::string const&), println >;
and now printer
is a type that any instance of it will call println
when you use its operator()
. We store the pointer-to-println
in the type of printer
, instead of storing a copy of the pointer inside of it. This makes each instance of printer
stateless.
Next, a stateless order_by
that wraps a function call:
template<class Sig, Sig* f>
struct order_by_f:
order_by_t< invoke_func_t<Sig, f> >
{};
which is one line, a side effect of the above being pretty polished.
Now we use it:
class Message; class Label;
// impl elsewhere:
std::string const& GetMessageName( std::shared_ptr<Message> const& );
std::string const& GetLabelName( std::shared_ptr<Label> const& );
class Label {
private:
std::string name_;
using message_name_order = order_by_f<
std::string const&(std::shared_ptr<Message> const&),
GetMessageName
>;
std::set<std::shared_ptr<Message>, message_name_order > messages_;
};
where I jumped through a bunch of hoops to make it clear to the std::set
that we are ordering by calling GetMessageName
and calling <
on the returned std::string const&
s, with zero overhead.
This can be done simpler more directly, but I personally like each of the onion layers I wrote above (especially order_by
).
The shorter version:
class Message;
bool order_message_by_name( std::shared_ptr<Message> const&, std::shared_ptr<Message> const& );
class Label {
private:
std::string name_;
std::set<std::shared_ptr<Message>,
bool(*)(std::shared_ptr<Message>const&, std::shared_ptr<Message>const&)
> messages_; // Message is incomplete!
Label(std::string name):name_(std::move(name)),
messages_(&order_messages_by_name)
{}
};
where we store a function pointer in our set that tells the class how to order it.
This has run time costs (the compiler will have difficulty proving that the function pointer always points to the same function, so will have to store it and dereference it on each ordering call), forces you to write order_messages_by_name
(an ugly specific-purpose function), and has maintenance costs (you have to prove that the function pointer never changes whenever you think about that set).
Plus, it doesn't give you the cool order_by
function, which you'll love every time you want to sort
a std::vector
by anything except <
.