y = x; // conceptually read x again (see below)
x = 10; // write x
x = 20; // write x again
and optimize it to this:
auto y = x; // conceptually read x (see below)
x = 20; // write x
For special memory, this is clearly unacceptable behavior.
Now, as it happens, neither of these two statements will compile when x
is std::atomic
:
auto y = x; // error!
y = x; // error!
That's because the copy operations for std::atomic
are deleted (see Item 11). And with good reason. Consider what would happen if the initialization of y
with x
compiled. Because x
is std::atomic
, y
's type would be deduced to be std::atomic
, too (see Item 2). I remarked earlier that one of the best things about std::atomic
s is that all their operations are atomic, but in order for the copy construction of y
from x
to be atomic, compilers would have to generate code to read x
and write y
in a single atomic operation. Hardware generally can't do that, so copy construction isn't supported for std::atomic
types. Copy assignment is deleted for the same reason, which is why the assignment from x
to y
won't compile. (The move operations aren't explicitly declared in std::atomic
, so, per the rules for compiler-generated special functions described in Item 17, std:: atomic
offers neither move construction nor move assignment.)
It's possible to get the value of x
into y
, but it requires use of std::atomic
's member functions load
and store
. The load
member function reads a std::atomic
's value atomically, while the store
member function writes it atomically. To initialize y
with x
, followed by putting x
's value in y
, the code must be written like this:
std::atomic y( x.load()); // read x
y.store(x.load()); // read x again
This compiles, but the fact that reading x
(via x.load()
) is a separate function call from initializing or storing to y
makes clear that there is no reason to expect either statement as a whole to execute as a single atomic operation.
Given that code, compilers could “optimize” it by storing x
's value in a register instead of reading it twice:
register = x.load(); // read x into register
std::atomic y( register ); // init y with register value
y.store( register ); // store register value into y
The result, as you can see, reads from x
only once, and that's the kind of optimization that must be avoided when dealing with special memory. (The optimization isn't permitted for volatile
variables.)
The situation should thus be clear:
• std::atomic
is useful for concurrent programming, but not for accessing special memory.
• volatile
is useful for accessing special memory, but not for concurrent programming.
Because std::atomic
and volatile
serve different purposes, they can even be used together:
volatile std::atomicvai; // operations on vai are
// atomic and can't be
// optimized away
This could be useful if vai
corresponded to a memory-mapped I/O location that was concurrently accessed by multiple threads.
As a final note, some developers prefer to use std::atomic
's load
and store
member functions even when they're not required, because it makes explicit in the source code that the variables involved aren't “normal.” Emphasizing that fact isn't unreasonable. Accessing a std::atomic
is typically much slower than accessing a non- std::atomic
, and we've already seen that the use of std::atomic
s prevents compilers from performing certain kinds of code reorderings that would otherwise be permitted. Calling out loads and stores of std::atomic
s can therefore help identify potential scalability chokepoints. From a correctness perspective, not seeing a call to store
on a variable meant to communicate information to other threads (e.g., a flag indicating the availability of data) could mean that the variable wasn't declared std::atomic
when it should have been.
This is largely a style issue, however, and as such is quite different from the choice between std::atomic
and volatile
.
Things to Remember
• std::atomic
is for data accessed from multiple threads without using mutexes. It's a tool for writing concurrent software.
• volatile
is for memory where reads and writes should not be optimized away. It's a tool for working with special memory.
For every general technique or feature in C++, there are circumstances where it's reasonable to use it, and there are circumstances where it's not. Describing when it makes sense to use a general technique or feature is usually fairly straightforward, but this chapter covers two exceptions. The general technique is pass by value, and the general feature is emplacement. The decision about when to employ them is affected by so many factors, the best advice I can offer is to consider their use. Nevertheless, both are important players in effective modern C++ programming, and the Items that follow provide the information you'll need to determine whether using them is appropriate for your software.
Item 41: Consider pass by value for copyable parameters that are cheap to move and always copied.
Some function parameters are intended to be copied. [22] In this Item, to “copy” a parameter generally means to use it as the source of a copy or move operation. Recall on page 2 that C++ has no terminology to distinguish a copy made by a copy operation from one made by a move operation.
For example, a member function addName
might copy its parameter into a private container. For efficiency, such a function should copy lvalue arguments, but move rvalue arguments:
class Widget {
public:
void addName( const std::string&newName) // take lvalue;
{ names.push_back(newName); } // copy it
void addName( std::string&&newName) // take rvalue;
{ names.push_back(std::move(newName)); } // move it; see
… // Item 25 for use
// of std::move
private:
std::vector names;
};
This works, but it requires writing two functions that do essentially the same thing. That chafes a bit: two functions to declare, two functions to implement, two functions to document, two functions to maintain. Ugh.
Furthermore, there will be two functions in the object code — something you might care about if you're concerned about your program's footprint. In this case, both functions will probably be inlined, and that's likely to eliminate any bloat issues related to the existence of two functions, but if these functions aren't inlined everywhere, you really will get two functions in your object code.
Читать дальше