1

Suppose that we need to store information about labeled e-mail messages. Each message can be assigned many labels. Also, we would like to be able to quickly retrieve all messages assigned to a given label. Here is my design:

class Message;
class Label {
public:
    ...
private:
    std::string name_;
    std::set<std::shared_ptr<Message>, 
             std::function<bool(...)>> messages_; // Message is incomplete!
};

class Message {
public:
    ...
private:
    std::string title_;
    std::set<Label *, 
             std::function<bool(...)>> labels_; // fine
};

Each label stores the set of messages to which the label is assigned. Since this set needs to be searchable by the message title, we pass std::function for comparison as the second template parameter of std::set. The Problem: this function object needs to be able to access the Message's members. However, Message is an incomplete type at this point.

The situation cannot be fixed by putting the definition of Message before the definition of Label, because then we would have a similar problem with std::function passed to the set of labels (the line commented as being fine in the above code), which needs to be searchable by label name.

Is there a fix or a better design for this?

AlwaysLearning
  • 7,257
  • 4
  • 33
  • 68
  • 2
    To me, Label should not store references to Messages... Maybe a LabelManager could do it. – Paolo M Jul 22 '15 at 14:14
  • I agree. It might make more sense to separate the relationships between labels and messages from the actual labels and messages themselves. – Cameron Jul 22 '15 at 14:15
  • you only have to provide the comparison function to the `set`'s ctor from a `Label` ctor which is fine to be defined after `Message` declaration, or am I missing something? – Alexander Balabin Jul 22 '15 at 14:17
  • @AlexanderBalabin I am new in this business, but I believe that the custom ordering is part of the type. – AlwaysLearning Jul 22 '15 at 14:20
  • @PaoloM Is `LabelManager` an example of a known design pattern? If so, could you please provide a link? (it would help me a lot to see an example) – AlwaysLearning Jul 22 '15 at 14:22
  • 1
    It is only the type of comparer you need to provide, the actual comparer is provided to the ctor. Have a read through this: http://stackoverflow.com/questions/14896032/c11-stdset-lambda-comparison-function – Alexander Balabin Jul 22 '15 at 14:23
  • @MeirGoldenberg No, LabalManager as a custom class. I just mean that Label to me is just a tag, it does not take care of nothing. – Paolo M Jul 22 '15 at 14:32
  • @PaoloM It is so in this case. But this is just an exercise. One can think of a problem with folders or whatever, whereby several entities collectively store the same message. – AlwaysLearning Jul 22 '15 at 14:40
  • Can you replace your `...` with a guess asto how you'd want to solve the problem? Is there a reason you used `std::function` and not a naked function pointer or a function object? – Yakk - Adam Nevraumont Jul 22 '15 at 14:55

1 Answers1

0

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

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • That is one hell of a big onion. – Quentin Jul 22 '15 at 15:12
  • @Quentin We are writing code in C++, but we really want to be writing code in (the better parts of) python. Which means we gotta write a bit of boilerplate to turn C++ into python. And if we do it right, we get zero-overhead at runtime, instead of having an interpreter's worth of overhead. I suppose the other way is Cython, but that is (A) not something I'm fluent in, and (B) from what I can tell, not as slick as (the better parts of) python (you gotta restrict what kind of types you work with, etc, for bare-metal performance). – Yakk - Adam Nevraumont Jul 22 '15 at 15:15
  • Maybe I'm missing something, but does Python have a reason to show up on this question, or are you doing this just for the hell of it ? I do like the metamagic in there (it's a shame that you need to repeat the type of the function for `invoke_func_t` though), but *man* that's far-fetched. – Quentin Jul 22 '15 at 15:27
  • @Quentin I always try to make my C++ code look python-esque at "point of use". Because (the good parts of) python code, at point of use, is pretty. That is why I brought it up. The goal of those onion skins is to make the next layer look prettier, without a sacrifice of performance. And yes, having to repeat the signature is annoying, there was at least 1 proposal in front of the C++ standard committee to fix that (I do not know its current status). – Yakk - Adam Nevraumont Jul 22 '15 at 15:35