Basics! Essentials of Modern C++ Style - Herb Sutter - CppCon
Basics! Essentials of Modern C++ Style - Herb Sutter - CppCon
Herb Sutter
Herb Sutter
CA
Complexity Anonymous
A 12-step program
for goodpeople attempting to
recover from complexity addiction
This talk focuses on defaults, basic styles and idioms in modern C++.
“Default” != “don’t think.”
“Default” == “don’t overthink.” Esp. don’t optimize prematurely.
why do this
for( auto i = begin(c); i != end(c); ++i ) { … use(*i); … }
for( e : c ) { … use(e); … }
wait, what?
C++98: C++14:
widget* factory(); unique_ptr<widget> factory();
void caller() { void caller() {
widget* w = factory(); auto w = factory();
gadget* g = new gadget(); auto g = make_unique<gadget>();
use( *w, *g ); use( *w, *g );
delete g; }
delete w;
}
red now “mostly wrong”
…
f( *upw );
auto spw = make_shared<widget>();
* and & FTW …
(More on parameter passing coming later…) g( spw.get() );
1. Never pass smart pointers (by value or by reference) unless you actually
want to manipulate the pointer store, change, or let go of a reference.
Prefer passing objects by * or & as usual – just like always.
Else if you do want to manipulate lifetime, great, do it as on previous slide.
2. Express ownership using unique_ptr wherever possible, including when
you don’t know whether the object will actually ever be shared.
It’s free = exactly the cost of a raw pointer, by design.
It’s safe = better than a raw pointer, including exception-safe.
It’s declarative = expresses intended uniqueness and source/sink semantics.
It removes many (often most) objects out of the ref counted population.
3. Else use make_shared up front wherever possible, if object will be shared.
// global (static or heap), or aliased local // global (static or heap), or aliased local
… shared_ptr<widget> g_p … … shared_ptr<widget> g_p …
// global (static or heap), or aliased local // global (static or heap), or aliased local
… shared_ptr<widget> g_p … … shared_ptr<widget> g_p …
1. Never pass smart pointers (by value or by reference) unless you actually
want to manipulate the pointer store, change, or let go of a reference.
Prefer passing objects by * or & as usual – just like always.
Remember: Take unaliased+local copy at the top of a call tree, don’t pass f(*g_p).
Else if you do want to manipulate lifetime, great, do it as on previous slide.
2. Express ownership using unique_ptr wherever possible, including when
you don’t know whether the object will actually ever be shared.
It’s free = exactly the cost of a raw pointer, by design.
It’s safe = better than a raw pointer, including exception-safe.
It’s declarative = expresses intended uniqueness and source/sink semantics.
It removes many (often most) objects out of the ref counted population.
3. Else use make_shared up front wherever possible, if object will be shared.
Example:
void f( const vector<int>& v ) {
vector<int>::iterator i = v.begin(); // ?
}
Options:
void f( const vector<int>& v ) {
vector<int>::iterator i = v.begin(); // error
vector<int>::const_iterator i = v.begin(); // ok + extra thinking
auto i = v.begin(); // ok, default
}
Using deduction makes your code more robust in the face of change.
Deduction tracks the correct type when an expression’s type changes.
Committing to explicit type = silent conversions, needless build breaks.
Examples:
int i = f(1,2,3) * 42; // before: ok enough
int i = f(1,2,3) * 42.0; // after: silent narrowing conversion
auto i = f(1,2,3) * 42.0; // after: still ok, tracks type
widget w = factory(); // before: ok enough, returns a widget
widget w = factory(); // after: silent conversion, returns a gadget
auto w = factory(); // after: still ok, tracks type
map<string,string>::iterator i = begin(dict); // before: ok enough
map<string,string>::iterator i = begin(dict); // after: error, unordered_map
auto i = begin(dict); // after: still ok, tracks type
Using deduction is your only good (usable and efficient) option for
hard-to-spell and unutterable types like:
lambdas,
binders,
detail:: helpers,
template helpers, such as expression templates (when they should stay
unevaluated for performance), and
template parameter types, which are anonymized anyway,
… short of resorting to:
repetitive decltype expressions, and
more-expensive indirections like std::function.
Consider:
auto x = value;
Consider:
auto x = type{value};
Observation
Just as exception safety isn’t all about writing try and catch,
using move semantics isn’t all about writing move and &&
Expensive to copy
Cheap to copy Moderate cost to copy (e.g., string, BigPOD)
(e.g., vector,
(e.g., int) or Don’t know (e.g., unfamiliar type, template)
BigPOD[])
In/Out f(X&)
In
f(X) f(const X&)
In & retain copy
Cheap or
Cheap to move (e.g., vector<T>, string) Expensive to move
impossible to
or Moderate cost to move (e.g., array<vector>, BigPOD) (e.g., BigPOD[],
copy (e.g., int,
or Don’t know (e.g., unfamiliar type, template) array<BigPOD>)
unique_ptr)
Out X f() f(X&) *
In/Out f(X&)
In
f(X) f(const X&)
In & retain “copy”
Cheap or
Cheap to move (e.g., vector<T>, string) Expensive to move
impossible to
or Moderate cost to move (e.g., array<vector>, BigPOD) (e.g., BigPOD[],
copy (e.g., int,
or Don’t know (e.g., unfamiliar type, template) array<BigPOD>)
unique_ptr)
Out X f() f(X&) *
+1 consistency:
In/Out f(X&) same optimization
In f(const X&) guidance as overloaded
f(X) copy+move construction
In & retain copy f(const X&) + f(X&&) & move **
and assignment
In & move from f(X&&) **
Cheap or
Cheap to move (e.g., vector<T>, string) Expensive to move
impossible to
or Moderate cost to move (e.g., array<vector>, BigPOD) (e.g., BigPOD[],
copy (e.g., int,
or Don’t know (e.g., unfamiliar type, template) array<BigPOD>)
unique_ptr)
Out X f() f(X&) *
In/Out f(X&)
In f(const X&)
f(X)
In & retain copy f(const X&) + f(X&&) & move **
Cheap or
Cheap to move (e.g., vector<T>, string) Expensive to move
impossible to
or Moderate cost to move (e.g., array<vector>, BigPOD) (e.g., BigPOD[],
copy (e.g., int,
or Don’t know (e.g., unfamiliar type, template) array<BigPOD>)
unique_ptr)
Out X f() f(X&)
In/Out f(X&)
In f(const X&)
?
In & retain copy f(X) *
f(X) & move
In & move from
* GOOD: this can be faster than C++98 – can move from rvalues;
BUT: also can be much slower than C++98 – always incurs a full copy, prevents reusing
buffers/state (e.g., for vectors & long strings, incurs memory allocation 100% of the time)
BUT: also problematic for noexcept
Consider:
class employee {
std::string name_;
public:
void set_name( /*… ?? …*/ ); // change name_ to new value
};
Q: What should we tell people to write here?
Hint: There has been a lot of overthinking going on about this.
(I include myself.)
5000
4000
3000
2000
1000
0
lvalue (1-10) lvalue (1-50) xvalue (1-10) xvalue (1-50) char* (1-10) char* (1-50)
Option 1: const string& Option 2: const string& + string&& Option 3: string Option 4: String&& perfect fwding
Clang/libc++ Release
1200
1000
800
600
400
200
0
lvalue (1-10) lvalue (1-50) xvalue (1-10) xvalue (1-50) char* (1-10) char* (1-50)
Option 1: const string& Option 2: const string& + string&& Option 3: string Option 4: String&& perfect fwding
1200
1000
800
600
400
200
0
lvalue (1-10) lvalue (1-50) xvalue (1-10) xvalue (1-50) char* (1-10) char* (1-50)
Option 1: const string& Option 2: const string& + string&& Option 3: string Option 4: String&& perfect fwding
1000
800
600
400
200
0
lvalue (1-10) lvalue (1-50) xvalue (1-10) xvalue (1-50) char* (1-10) char* (1-50)
Option 1: const string& Option 2: const string& + string&& Option 3: string Option 4: String&& perfect fwding
Constructor operator=
Default
$$
Move $
This talk focuses on defaults, basic styles and idioms in modern C++.
“Default” != “don’t think.”
“Default” == “don’t overthink.” Esp. don’t optimize prematurely.
Questions?