一种涉及比较函子的循环依赖关系

A circular dependency involving comparison functors

本文关键字:循环 关系 依赖 比较 一种      更新时间:2023-10-16

假设我们需要存储有关标记电子邮件的信息。每个消息都可以分配许多标签。此外,我们希望能够快速检索分配给给定标签的所有消息。这是我的设计:

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
};

每个标签都存储分配了标签的一组消息。由于该集合需要通过消息标题进行搜索,因此我们将std::function作为std::set的第二个模板参数进行比较问题:此函数对象需要能够访问Message的成员。然而,Message在这一点上是一个不完整的类型。

这种情况无法通过将Message的定义放在Label的定义之前来解决,因为如果将std::function传递到标签集(上面代码中注释为fine的行(,则我们会遇到类似的问题,该标签集需要通过标签名称进行搜索。

对此有没有解决方案或更好的设计?

首先,一种将投影映射到排序中的方法:

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)};
}

投影采用类型X,并将其"投影"到类型Y上。这里的技巧是,类型Y是我们希望按其排序X的字段的类型(在这种情况下,是一个字符串,投影将X命名为X(。

这意味着我们所要做的就是定义投影(从我们的类型到我们想要排序的类型部分的映射(,然后将其提供给order_by_t,它将为我们生成排序函数

order_by_t看起来是有状态的,但它不一定是。如果F是无状态的,那么order_by_t也可以!无状态意味着我们不必初始化F,我们可以直接使用它,也可以让编译器更好地理解代码(跟踪状态对编译器来说很难,无状态的东西很容易优化(。

或者,简而言之,无状态总比有状态好。下面是一个包装函数调用的无状态类型:

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)...);
  }
};

示例用法:

void println( std::string const& s ) {
  std::cout << s << 'n';
}
using printer = invoke_func_t< void(std::string const&), println >;

现在printer是一个类型,当您使用它的operator()时,它的任何实例都会调用println。我们将指向-println的指针存储在printer类型中,而不是在其中存储指针的副本。这使得printer的每个实例都是无状态的。

接下来是一个无状态order_by,它封装了一个函数调用:

template<class Sig, Sig* f>
struct order_by_f:
  order_by_t< invoke_func_t<Sig, f> >
{};

这是一条线,上面的副作用被打磨得很好。

现在我们使用它:

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_;
};

在这里,我跳过了一堆困难,向std::set明确表示,我们正在通过在返回的std::string const& s上调用GetMessageName<进行订购,而没有任何开销。

这可以更简单、更直接地完成,但我个人喜欢我上面写的每一层洋葱(尤其是order_by(。


较短的版本:

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)
  {}
};

我们在集合中存储一个函数指针,该指针告诉类如何排序

这会带来运行时成本(编译器很难证明函数指针总是指向同一个函数,因此必须存储它并在每次排序调用时取消引用它(,迫使您编写order_messages_by_name(一个丑陋的专用函数(,并带来维护成本(无论何时考虑该集,都必须证明函数指针从未更改(。

此外,它没有给你酷炫的order_by功能,每当你想用除<之外的任何东西sortstd::vector时,你都会喜欢它。