I know, it has been made quite clear in a couple of questions/answers before, that volatile
is related to the visible state of the c++ memory model and not to m
This catches some kinds of thread-unsafe code (concurrent access), but misses others (deadlocks due to locking inversion). Neither is especially easy to test for, so it's a modest partial win. In practice, remembering to enforce a constraint that a particular private member is accessed only under some specified lock, hasn't been a big problem for me.
Two answers to this question have demonstrated that you're correct to say that confusion is a significant disadvantage - maintainers may have been so strongly conditioned to understand that volatile's memory-access semantics have nothing to do with thread-safety, that they will not even read the rest of the code/article before declaring it incorrect.
I think the other big disadvantage, outlined by Alexandrescu in the article, is that it doesn't work with non-class types. This might be a difficult restriction to remember. If you think that marking your data members volatile
stops you using them without locking, and then expect the compiler to tell you when to lock, then you might accidentally apply that to an int
, or to a member of template-parameter-dependent type. The resulting incorrect code will compile fine, but you may have stopped examining your code for errors of this kind. Imagine the errors which would occur, especially in template code, if it was possible to assign to a const int
, but programmers nevertheless expected the compiler would check const-correctness for them...
I think the risk that the data member's type actually has any volatile
member functions should be noted and then discounted, although it might bite somebody someday.
I wonder if there's anything to be said for compilers providing additional const-style type modifiers via attributes. Stroustrup says, "The recommendation is to use attributes to only control things that do not affect the meaning of a program but might help detect errors". If you could replace all mentions of volatile
in the code with [[__typemodifier(needslocking)]]
then I think it would be better. It would then be impossible to use the object without a const_cast
, and hopefully you wouldn't write a const_cast
without thinking about what it is you're discarding.
In the article the keyword is used more like a
required_thread_safety
tag than the actual intended use of volatile.
Without having read the article – why isn’t Andrei using said required_thread_safety
tag then? Abusing volatile
doesn’t sound such a good idea here. I believe this causes more confusion (like you said), rather than avoiding it.
That said, volatile
may sometimes be required in multi-threaded code even if it’s not a sufficient condition, just to prevent the compiler from optimizing away checks that rely on asynchronous update of a value.
Look at this from a different perspective. When you declare a variable as const, you are telling the compiler that the value cannot be changed by your code. But that doesn't mean that the value won't change. For example, if you do this:
const int cv = 123;
int* that = const_cast<int*>(&cv);
*that = 42;
...this evokes undefined behavior according to the standard, but in practice something will happen. Maybe the value will be changed. Maybe there will be a sigfault. Maybe flight simulator will launch -- who knows. The point is you don't know on a platform-independant basis what's going to happen. So the apparent promise of const
is not fulfilled. The value may or may not actually be const.
Now, given that this is true, is using const
an abuse of the language? Of course not. It is still a tool that the language provides to help you write better code. It will never be the end-all, be-all tool to ensure that values remain unchanged -- the programmer's brain is ultimately that tool -- but does that make const
unuseful?
I say no, using const as a tool to help you write better code is not an abuse of the language. In fact I'd go one step further, and say it is the intent of that feature.
Now, the same is true of volatile. Declaring something as volatile will not make your program thread safe. It probably won't even make that variable or object thread safe. But the compiler will enforce CV-qualification semantics, and careful programmer can leverage this fact to help him write better code by helping the compiler to identify places where he might be writing a bug. Just like the compiler helps him when he tries to do this:
const int cv = 123;
cv = 42; // ERROR - compiler complains that the programmer is potentially making a mistake
Forget about memory fences and atomicity of volatile objects and variables, just like you have long forgotten about cv
's true constness. But use the tools that the language gives you to write better code. One of those tools is volatile
.
You must better not do that. volatile was not even invented to provide thread-safety. It was invented to access memory-mapped hardware registers properly. volatile keyword has no effect over CPU's out-of-order execution feature. You should use proper OS calls or CPU defined CAS instructions, memory fences, etc.
CAS
Memory Fence
Building on other code and removing the need for the volatile specifier entirely, this not only works, but correctly propagates const (similar to iterator vs const_iterator). Unfortunately, it requires quite a bit of boilerplate code for the two interface types, but you don't have to repeat any logic of methods: each is still defined once, even if you do have to "duplicate" the "volatile" versions similarly to normal overloading of methods on const and non-const.
#include <cassert>
#include <iostream>
struct ExampleMutex { // Purely for the sake of this example.
ExampleMutex() : _locked (false) {}
bool try_lock() {
if (_locked) return false;
_locked = true;
return true;
}
void lock() {
bool acquired = try_lock();
assert(acquired);
}
void unlock() {
assert(_locked);
_locked = false;
}
private:
bool _locked;
};
// Customization point so these don't have to be implemented as nested types:
template<class T>
struct VolatileTraits {
typedef typename T::VolatileInterface Interface;
typedef typename T::VolatileConstInterface ConstInterface;
};
template<class T>
class Lock;
template<class T>
class ConstLock;
template<class T, class Mutex=ExampleMutex>
struct Volatile {
typedef typename VolatileTraits<T>::Interface Interface;
typedef typename VolatileTraits<T>::ConstInterface ConstInterface;
Volatile() : _data () {}
Volatile(T const &data) : _data (data) {}
Interface operator*() { return _data; }
ConstInterface operator*() const { return _data; }
Interface operator->() { return _data; }
ConstInterface operator->() const { return _data; }
private:
T _data;
mutable Mutex _mutex;
friend class Lock<T>;
friend class ConstLock<T>;
};
template<class T>
struct Lock {
Lock(Volatile<T> &data) : _data (data) { _data._mutex.lock(); }
~Lock() { _data._mutex.unlock(); }
T& operator*() { return _data._data; }
T* operator->() { return &**this; }
private:
Volatile<T> &_data;
};
template<class T>
struct ConstLock {
ConstLock(Volatile<T> const &data) : _data (data) { _data._mutex.lock(); }
~ConstLock() { _data._mutex.unlock(); }
T const& operator*() { return _data._data; }
T const* operator->() { return &**this; }
private:
Volatile<T> const &_data;
};
struct Something {
class VolatileConstInterface;
struct VolatileInterface {
// A bit of boilerplate:
VolatileInterface(Something &x) : base (&x) {}
VolatileInterface const* operator->() const { return this; }
void action() const {
base->_do("in a thread-safe way");
}
private:
Something *base;
friend class VolatileConstInterface;
};
struct VolatileConstInterface {
// A bit of boilerplate:
VolatileConstInterface(Something const &x) : base (&x) {}
VolatileConstInterface(VolatileInterface x) : base (x.base) {}
VolatileConstInterface const* operator->() const { return this; }
void action() const {
base->_do("in a thread-safe way to a const object");
}
private:
Something const *base;
};
void action() {
_do("knowing only one thread accesses this object");
}
void action() const {
_do("knowing only one thread accesses this const object");
}
private:
void _do(char const *restriction) const {
std::cout << "do action " << restriction << '\n';
}
};
int main() {
Volatile<Something> x;
Volatile<Something> const c;
x->action();
c->action();
{
Lock<Something> locked (x);
locked->action();
}
{
ConstLock<Something> locked (x); // ConstLock from non-const object
locked->action();
}
{
ConstLock<Something> locked (c);
locked->action();
}
return 0;
}
Compare class Something to what Alexandrescu's use of volatile would require:
struct Something {
void action() volatile {
_do("in a thread-safe way");
}
void action() const volatile {
_do("in a thread-safe way to a const object");
}
void action() {
_do("knowing only one thread accesses this object");
}
void action() const {
_do("knowing only one thread accesses this const object");
}
private:
void _do(char const *restriction) const volatile {
std::cout << "do action " << restriction << '\n';
}
};
C++03 §7.1.5.1p7:
If an attempt is made to refer to an object defined with a volatile-qualified type through the use of an lvalue with a non-volatile-qualified type, the program behaviour is undefined.
Because buffer_ in your example is defined as volatile, casting it away is undefined behavior. However, you can get around that with an adapter which defines the object as non-volatile, but adds volatility:
template<class T>
struct Lock;
template<class T, class Mutex>
struct Volatile {
Volatile() : _data () {}
Volatile(T const &data) : _data (data) {}
T volatile& operator*() { return _data; }
T const volatile& operator*() const { return _data; }
T volatile* operator->() { return &**this; }
T const volatile* operator->() const { return &**this; }
private:
T _data;
Mutex _mutex;
friend class Lock<T>;
};
The friendship is needed to strictly control non-volatile access through an already locked object:
template<class T>
struct Lock {
Lock(Volatile<T> &data) : _data (data) { _data._mutex.lock(); }
~Lock() { _data._mutex.unlock(); }
T& operator*() { return _data._data; }
T* operator->() { return &**this; }
private:
Volatile<T> &_data;
};
Example:
struct Something {
void action() volatile; // Does action in a thread-safe way.
void action(); // May assume only one thread has access to the object.
int n;
};
Volatile<Something> data;
void example() {
data->action(); // Calls volatile action.
Lock<Something> locked (data);
locked->action(); // Calls non-volatile action.
}
There are two caveats. First, you can still access public data members (Something::n), but they will be qualified volatile; this will probably fail at various points. And second, Something doesn't know if it really has been defined as volatile and casting away that volatile (from "this" or from members) in methods will still be UB if it has been defined that way:
Something volatile v;
v.action(); // Compiles, but is UB if action casts away volatile internally.
The main goal is achieved: objects don't have to be aware that they are used this way, and the compiler will prevent calls to non-volatile methods (which is all methods for most types) unless you explicitly go through a lock.