Is C++ too complex?

Started by
121 comments, last by Vortez 11 years, 4 months ago
Is the following C++ pseudocode (assuming C++03) bad/evil/dangerous? ... Instruction reordering isn't bad in this example
That's probably the most common/acceptable use of volatile -- telling the compiler that it should definitely read that boolean each iteration, instead of optimising it to a single read -- especially in cases where reordering isn't a concern.
This is only something that works in practice though, and is reliant on assumptions about your hardware. There's no requirement in C++03 that when one thread writes a value of 'true' to the boolean, that this value will ever become visible to other threads, volatile or not.
since reads/writes are atomic
This is another hardware-specific detail, not specified by C++03.
And what C++11 data types would be appropriate here?
In your case, [font=courier new,courier,monospace]atomic[/font] and [font=courier new,courier,monospace]memory_order_relaxed[/font]. Deciding that locks are too slow though is an optimisation issue.

One thing that bugs me is that MSVC's [font=courier new,courier,monospace]volatile[/font] has acted like C++11's [font=courier new,courier,monospace]atomic[/font] (with [font=courier new,courier,monospace]memory_order_seq_cst[/font]) since VS2005 -- i.e. on x86, it uses [font=courier new,courier,monospace]cmpxchg[/font]-type instructions. This is because too many people wrote bad [font=courier new,courier,monospace]volatile[/font]-based code that should be wrong due to re-ordering issues, so Microsoft changed the meaning of [font=courier new,courier,monospace]volatile[/font] to include a full memory fence (no read/write can be reordered past a volatile read/write), to fix people's buggy code, which just encourages people to write more buggy code that will break on other C++ compilers...

With your loop, using MSVC's [font=courier new,courier,monospace]volatile[/font] or C++11's [font=courier new,courier,monospace]atomic[/font], you get one fully-fenced read per iteration. Using locks, and assuming no contention (wink.png) you get a fenced read, a regular read, and a fenced write per iteration, which isn't much different. Taking contention into account though, you also might get a busy-wait with repeated fenced reads, and possibly a context-switch.
Aside from these performance differences, there's sometimes theoretical reasons to want a particular kind of non-blocking guarantee, which is a better reason to avoid locks. N.B. some kinds of lock-free systems will have worse performance than locking ones, but do so because they require the guarantee for whatever reason.
almost all of those volatile overloads I mentioned are for the C++11 threading library
Are there any valid use-cases for [font=courier new,courier,monospace]volatile[/font] in multi-threaded code, aside from ones like the above that can be replaced with [font=courier new,courier,monospace]atomic[/font]?
Advertisement
I'm not the one who put in those overloads. The standards committee put in the volatile overloads only for threading library. If you've got a problem with threading and volatile being associated by the C++ standard library, you'll have to take it up with them. I'm just reporting the fact that they are.
[quote name='Hodgman' timestamp='1354933996' post='5008330']
Are there any valid use-cases for volatile in multi-threaded code, aside from ones like the above that can be replaced with atomic?
I'm not the one who put in those overloads.[/quote]That was a genuine bit of curiosity on my part. If the standards committee has done this, then there must be some use for it... but in my experience [font=courier new,courier,monospace]atomic[/font] solves most of them. I was hoping someone could teach me a genuine situation where we'd need [font=courier new,courier,monospace]volatile[/font] in threaded code.

[quote name='Cornstalks' timestamp='1354931469' post='5008309']And what C++11 data types would be appropriate here?
In your case, [font=courier new,courier,monospace]atomic[/font] and [font=courier new,courier,monospace]memory_order_relaxed[/font].[/quote]

Would memory_order_acquire/memory_order_release be more appropriate? I guess you would want the two threads to agree on the order of writes.

I'm also in the camp that gets extremely bothered by the fact that in C#/Java/etc there's no separation between declaration and implementation. I really love the fact that .h files can just show you the what without the how.


You could argue that the separation between declaration and implementation is built into the language in C# and java using interfaces, as opposed to C++s frankly demented compilation model.


I often use PIMPL-style idioms, where I'm writing the same thing 3 times, instead of twice, but still prefer it over C# style classes... It's not like writing lines of code is actually anyone's bottleneck or main time-sink when programming anyway.


No but the best code is the code you don't have to write. I'm not saying it's a huge burden, but it's just unnecessary.

A C# interface is a much cleaner expression of intent than a header file
if you think programming is like sex, you probably haven't done much of either.-------------- - capn_midnight
Someone just down-voted all my posts in this thread about volatile -- I'd appreciate being told why I'm wrong as well as that I am wrong, so I (and others) can learn from my mistake.


To explain why I cringe at the sight of [font=courier new,courier,monospace]volatile[/font] in C++ code that isn't interacting with special hardware registers or memory mappings:

Volatile is so often misunderstood that some code-bases check for it in commit logs and automatically flag it as a bug.
See: http://kernel.org/do...red-harmful.txt
See also the "double checked locking is broken" fiasco.

Volatile isn't required in threaded code if you're already using synchronisation primitives like mutexes or semaphores.

Carnegie Mellon teach in their secure coding materials, not to use [font=courier new,courier,monospace]volatile[/font] as a synchronisation primitive, because it's not required to work as one.

It's taught that when using POSIX threads, you shouldn't ever use volatile, because pthreads' synchronisation primitives make it unnecessary.

HP/IBM employees opine to remind us that volatile isn't required if using synchronisation primitives, that volatile variables aren't necessarily atomic, and that the actual details are always platform specific anyway.

When it's used as a synchronisation primitive in threaded code, it's often incorrect, for example:
int data = 42;
volatile bool ready = false;
void Thread1()
{
data = 100;
ready = true;
}
void Thread2()
{
while(!ready){Sleep();}
printf("%d", data );
}
The above code will print 100 on a 2005+ Microsoft compiler, but is not required to do so according to the C++ standard. A compliant C++ compiler will likely print 100 or 42, but could print anything. The write of "[font=courier new,courier,monospace]ready = true[/font]" is allowed to become visible to other threads before the write of "[font=courier new,courier,monospace]data = 100[/font]" does. N.B. even if you make data volatile, the order that the changes become visible in [font=courier new,courier,monospace]Thread2[/font] is still unspecified.

You can't correctly implement the above synchronisation using just volatile. Maybe this works on your compiler and your CPU, but it's not required to. Because of this, if you're writing multi-threaded code that uses volatile to synchronise data between threads, you've likely got a bug (maybe not on your current platform if you're lucky).

In order to correctly implement thread synchronisation in the above example, you need to be aware of the memory fences required on your target hardware (or use C++11's atomic and memory ordering functionality); volatile doesn't address these issues at all... Well, on MSVC it does (MSVC inserts full fences around volatile accesses, well beyond what is required by the spec), which is very bad IMHO, because this causes people write incorrect code which happens to work until they port it to another platform...

To paraphrase Herb Sutter, if you're looking for [font=courier new,courier,monospace]volatile[/font] in C++, you probably really want [font=courier new,courier,monospace]atomic[/font].

You could argue that the separation between declaration and implementation is built into the language in C# and java using interfaces, as opposed to C++s frankly demented compilation model.

A C# interface is a much cleaner expression of intent than a header file
If you make a pure virtual class in C++, how is that not a clean expression of intent?
You misinterpreted ChaosEngine.

It's not about the C++ runtime model (on which the pure virtual class acts like an interface indeed), what he criticized was the source code allegedly separation between declaration (header files) and implementation (.cpp files), which IMO violates the DRY principle. More so, AFAIK the compiler and the preprocessor don't enforce this source code separation, so it's sintatically legal to write code in the header files.
Not only is it syntactically legal to write code in the header files: Templated code has to go there.
Yeah, totally forgot about the templates.

So one could argue that the header/source-code model fail to provide a clear separation between declaration and implementation, becoming (taking in account only this feature) ultimatelly a hindrance to the programmer, that must keep two files in sync instead of only one.

This topic is closed to new replies.

Advertisement