3

Suppose we have a situation where we need FIFO data structure. For example, consume some events in the order they came in.

Additionally, we need to clear the entire queue from time to time.

std::queue seems like the perfect fit for doing that, but unfortunately it lacks a function for clearing the container.

So at this point, we have 2 alternatives:

std::queue

  • we asked the STL lib what we need. Granted, the STL lib will give us more: it will give us an std::deque disguised as a std::queue
  • we got back only a part from what we need, namely the pop front and push back but without clear
  • we will have to "emulate" clear somehow, without the naive way of looping and popping

std::deque

  • we asked the STL lib what we need
  • we got back what we asked for, but we've got too much: we also got push front and pop back

Overall, we either received too few or too much, never exactly what we really wanted.

Here is the thing that took me by surprise, while I was trying to provide clear functionality for using with std::queue which is a member var of my object

struct message
{
};
struct consumer
{
    std::queue<message> _pending;

    void clear_ver_1()
    {
       auto will_be_deleted_when_out_of_scope = std::move(_pending);
    }

    void clear_ver_2()
    {
        std::queue<message> will_be_deleted_when_out_of_scope;
        _pending.swap(will_be_deleted_when_out_of_scope);
    }
};

I've read the specs and I can not say for sure if clear_ver_1 will leave the _pending in a valid but unspecified state or not. See the string example there.

I'm quite surprised that the specs are so vague about this topic. Am I not looking in the right place?

Thank you all!

Update

It seems there is a non-ignorable difference between assigning and clearing. Internally, queue and deque are almost the same (one is using the other one)

enter image description here

shiretu
  • 303
  • 2
  • 10
  • 1
    Looks like `clear_ver_2` is the way to go: https://stackoverflow.com/questions/709146/how-do-i-clear-the-stdqueue-efficiently – wohlstad Mar 31 '22 at 14:36
  • 1
    *I'm quite surprised that the specs are so vague about this topic* -- ["Unless otherwise specified, all standard library objects that have been moved from are placed in a "valid but unspecified state"](https://en.cppreference.com/w/cpp/utility/move) – PaulMcKenzie Mar 31 '22 at 14:37
  • @PaulMcKenzie: good point. It also continues with ```meaning the object's class invariants hold (so functions without preconditions, such as the assignment operator, can be safely used on the object after it was moved from)```. So that means std::queue::front is safe to call? it has no precoditions (https://en.cppreference.com/w/cpp/container/queue/front) – shiretu Mar 31 '22 at 14:46

3 Answers3

5

The usage of std::move

std::move isn't meant to be used that way. You should only use std::move, well, to move an object to somewhere else in the program when you won't be using it anymore. As you said, it is then left in a valid but unspecified state:

  • Valid because it is completely safe for destruction;
  • Unspecified because you should not be accessing that object anymore.

std::queue vs std::deque

If you only plan to use FIFO functionality, I would recommend using std::queue. It really makes it clear that you will only use std::deque as a FIFO data structure — clarity is kind of the only reason for std::queue to exist in the first place.

Clearing a std::queue

You can just assign it to an empty std::queue, or as you did it, swap it for for an empty one.

// ...

struct consumer
{
    std::queue<message> _pending;

    void clearQueue()
    {
        _pending = {};
    }
};

Edited according to DevSolar's comment

Debaug
  • 452
  • 4
  • 14
  • 1
    As usual, if your solution looks complicated, that's usually because you did it wrong. ;-) I would not bother with a *function* `clearQueue` either. Just `_pending = {}` will do nicely and be readily understood. – DevSolar Mar 31 '22 at 15:00
  • 1
    @DevSolar: then it begs the question: why are there containers with clear() and containers without? What is so special about a queue that it does not have that? I figure is common sense functionality when working with a FIFO: be able to clear it. – shiretu Mar 31 '22 at 15:03
  • 2
    @DevSolar: I'm even more confused why deque has clear. It also can be reset with ` = {}`. Yet, it has explicit clear() – shiretu Mar 31 '22 at 15:05
  • @shiretu you brought valid points. – balu Mar 31 '22 at 15:26
  • @DevSolar I honestly didn't think about OP's existing code... And I often wrap things up in functions when writing SO answers. Edited – Debaug Mar 31 '22 at 15:33
  • 1
    @shiretu: `std::queue` is not a container, just like `std::stack` isn't. These are container *adaptors*, which wrap a semantic API (that of a queue, or a stack) around an actual container type. The default container of `std::queue` is `std::deque`. You could also declare a `std::queue< message, std::map >` if you'd like to. Apparently the designers of these adaptors disagreed with you on the matter of them being in the business of being `clear()`ed. (I am struggling to think why you *would* `clear()` a FIFO list and lose those events...) – DevSolar Mar 31 '22 at 15:52
  • 1
    @balu: See above. Containers can be `clear()`ed, container adaptors cannot. – DevSolar Mar 31 '22 at 15:52
  • @DevSolar: because of a logical reset, just as an example. The reasons are countless. Is not like one needs to think long and hard about why clear would be needed. One needs to think long and hard as to why clear is NOT needed in any kind of situation for a FIFO, therefore not putting it into the STD. – shiretu Mar 31 '22 at 17:26
  • @shiretu: You seem very adamant on getting a data type that *exactly* matches your needs, no more, no less. Well, the standard committee apparently disagreed with you on what is "strictly required" for `std::queue`, so you are "stuck" with `std::deque`. Is that so much of a problem? – DevSolar Mar 31 '22 at 17:51
  • @DevSolar: You read this the wrong way. Please understand that I'm not adamant about the structure/container/etc. There is nothing to be adamant about, as there is nothing I can do to change anything inside STD. However, I do want to know the magic behind this decision of excluding clear from that. If you think you know the reasons, please share them. I bet there are a lot more ppl out there wanting to know the reasoning behind this design. Magic designs are... magic. – shiretu Mar 31 '22 at 17:57
  • @shiretu: `clear()` was not considered necessary for a `std::queue`, simple as that. You *can* set out to change that, the C++ committee accepts contributions. I just don't get why this is even something you would ask. `queue` has less functions than `deque` to make it minimal, and `clear()` wasn't considered part of "minimal". {shrug} – DevSolar Mar 31 '22 at 18:01
  • @DevSolar: stating the obvious (that it was not considered necessary) is not a satisfactory answer. I already see that. It can't be more obvious than that. Is like asking someone why do you do this and that, and they reply with "I'm doing this and that". Got it! But why? – shiretu Mar 31 '22 at 18:07
1

we got back what we asked for, but we've got too much: we also got push front and pop back

You got exactly what you asked for, a dequeue is a data structure that allows efficient inserts and deletes at either end point. It might not be the data structure for you, but that's your fault for choosing it.

we will have to "emulate" clear somehow, without the naive way of looping and popping

For the record, popping is extremely cheap in terms of performance, it simply decrements a number. A pop in a while loop translates to decrementing an integer until 0, which unless you have a lot of numbers is very fast. In fact, it's probably much faster than allocating memory, which brings us to:

The STL way of clearing these collection classes is to swap them with an empty collection (which is what you figured out on your own) or to just straight up re-allocate them in place (apple's answer). Both of those will (probably, the standard is vague about this point) allocate memory, which is a very expensive linear operation.

You have all the pieces to do it, though I'd suggest profiling to see which way is faster if it really matters to you. Personally I just pop the queue in a loop, it leaves the allocated memory in place for the next time I need to push more, so it saves on potentially multiple allocations and re-allocations (when compared to resetting the queue), depending on the amount of data you have.

Blindy
  • 65,249
  • 10
  • 91
  • 131
  • My point regarding asking for std::deque from STL is that if I want clear() too, I must ask for deque, a kinda forced option. So not really my fault as a user, as there are no good alternatives. Conceptually I asked for the pop front, push back, and clear, but was provided with one alternative which did extra stuff. – shiretu Mar 31 '22 at 14:58
  • Conceptually you asked for pop front, push back and clear, got no such collection, and picked something else. `dequeue`, `vector`, whatever, you made that alternative choice when you found nothing in the STL that had your requirements. – Blindy Mar 31 '22 at 15:35
  • Indeed. That's what happened. But perhaps we can be a little less pedantic and understand the core issue here: when you need a $5 tool for doing something, and your only alternative is that tool but a professional version that does extra stuff and costs $150, what do you do!? You either buy or make your own. If you do buy it, it is not precisely a wholehearted decision. Yes, I took that decision; I stand by it. But more like coerced. – shiretu Mar 31 '22 at 17:33
  • see the updated post. It seems popping is cheap, you were right – shiretu Mar 31 '22 at 18:37
0

To clear the queue, you can also simply write

_pending = {};

Note: the first method may not work and should not be rely on, since moved-form object are in valid but unspecified state.

apple apple
  • 10,292
  • 2
  • 16
  • 36