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 xis std::atomic:
auto y = x; // error!
y = x; // error!
That's because the copy operations for std::atomicare deleted (see Item 11). And with good reason. Consider what would happen if the initialization of ywith xcompiled. Because xis 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::atomics is that all their operations are atomic, but in order for the copy construction of yfrom xto be atomic, compilers would have to generate code to read xand write yin a single atomic operation. Hardware generally can't do that, so copy construction isn't supported for std::atomictypes. Copy assignment is deleted for the same reason, which is why the assignment from xto ywon'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:: atomicoffers neither move construction nor move assignment.)
It's possible to get the value of xinto y, but it requires use of std::atomic's member functions loadand store. The loadmember function reads a std::atomic's value atomically, while the storemember function writes it atomically. To initialize ywith 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 ymakes 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 xonly once, and that's the kind of optimization that must be avoided when dealing with special memory. (The optimization isn't permitted for volatilevariables.)
The situation should thus be clear:
• std::atomicis useful for concurrent programming, but not for accessing special memory.
• volatileis useful for accessing special memory, but not for concurrent programming.
Because std::atomicand volatileserve 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 vaicorresponded 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 loadand storemember 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::atomicis typically much slower than accessing a non- std::atomic, and we've already seen that the use of std::atomics prevents compilers from performing certain kinds of code reorderings that would otherwise be permitted. Calling out loads and stores of std::atomics can therefore help identify potential scalability chokepoints. From a correctness perspective, not seeing a call to storeon 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::atomicwhen it should have been.
This is largely a style issue, however, and as such is quite different from the choice between std::atomicand volatile.
Things to Remember
• std::atomicis for data accessed from multiple threads without using mutexes. It's a tool for writing concurrent software.
• volatileis 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 addNamemight 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.
Читать дальше