to const or not to const

J

James Kanze

I recently dived into multi-threaded issues. To make my life a
little easier, I decided that it would be nice to have a
simple fifo buffer to ease communications between threads. The
interface is this:
template < typename T >
class fifo {
public:
typedef T value_type;
typedef std::size_t size_type
fifo ( size_type n = -1 );
void push ( value_type const & value );
value_type pop ( void );
// blocks until an item is available
bool available ( void ) const;
// returns true if the a call to pop() would not block
void wait_available ( void ); // should this be const ?
// blocks until a call to pop() will not block
};
Typically there are some threads push()ing items into the fifo
and one thread pop()ing items off the buffer and processing
them.

If there's more than one thread popping, available and
wait_available are useless, of course.

In my own code, I've handled this by providing a timeout on pop:
if zero, it returns immediately, and if infinity, it behaves
effectively as your pop. But I've also had cases where it made
sense to wait at most 100 milliseconds, or such. (This solution
also avoids the const problem:).)
It is clear that push() and pop() should not be const:
they change the contents of the buffer. Also, availble()
should be const since it just returns a property of the buffer
without modifying it.
But, what about wait_available()? Even though it does not
change the buffer contents by itself, once it returns it
effectively signals to the thread where it was called that
another thread has pushed in item. So it marks a change of the
buffer, even though it does not in itself bring that change
about. Should this method be const or not?

Does it change the (logical) contents of the buffer or not?

Practically speaking, however: is there the slightest reason to
worry about const on this class? Are there any real scenarios
where one might pass a const reference to it, or not have access
to the non-const instance for some reason?
 
J

James Kanze

I was thinking along the same lines. Would it be possible in
C++0X to write pop() with a strong exception safety guarantee?

Not for an arbitrary object. In practice, interthread
communication doesn't deal with arbirary objects; in practice,
it's usually a pointer. And you can give the strong exception
guarantee for pointers, since copying one doesn't throw.
 
K

Kai-Uwe Bux

James said:
Not for an arbitrary object. In practice, interthread
communication doesn't deal with arbirary objects; in practice,
it's usually a pointer. And you can give the strong exception
guarantee for pointers, since copying one doesn't throw.

Thanks. I was afraid of that.

I wonder, though. The implementation of pop() is something like:

value_type pop ( void ) {
posix::guard data_guard ( data_lock );
while ( true ) {
if ( ! the_data.empty() ) {
value_type result = the_data.front();
the_data.pop();
if ( the_data.size() < max_size ) {
non_full.signal();
}
return ( result );
} else {
data_guard.wait( non_empty );
}
}
}

where the_data is a std::queue. The copy-constructor initializing result
comes before the_data.pop(). If we had a guarantee that the compiler uses
RVO in this case, maybe, one would be back in business :)


Best

Kai-Uwe Bux
 
K

Kai-Uwe Bux

James said:
If there's more than one thread popping, available and
wait_available are useless, of course.
Yes.

In my own code, I've handled this by providing a timeout on pop:
if zero, it returns immediately, and if infinity, it behaves
effectively as your pop. But I've also had cases where it made
sense to wait at most 100 milliseconds, or such. (This solution
also avoids the const problem:).)

I think, timeouts address a slightly different problem. Here is what I use
available() for when there is only one consumer:

while ( true ) {
if ( ! fifo.available() ) {
update_screen();
}
event e = fifo.pop();
e.handle();
}

This loop handles events as long as they are available and only when there
is a slow down on the producer side, the screen is updated.
Does it change the (logical) contents of the buffer or not?

Well, you could have something like this in the consumer thread:

bool b; // only introduced for exposition
while ( b = buffer.available() ) // really = not ==
{
buffer.pop()
}
// now, b is false, i.e., the last call to available() returned false.
buffer.wait_available();
assert( b != buffer.available() ) // available() now returns true.

The point is: even though wait_available() does not alter the contents of
the queue, in the case of a single consumer you can _know_ that the queue is
non-empty immediately after wait_available() returns. In that sense, it
changes the logical state of the queue from [unknown] to [known to be non-
empty].

(Since we agree that wait_available() is useless in the case of several
consumer threads, I only discuss the remaining use case.)
Practically speaking, however: is there the slightest reason to
worry about const on this class? Are there any real scenarios
where one might pass a const reference to it, or not have access
to the non-const instance for some reason?

No, it's not a question of practical relevance. But, through this example I
realized that my intuition about what is "logical constness" gets shaky when
there are multiple threads involved. (I am slowly getting ready for C++0X, I
guess.)


Best

Kai-Uwe Bux
 
J

Jonathan Lee

where the_data is a std::queue. The copy-constructor initializing result
comes before the_data.pop(). If we had a guarantee that the compiler uses
RVO in this case, maybe, one would be back in business :)

Why don't you use strict ownership semantics, like auto_ptr?
i.e., don't copy your event object around, just the pointer
to it. Qt does this (see QCoreApplication::postEvent).

(Well, actually Qt just straight passes the pointer and
documents the ownership change, requiring the event object
to be allocated on "the heap")

--Jonathan
 
K

Kai-Uwe Bux

Jonathan said:
Why don't you use strict ownership semantics, like auto_ptr?

Thanks for the suggestion. I think, unique_ptr could be slightly more
appropriate for communication between threads: internally, the fifo<T> uses
a queue and I have the vague recollection that auto_ptr<T> does not fare
well in containers. (I have to admit, that I am not sure about unique_ptr
either:)

However, it is somewhat unfortunate that you snipped the context: the
question has migrated and in this subthread became "is it possible to
implement pop() with strong exception safety guarantee".

It's not a practical concern to _use_ fifo<T> in an exception safe manner.
Just make sure that the constructors of T don't throw. But that we have ways
of using fifo<T> in an exception safe way is not the same as finding an
implementation for pop() that does that out of the box for any type.


Best

Kai-Uwe Bux
 
J

Jonathan Lee

I have the vague recollection that auto_ptr<T> does not fare
well in containers

You'll have to be careful, of course.
However, it is somewhat unfortunate that you snipped the context: the
question has migrated and in this subthread became "is it possible to
implement pop() with strong exception safety guarantee".

Sorry, it's always tricky trimming. Too little, or too much...

Anyway, it seemed that your concern was the copy constructor
might throw. But James had just confirmed for you that copying
pointers
is exception safe. All I'm suggesting is "avoid the copy constructor"
(by moving the pointer along, safely).

--Jonathan
 
J

James Kanze

Thanks for the suggestion. I think, unique_ptr could be
slightly more appropriate for communication between threads:
internally, the fifo<T> uses a queue and I have the vague
recollection that auto_ptr<T> does not fare well in
containers.

It doesn't, but there's no law that the container has to use the
same type as the interface. My multithread queues all use
std::auto_ptr at the interface, and raw pointers internally,
with something like:
m_data.push_back(in.get());
in.release();
when writing (to ensure that the pointer actually is in the
deque before calling release). And:
std::auto_ptr<T> result(m_data.front());
m_data.pop();
when reading. Depending on the context, you can either delete
anything left in the queue in the destructor, or assert that the
queue is empty when the destructor is called.
(I have to admit, that I am not sure about unique_ptr
either:)

I thought one of the goals was to allow it to be used in a
container. (But I've not looked into the details.)
However, it is somewhat unfortunate that you snipped the
context: the question has migrated and in this subthread
became "is it possible to implement pop() with strong
exception safety guarantee".
It's not a practical concern to _use_ fifo<T> in an exception
safe manner. Just make sure that the constructors of T don't
throw. But that we have ways of using fifo<T> in an exception
safe way is not the same as finding an implementation for
pop() that does that out of the box for any type.

Yes. I was just responding to the immediate question. In
practice, I've not found this to be an issue in interthread
communication, because you're not normally using types whose
copy constructor might throw.
 
J

James Kanze

I think, timeouts address a slightly different problem.

Fundamentally, yes. They can do a lot more than you need. But
timeouts of 0 and infinity effectively support what you seem to
need as well.
Here
is what I use available() for when there is only one consumer:
while ( true ) {
if ( ! fifo.available() ) {
update_screen();
}
event e = fifo.pop();
e.handle();
}

update_screen(); // if necessary before the first event.
while ( true ) {
event e = fifo.pop(infinite_timeout);
while (e.valid()) {
e.handle();
e = fifo.pop(timeout_0);
}
update_screen();
}

I think that basically does the same thing as your loop.

Of course, if you don't already have a queue with timeouts, and
you don't need it, your solution would probably be simpler to
implement, and thus more appropriate. (You don't, for example,
have to provide an invalid event state---although this is
already present if your queue returns an std::auto_ptr.)

(It's interesting to note that while the two versions do the
same thing, they say something different to the reader. Yours
says: if there's nothing to do, update the screen; then wait for
an event. Mine says process all events that have arrived, then
update the screen.)
This loop handles events as long as they are available and
only when there is a slow down on the producer side, the
screen is updated.

Well, you could have something like this in the consumer thread:

What the client code does after the function is irrelevant to
whether the function is "const". A function is const if 1) it
doesn't modify the internal state (or what is considered the
internal state of that object) of the object, and 2) it doesn't
provide a "backdoor", a means by which the client code can
modify the internal state of the object without referring to the
object.
bool b; // only introduced for exposition
while ( b = buffer.available() ) // really = not ==
{
buffer.pop()
}
// now, b is false, i.e., the last call to available() returned false.
buffer.wait_available();
assert( b != buffer.available() ) // available() now returns true.
The point is: even though wait_available() does not alter the
contents of the queue, in the case of a single consumer you
can _know_ that the queue is non-empty immediately after
wait_available() returns.

So, should std::vector<>::size() be non-const, simply because
the client code can know something about the vector after that,
and perhaps call a non-const function on it as a consequence of
that knowledge? By that logic, nothing can be const.
In that sense, it changes the logical state of the queue from
[unknown] to [known to be non- empty].

The logical state of the queue is never unknown to the queue.
And that's all that matters: see above: by your logic,
std::vector<>::size() changes the logical state of the vector
from [size unknown] to [size known to be some specific value].
(C++ objects do not involve quantum mechanics: observing the
state does not "fix" it, where it was undetermined before.)
(Since we agree that wait_available() is useless in the case
of several consumer threads, I only discuss the remaining use
case.)
No, it's not a question of practical relevance. But, through
this example I realized that my intuition about what is
"logical constness" gets shaky when there are multiple threads
involved. (I am slowly getting ready for C++0X, I guess.)

"Logical const-ness" is a tenuous concept when different
entities have different views of an object. State that is
internal, and irrelevant to const, in one view might be exposed
in another.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,768
Messages
2,569,574
Members
45,050
Latest member
AngelS122

Latest Threads

Top