【问题标题】:Does emptying a std::queue using a scoped queue::swap break any rules?使用作用域 queue::swap 清空 std::queue 是否违反任何规则?
【发布时间】:2019-05-16 21:16:51
【问题描述】:

我有很多使用队列的情况,队列的大小可能会增长到数百个。不幸的是,如果有必要的话,没有一键式的清空队列的解决方案。我想知道是否使用作用域队列进行交换,然后让作用域队列被销毁,是否会破坏任何内存分配/管理规则?

以下 sn-p 是我提议的示例。似乎有效,如果长期使用多次,则不确定结果。

#include <cstdlib>
#include <iostream>
#include <queue>
int
main()
{
    std::queue<int> foo;
    foo.push(10);
    foo.push(20);
    foo.push(30);
    std::cout << "size of before foo: " << foo.size() << '\n';

    {
        std::queue<int> bar;
        swap(foo, bar);
    }
    std::cout << "size of after foo: " << foo.size() << '\n';
    return 0;
}

【问题讨论】:

  • 应该没问题:std::queue&lt;&gt;::swap()。但是既然你没有在std::queue&lt;&gt;的实例化中指定不同的容器,为什么不直接使用std::deque&lt;&gt;std::deque::clear()呢?
  • @Swordfish 据我了解,我正在使用的代码的原作者希望确保使用的队列结构强制 FIFO。就认证而言,这是一个安全问题。然而,他清除队列的解决方案是循环执行弹出直到为空。为了保持一致,我决定使用队列,而不是双端队列,但如果有必要,我正在寻找一种更清洁的方式来消除它。好问题。

标签: c++ scope queue


【解决方案1】:

您的代码很好。 swap 将使foo 成为默认构造的std::queue,当bar 在作用域结束时被销毁时,它将释放foo 正在使用的内存。由于您没有使用newdelete,所以没有问题,因为std::queue“做正确的事情”(RAII 类型是一件很棒的事情)

实际上你已经完成了

std::queue<int>{std::move(foo)}; // move foo into a temporary that is immediately destroyed to release the storage

但是您的方法为您提供了关于foo 状态的更强保证。您的方法使 foo 处于默认构造状态,而上述方法使其处于有效但未指定的状态。


另一种选择是使用Is there a way to access the underlying container of STL container adaptors? 中提供的解决方案之一从foo 获取底层容器并对其调用clear。看起来像

#include <cstdlib>
#include <iostream>
#include <queue>

// function from https://stackoverflow.com/a/29325258/4342498 by jxh: https://stackoverflow.com/users/315052
template <class ADAPTER>
typename ADAPTER::container_type & get_container (ADAPTER &a)
{
    struct hack : ADAPTER {
        static typename ADAPTER::container_type & get (ADAPTER &a) {
            return a.*&hack::c;
        }
    };
    return hack::get(a);
}

int main()
{
    std::queue<int> foo;
    foo.push(10);
    foo.push(20);
    foo.push(30);
    std::cout << "size of before foo: " << foo.size() << '\n';

    get_container(foo).clear();

    std::cout << "size of after foo: " << foo.size() << '\n';
    return 0;
}

【讨论】:

    【解决方案2】:

    这不仅是完全安全的,它也是容器的移动构造函数通常的工作方式:通过与一个短期(或至少即将被销毁)的其他对象交换,然后让其他对象死亡。然后,析构函数会尽快为您完成所有数据清理工作。 (这里可以解决缺少 clear() 成员函数的问题。)

    我认为如果我需要一次性“清除”操作,并且我真的很想使用队列(例如,像你说的那样强制 FIFO),那么我会做同样的事情。

    虽然如果您可以让旧容器超出范围并切换到使用新声明的容器,那就更好了。

    【讨论】:

      【解决方案3】:

      不应该违反任何规则,但你可以这样做

      foo = std::queue&lt;int&gt;{};

      【讨论】:

      • @即使我在这个逻辑火车的某个时候考虑过你的建议。我在我们的微处理器中循环运行了一个测试样本 1,000,000 次,可执行文件的内存空间大小显着增长——并且一直保持这种状态。我不确定为什么。但是该应用程序受到严重的空间限制,这产生了它自己的问题。
      • @Weedware,由于内存碎片导致的堆增长是内存分配算法和分配模式的问题。也许您应该考虑为您的容器提供一个特殊的自定义分配器。
      【解决方案4】:

      这完全没问题,即使在很长一段时间内做了很多次。如果销毁非空队列是合法的,并且交换队列是合法的,那么交换和销毁是合法的。

      我会说将队列(或任何其他容器)交换到临时对象是在线程感知上下文中清理容器的首选方式:

      void MyClass::Reset()
      {
          // ...
          {
              Container      tmp;
              {
                  std::scoped_lock     lock(m_mutex);
                  tmp.swap(m_container);
              }
          }
          // ...
      }
      

      原因是,

      1. 更短的临界区。因为同步锁只需要在相当短的交换操作期间获得。
      2. 减少了死锁的范围。 因为在释放锁后调用了所包含对象的析构函数。但这当然取决于那些析构函数可以调用什么。

      【讨论】:

        【解决方案5】:

        不需要范围:

        std::queue<int>().swap(foo);
        

        否则,您的代码是有效的。

        在这里,我们创建一个临时的空队列。然后我们将其状态与具有大量状态的队列交换;所以foo 没有状态,临时队列有foo 的旧状态。

        然后在语句结束时,我们销毁临时队列以及foo 的旧状态。

        还有一个简短的,基本上适用于所有类型:

        template<class T>
        void clear( T& t ) {
          using std::swap;
          swap( static_cast<T&>(T()), t );
        }
        

        这不是普通的旧数据。检测我们希望对 T 进行零初始化的情况很棘手。

        无法默认构造的对象 () 将无法在此处编译。

        【讨论】:

        • 您能否提供一两个班轮来解释您的答案?我想我明白了,但在我看来,它正在将队列交换为空。直觉上,至少对我来说,我不确定这项技术。
        • @Weedware 是的,你把你的队列换成了虚无,而你队列中剩下的就是......什么都没有。没有什么来自无,但没有什么可以带走一切。
        猜你喜欢
        • 1970-01-01
        • 2014-08-22
        • 2022-01-20
        • 1970-01-01
        • 2010-11-18
        • 2015-08-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多