假设我们需要存储有关标记的电子邮件的信息。可以为每条消息分配许多标签。此外,我们希望能够快速检索分配给给定标签的所有消息。这是我的设计:
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
的行,需要通过标签名称进行搜索。
是否有针对此的修复或更好的设计?
答案 0 :(得分:0)
首先,将投影映射到排序的方法:
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
是一种类型,当您使用println
时,它的任何实例都会调用operator()
。我们将指针 - 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
并在返回的GetMessageName
上调用<
来清楚地告诉std::string const&
我们要订购的内容,没有开销。
这可以更直接地完成,但我个人喜欢上面写的每个洋葱层(尤其是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
功能,除了sort
之外,您每次想std::vector
<
时都会喜欢它。