有状态的STL比较器有更好的替代方案吗?

时间:2010-10-28 15:06:31

标签: c++ stl

我有一个使用std::priority_queue的相当大的复杂算法。在某些情况下,我希望队列成为最小队列,而在其他情况下,我希望队列成为最大队列。所以我必须提供一个比较器类型,可以执行其中一个。由于比较器是模板参数,因此它基本上是std::priority_queue类型的一部分。因此,在引用队列的任何地方,代码都必须知道类型。

当然,一个选项是提供一个具有状态的自定义比较器,并在operator()的每次调用中在升序和降序之间进行选择,但我试图避免使用此分支的性能开销每一次比较。我能看到的另一个选择是复制整个算法,一个用于升序排序,另一个用于降序。

这个问题有更好的替代方案吗?

编辑:由于有些人似乎非常渴望在没有真正了解问题的情况下提供帮助,我会尝试详细说明一下。

我实现的是外部排序算法(外部合并排序)。该算法在合并阶段使用std::priority_queue。现在,排序可以是升序或降序。对于这两种情况,std::priority_queue比较器必须不同。你可以想象一个简单的排序算法可以很容易地参数化来处理升序/降序,如:

// psuedocode...
if (ascending) {
    if (a > b) // do something
} else {
    if (a < b) // do something
}

这样做的一个问题是必须检查ascending标志每次迭代的排序循环。 'jalf'建议编译器可以“内联”这个分支,但我认为我没有看到它发生过,至少不是VC ++(是的,我看过一些程序集)。

现在,当使用std::priority_queue以排序顺序保存某些内容时,队列的实际类型对于升序和降序排序是不同的(因为比较器必须不同,并且它是一个模板参数)。所以选项是:

  1. 复制降序的算法(函数)。
  2. 按排序顺序对算法进行参数化,并使用一个队列进行升序,使用另一个队列进行降序。
  3. 使用具有单个状态(isAscending)的比较器仿函数,以便operator()()可以对升序和降序执行正确的比较。
  4. 模板化函数并要求比较器作为模板参数传递。
  5. 使用函数指针作为比较器模板参数。
  6. 请注意我在算法期间在两种不同的队列行为之间切换(正如一些人所要求的那样)。我在算法开始时将队列设置为最小或最大队列,并且对于该特定排序仍然如此。

    现在比较上面的选项。

    选项1因为不需要的代码重复和维护而被淘汰(尽管它可能会产生性能最佳的代码)。

    选项2是不可取的,因为所有额外的运行时检查和分支,以及不必要的队列都不会被使用。

    选项3更具吸引力,因为分支在比较器函数中被封装/隔离 - 但仍需要对每次比较进行运行时检查(这基本上是我想要避免的)。

    选项4将问题提升一级,并要求调用者知道并可以访问比较器仿函数 - 有点乱。

    选项5似乎相当不错 - 通过允许比较器功能在运行时不同而不改变队列的类型,它可以实现干净的代码。决策逻辑不必暴露给调用者(就像选项4一样)。

    我可以在选项5中看到唯一的缺点是编译器可能无法通过函数指针内联调用 - 但在我的情况下它确实如此,因为调用了被调用的函数在同一个翻译单元中。如果它没有内联,我猜测选项3本来会更好,但在我的情况下,选项5的表现更好。

    此外,在我意识到选项5是可能的(我之前没有)之后,我发现使用函数指针而不是函子可能没有做太多,我怀疑人们有时会跳过箍使用仿函数(正如我必须的那样),当一个函数指针可以使代码更清晰时(特别是当性能不是一个问题时)。

3 个答案:

答案 0 :(得分:3)

使用模板

template <typename Comparator>
void algo()
{
  std::priority_queue<int, std::vector<int>, Comparator> pqueue;
  ...
}

- 编辑
我看了你的编辑,但我仍然没有真正得到困扰你的东西。你说

Option 4 pushes the problem up one level and requires the
caller to know and have access to the comparator functors 
-- kind of messy.

无论如何,来电者必须在升序与否之间做出选择。 当然,他没有必要知道使用哪种比较器类型:

void algo_ascending() {
  algo<Ascending_Comparator>();
}
void algo_descending() {
  algo<Descending_Comparator>();
}

甚至

void algo(bool ascending) {
  if (ascending)
     algo<Ascending_Comparator>();
  else
     algo<Descending_Comparator>();
}

答案 1 :(得分:1)

如果要使用具有状态的比较器,则无法更改该状态,因为比较器已被队列复制。为了在构造队列后更改比较器的状态,可以使用共享状态。

但是,在将元素添加到队列后,不应使用状态来更改比较结果。这可能会导致队列的奇怪行为。

struct shared_state_t
{
 int number;
};

struct compare_t
{
 bool operator <(const record_t &left, const record_t &right) const { left < right; }
 tr1::shared_ptr<shared_state_t> shared;
};

void func()
{
 tr1::shared_ptr<shared_state_t> state(new shared_state_t());
 state->number = 42;
 compare_t comp;
 comp.shared = state;
 std::priority_queue<record_t, vector<record_t>, compare_t> queue(comp);
 // do something with queue
 state->number = 999;
 // compare object of queue is aware of the new state
}

答案 2 :(得分:0)

std::priority_queue在其构造函数中使用比较器对象。将对象作为函数参数并将其传递给构造函数。每次调用都将根据参数创建不同的容器。所以这个分支只是在建设时。

template<class C>
void the_algorithm(const C& compare)
{
    std::priority_queue<Type> q(compare);
    // ...
}

bool this_before_that(const Type& op1, const Type& op2);

// ...
{
    the_algorithm(this_before_that);
    the_algorithm(ThatBeforeThis_Functor());
}

这里的分支计数仅仅是对算法的调用顺序。