- It's not dumbing the language down. It's about only do things otherwise when you have a good reason to do so. Don't optimize prematurely & don't overthink.
- Optimal by default v.s. good(but not optimal) performance by default
- Clear code is more important than optimal code. You don't always need to open the hood.
- If you need to traverse every element in the collection, prefer range-for with
auto &
instead of iterators for (e: c)
are coming!
- Unique pointers are fairly cheap. Don't use owning *,
new
ordelete
. Usemake_unique
by default andmake_shared
if necessary, and you don't need todelete
anymore.
-
Non-owning references/pointers(lifetime controlled by callers) are still great!
void f(widget &w) { use(w); } void g(widget *w) { if (w) use(*w); }
In modern C++, you pass them through
*smart_pointer
orsmart_pointer.get()
, but there's nothing wrong with the old pattern! -
If you are not transferring ownership, just use the good old references/pointers for parameters, because the callee shouldn't care about if that thing is shared or unique anyway.
-
Anti-pattern: when you don't use the smart pointer properly:
void f(refcnt_ptr<T> &w) { use(*w); // why on earth do you pass that reference when it will be counted? } void f(refcnt_ptr<T> w) { use(*w); // pass-by-value?! // You'll be incrementing the reference with every call, doesn't it hurt? }
-
Anti-pattern: copy reference counted pointers in loops
refcnt_ptr<T> w = ...; for (...) { auto w2 = w; // WTF?! use(w2); }
-
FB RocksDB improve 4x by changing pass-by-value
shared_ptr
to raw pointers/references
- Reference counted smart pointers are about managing owned object's lifetime. Only copy/assign them when you need to manipulate the ownership!
- People say smart pointers/vectors hurt performance because they are bitten by code that pass-by-value or copy/assign in loops with them, when they don't even intend to manipulate their ownerships.
- It's OK to return
unique_ptr
in factories(remember to usemake_unique
for optimization) - It't OK to pass
unique_ptr
by value when you are about to consumes them. - If you will or might change where a
unique_ptr
points to, it's OK to pass it by reference. - It's usually not right to pass
const unique_ptr<widget> &
. You should just pass aconst widget &
- It't OK to eturn a
shared_ptr
in factories(remember to usemake_shared
for optimization) when you know it will be shared. - If you want to share a
shared_ptr
(add one more reference count) to a function, you can pass it by value. - If you will or might change where a
unique_ptr
points to, it's OK to pass it by reference. - If you might share a
shared_ptr
, it's OK to passconst shared_ptr<widget> &
Don't dereference a non-local, possibly aliased reference-counted pointer, because it could be released elsewhere.
shared_ptr<widget> g_p; // global or aliased local
void f(widget &w) {
g();
use(w);
}
void g() {
g_p = ... // reseat it
}
void my_code() {
f(*g_p); // WARNING! g() might reseat `g_p`, releasing it, and then f() will dereference it with `use(w)`
// or
g_p->foo();
}
How to do it properly? Pin it with an unaliased local copy so you can be sure it won't be released when dereferencing it. When you need to dereference a global smart pointer, copy it locally first.
void my_code() {
auto pin = g_p; // +1 ref count!
f(*pin); // now pin is local
// or
pin->foo();
}
- Never pass smart pointers(by value/reference) unless you want to manipulate its ownership
- Use
unique_ptr
for ownership whenever possible, even when you don't know if it will be shared
- free, safe, declarative, no ref count headaches
- Else use
make_shared
up front when you know it will be shared. Don't dereference globalshared_ptr
, make a local copy before you do that.
- A good IDE(even emacs) can help you see the actual type when you are coding, so don't be afraid of losing track of the types.
auto
makes you focus on interfaces, not implementations- Templates/temporaries already ignore actual types
- With deduction, you always get the exact right type without accidentally changing modifiers/constness or getting implicit conversions
- Less typing LOL
- No accidental narrowing on literals(e.g.
42.0f
,42ul
) - The signatures will be on the right. Most of the times
auto
force you to put any possible conversions explicitly on the right hand side. If you put it on the left hand side(usually it's what you do when you don't useauto
), people will keep NOT seeing it. =
doesn't always mean copy/assignment in C++. That's a quirk in the standard -- it could mean initialization.- If you do
auto x = something
, something will be moved instead of copied. But if it's not movable, there could be an error(and remember, containers are expensive to move).
So, no implicit conversions/temporaries, no narrowing, no uninitialized variables!
- Cheap costs(e.g. a few
int
s) to copy: return-by-value and pass-by-value - Moderate costs(e.g. ~1KB contiguous)/unknown costs to copy: rely on RVO, pass by const references
- Expensive to copy(e.g. vectors,
BigPOD[]
): use references all the way through
- For cheap costs/impossible to copy(e.g.
unique_ptr
, return-by-value and pass-by-value(will trigger move semantics if necessary) - For moderate costs/cheap to move/unknown costs(e.g.
vector
,array<vector>
,BigPOD
), rely on RVO, pass by const references - Expensive to move(e.g.
BigPOD[]
,array<BigPOD>
), use references all the way through
So in more cases you just need RVO and const references.
- If you want to optimize for rvalues, add an overload for
&&
- For constructors, pass-by-value and then move might still be a good option, but usually just stick to
const &
T &&
is forwarding reference(because it doesn't care about T isconst
or whatnot)
C++ 98: pair
s
pair<set<string>::iterator, bool> result = func();
if (result.second) use(result.first);
C++ 11 with backward compatibility
auto result = func();
if (result.second) use(result.first);
C++ 11 new syntax
// declare iterator and success earlier...don't forget to initialize them!
tie(iterator, success) = func();
if (success) use(iterator)