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 Xs 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 <.