这是一个老问题,但当我想自己做这件事时,我对任何答案都不完全满意。不需要任何黑客攻击。 std::priority_queue 包含合法和惯用地执行此操作的所有机制。
std::priority_queue 有两个非常有用的数据成员,c(底层容器)和comp(比较谓词)。
同样有用的是,标准要求 Container 模板类型必须是 SequenceContainer 的模型,而迭代器是 RandomAccessIterator 的模型。
这很有帮助,因为 std::make_heap 的 Iter 参数类型具有相同的 RandomAccessIterator 模型要求。
这是一种冗长的说法,即std::priority_queue 是堆的包装器,因此std::make_heap(std::begin(c), std::end(c), comp) 必须是一个有效的表达式。
“坏”消息是 c 和 comp 受到保护。这实际上是个好消息,原因有两个:
您不能意外破坏堆。
如果您从std::priority_queue 派生,您可以有意修改堆。
因此,诀窍是从std::priority_queue 派生您的优先级队列,在成员函数中,以任何您喜欢的方式改变内部堆c,然后调用std::make_heap(std::begin(c), std::end(c), comp); 将其变回有效堆。
一般来说,从 STL 容器继承不是一个坏主意
嗯,是的,但是……
对于年轻人和/或粗心的人来说,这可能是一个坏主意,有两个原因。缺乏多态析构函数和切片的风险。
- 多态析构函数
实际上没有合理的用例拥有一个标准容器,通过一个指向它的基类的指针。容器是轻量级的(当其中没有任何东西时)并且可以廉价移动。您可能会想到用例,但我可以保证,通过将容器封装在另一个堆分配的对象中,可以将您打算做的任何事情做得更好。在精心设计的代码中,这不应该成为一个问题。无论如何,从priority_queue 模板类中私有继承可以消除这种风险。
- 切片
当我们传递继承的对象时,当然存在切片的风险。这里的答案是从priority_queue基类私有继承,然后在派生类中使用using只导出我们希望共享的基类接口部分。
下面的示例已更新以显示这一点。
下面是一个真实项目的例子。
要求:
保留必须通知客户更改的主题队列。按通知此主题的最早时间的时间戳对队列进行排序。不允许重复的主题名称。
这是一个工作演示:
#include <queue>
#include <string>
#include <chrono>
#include <cassert>
#include <iostream>
using topic_type = std::string;
using timestamp_clock = std::chrono::system_clock;
using timestamp_type = timestamp_clock::time_point;
struct notification {
topic_type topic;
timestamp_type timestamp;
};
bool topic_equals(const notification& l, const topic_type& r) {
return l.topic == r;
}
bool topic_equals(const topic_type& l, const notification& r) {
return l == r.topic;
}
bool age_after(const notification& l , const notification& r) {
return l.timestamp > r.timestamp;
}
bool age_after(const notification& l , const timestamp_type& r) {
return l.timestamp > r;
}
bool age_after(const timestamp_type& l , const notification& r) {
return l > r.timestamp;
}
struct greater_age
{
template<class T, class U>
bool operator()(const T& l, const U& r) const {
return age_after(l, r);
}
};
template<class T>
struct pending_queue_traits;
template<>
struct pending_queue_traits<notification>
{
using container_type = std::vector<notification>;
using predicate_type = greater_age;
using type = std::priority_queue<notification, container_type, predicate_type>;
};
class pending_notification_queue
: private pending_queue_traits<notification>::type
{
using traits_type = pending_queue_traits<notification>;
using base_class = traits_type::type;
public:
// export the constructor
using base_class::base_class;
// and any other members our clients will need
using base_class::top;
using base_class::pop;
using base_class::size;
bool conditional_add(topic_type topic, timestamp_type timestamp = timestamp_clock::now())
{
auto same_topic = [&topic](auto& x) { return topic_equals(topic, x); };
auto i = std::find_if(std::begin(c), std::end(c), same_topic);
if (i == std::end(c)) {
this->push(notification{std::move(topic), std::move(timestamp)});
return true;
}
else {
if (timestamp < i->timestamp) {
i->timestamp = std::move(timestamp);
reorder();
return true;
}
}
return false;
}
private:
void reorder() {
std::make_heap(std::begin(c), std::end(c), comp);
}
};
// attempt to steal only the base class...
void try_to_slice(pending_queue_traits<notification>::type naughty_slice)
{
// danger lurks here
}
int main()
{
using namespace std::literals;
auto pn = pending_notification_queue();
auto now = timestamp_clock::now();
pn.conditional_add("bar.baz", now);
pn.conditional_add("foo.bar", now + 5ms);
pn.conditional_add("foo.bar", now + 10ms);
pn.conditional_add("foo.bar", now - 10ms);
// assert that there are only 2 notifications
assert(pn.size() == 2);
assert(pn.top().topic == "foo.bar");
pn.pop();
assert(pn.top().topic == "bar.baz");
pn.pop();
// try to slice the container. these expressions won't compile.
// try_to_slice(pn);
// try_to_slice(std::move(pn));
}