
Smart pointers and move semantics in modern C++ - severine
https://www.oreilly.com/ideas/2-major-reasons-why-modern-c-is-a-performance-beast?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+oreilly%2Fradar%2Fatom+%28O%27Reilly+Radar%29
======
flohofwoe
I'm over-generalising, but: if you want performance, don't create a huge soup
of tiny objects on the heap pointing to each other as can often be seen in
'smart-pointer-heavy' C++ projects. Creating and destroying objects on the
heap is expensive, and you'll end up pointer-chasing with cache-misses all
over place.

Unfortunately smart-pointers are only useful with this 'every object lives
isolated on the heap' approach, since alloc-init and cleanup-free are merged
into the same operation. Of course there's ways around that by writing custom
allocators, but this just adds more complexity when often granular objects
with their own lifetime and ownership aren't needed in the first place.

Smart-pointers are a convenience feature to not having to think about object-
lifetime, and in the case of shared-pointers, also to not think about
ownership, they're not about performance. If you want performance, don't
allocate single objects on the heap, and spend some time thinking about data
layout in memory.

------
jnordwick
Part of the problem with the new move semantics for me, and C++ in general, is
figuring out what the compiler is going to do and their interaction with the
return value optimization. Sometimes I can't figure out if the RVO or the move
constructor is going to be used.

C++ has definitely blown it's complexity budget and the problem is getting
worse. I don't really see a way out of it either. I sure hope Rust starts to
gel or a better C++ comes along, because I don't think I can keep pace with
C++ too much longer.

~~~
hendzen
If you're using clang you can just turn on -Wpessimizing-move and the compiler
will tell you when you have a std::move call that is inhibiting RVO.

The rule of thumb is don't use std::move on a return value unless you are
returning a move-only type (which forces you to do so).

~~~
frankzinger
> The rule of thumb is don't use std::move on a return value unless you are
> returning a move-only type (which forces you to do so).

RVO will be automatically applied if the returned variable is local to a
function and of the same type as the function's return type. That's all that
the standard allows.

RVO is not applied to function arguments, regardless of whether or not they're
rvalue references.

So your rule of thumb breaks for cases such as these (X is a movable and
copyable type):

    
    
        X f(X&& x) {
          return x;  // x is copied, not moved
        }
    

In order to get x moved into the return location, you would need to do move it
explicitly.

Source: Effective Modern C++ by Meyers.

------
hellofunk
There are other C++11/14 features that improve performance even further that
are not mentioned here. For example, automatic type deduction often finds a
more correct type for a variable or iterator than you'd guess on your own,
since your guess might require an implicit conversion which might be costly in
some cases. Brace initialization can also avoid costly implicit conversions.
And using lambdas rather than named functions often leads to automatic
inlining, an additional performance benefit.

C++ is a changing language and is doing a good job keeping up the with times,
adding high-level features while increasing performance at the same time.

------
KKKKkkkk1
The calculus here is simple. You have two options:

1) Spend 90% of the time writing performance-neutral code in a modern high-
productivity language and 10% on performance-critical code in a modern high-
performance language.

2) Write everything in a "performance beast" of a language, spending 90% of
your time fighting said language and 10% delivering useful functionality.

Bottom line, if you choose option 2, you spend 10x time delivering the same
functionality and performance and you force yourself to hire the kind of
expensive developers who are willing to waste their time mastering modern C++.

~~~
hellofunk
You forgot option 3), which I use myself: Write in a high-performance language
that also has high-level features, and write 90% of your code without concern
for the language's cool optimization features.

That's what I do and it's great. I don't use move semantics or think about
them, I avoid heavy templates, and I focus a lot of my work on the functional
tools in C++ now and the more elegant ways of doing stuff at a high level. For
when I need those extra optimizations, no need to change languages, I'm
already there.

~~~
ced
Haven't used C++ in a decade - how is the modern functional programming
working out without a GC? Isn't object ownership tricky to untangle?

I read your option 3 and nodded - I'm using Julia for much the same reasons. I
didn't expect C++ to be the answer, but it's cool that it can work out.

~~~
cjhanks
If you are using Julia, then I will presume you are primarily working in some
form of numerical computing. It is a very wide field, so it is difficult to
say if object ownership will be tricky or not.

If you are working with (b|m)illions of particles or entities which are
capable of changing ownership, you will have a hard time in any language.
Whether that's the C++ ownership being unclear or the garbage collector
defragmenting your memory (just because you don't see the issue doesn't means
it's gone in GC languages).

That said, typically I find most algorithms have a computable upper-bound
memory constraint and so you just pre-allocate a slab and then objects
reference it. You will usually get better cache coherency and less wasted
operations/sec. You're left with only a couple pointers you need to delete
when you're done with the structure.

As for `functional programming`, it depends on what you're looking for. C++
has two _killer features_ for this, in my opinion; expression templates can
seriously improve performance of functional composition (see Eigen3), and
compile-time elision of type-dependent code branches (like a static if).

Since I have learned to use `unique_ptr`, `shared_ptr`, `weak_ptr`,
(`deferred_ptr`), and to prefer idiomatic accessors like `.at`, `.find`,
`.insert` I cannot recall the last time Valgrind was angry at me. I think I
end up writing `new` and `delete` maybe once a month(?).

If I was designing widgets or applications with hard to define user
interaction, I imagine I would have more complaints to share.

~~~
int_19h
One nifty trick to avoid writing new or delete even when you need manual heap
allocation is to always use std::make_unique (or std::make_shared, as needed)
instead. That way you're forced to spell out any transfer of ownership as it
is happening, and so someone's always be sure to delete the object. You also
dodge the bullet with indeterminate ordering of new's and constructors, that
might result in memory leaks if one of the constructors throws.

An important extension to this trick is that unique_ptr and shared_ptr are
specialized for arrays. So e.g. to allocate a dynamic array of size n, you can
do:

    
    
        auto a = std::make_unique<char[]>(100);
    

or

    
    
        auto a = std::make_unique<int[3]>(1, 2, 3);
    

and then use it mostly like an array - a[i] is overloaded and work as
expected, for example.

The reason why you'd want to do this rather than using std::vector is because
you don't get the overhead of overallocation (since vectors distinct capacity
and size) and default-initialization of the elements.

With these, it's quite possible to write >100 kLoC of C++ without a single new
or delete in sight.

~~~
shermanyo
Great tip, thanks for the clear example :)

------
PhantomGremlin
I hope everyone read down to the end to see this factoid about Leor Zolman,
the author: _In 1979, Leor wrote the BD Software C Compiler ( "BDS C"), the
first 8080/Z80 native-code C compiler both developed on and targeted for
personal computers._

Leor's C compiler didn't support floating point, but it compiled programs like
a bat out of hell on a 4 MHz 8-bit Z80 microprocessor with 64 KB of memory.

Yes, it was C, running well, on a 4 MHz micro with 65,536 bytes of memory.
Truly remarkable for its time.

[https://en.wikipedia.org/wiki/BDS_C](https://en.wikipedia.org/wiki/BDS_C)

------
filereaper
Does anyone have a good recommendation for a cannonical reference on Modern
C++14?

I have experience with C++ but its the old kind with raw pointers.

My usual starting point is Herb Sutter's books, with Bjarne's bible handy when
I need it. I'm not sure where to start with modern C++.

~~~
anuragbiyani
"The C++ Programming Language" has been updated in 2013, and includes C++11
changes:
[http://www.stroustrup.com/4th.html](http://www.stroustrup.com/4th.html)

"A Tour of C++" (also by Bjarne) is a quick overview of the language
(including C++11 changes):
[http://www.stroustrup.com/Tour.html](http://www.stroustrup.com/Tour.html)

Another very useful resources is the FAQs from "Standard C++ Foundation":
[https://isocpp.org/faq](https://isocpp.org/faq)

------
Animats
Move semantics are a good idea, but C++ doesn't have the compiler machinery to
catch when you move something away and then try to reference it. Move
semantics really need a borrow checker, like Rust. In C++, you have to be
very, very careful not to do things like accidentally move something passed to
you as a non-const ref.

~~~
int_19h
In C++, you can't "accidentally" move something that's a non-const ref, since
a move from one would require explicit use of std::move.

The only things which can be moved _accidentally_ are rvalues and rvalue
references. But for rvalues, that doesn't hurt anything; and for rvalue
references, the only reason why you'd use one is if you intended to play
tricks with move semantics, which implies that you know what you're doing.

Basically if you write C++ as if move semantics didn't exist (i.e. never use
rvalue references or std::move), you won't shoot yourself in the foot by those
means. Of course, this also means that compiler assumptions are fairly
pessimistic, so many places that could be a move aren't, because the move has
to be explicit.

------
snissn
Wow the author is giving a two day course soon in C++ for up to 40 people that
is costing 2500 per person. Is that a big industry? Do many people on HN run
programming workshops? I would be interested in chatting if you do. Email on
my profile

~~~
hellofunk
C++ training is a big industry, yes. Not sure about other languages.

------
oldsklgdfth
The vector is initialized with 500000 widget objects. The size and capacity of
the vector are equal. The following while loop is never executed.

>> while (vw.size() < vw.capacity()) >> vw.push_back(Widget());

In the situation that the vector is not initialized with elements and
populated in a loop the time for a single push_back is equal. I think this is
because there is no need for a resize.

>> vector<Widget> vw; >> for(int i = 0; i < 500000; i++) { >>
vw.push_back(Widget()); >> }

Does this illustrate how fast a vector of objects can be copied/resized using
move semantics?

------
alexnewman
As a rust programmer. I've been very happy to see c++ talking about the things
that are core to how we get stuff done in rust. As a c++ programmer, do you
ever wish that auto wasn't const by default? Couldn't you have auto (let) and
volatile (mut) :) .

I'm pretty sure this wouldn't be too hard to implement.

------
nurettin
I think most of these educated comments about memory safety wouldn't be here
without the hindsight provided by Rust. So thanks, Rust!

------
armitron
If only it was a reliability/robustness/simplicity beast instead. Instead,
it's evolved to be this atrocity of unspeakable proportions that continues to
wreak havoc (security bugs, technical debt, complexity) pretty much everywhere
it's used. When the best C++ developers on the planet can not do better than
that, you know we have a problem.

It remains to be seen if the alternatives that are now slowly emerging will
replace it. My personal estimation based on all the expert C++ programmers I
know is that this will not happen through people abandoning C++ since it sits
so high on the abstract ladder of complexity that for one to even begin to
question its foundations (and all the time/years/decades one has sank into
becoming an expert C++ programmer) will be a Tower-of-Babel moment. On top of
that you have the (quite illusionary) tremendous feeling of empowerment one
gets when using something that required such an investment to master.

JWZ once wrote (on an unrelated topic):

“No matter how hard you try, you can’t make a racehorse out of a pig. You can,
however, make a faster pig.”

This seems fitting to the way C++ is evolving.

~~~
user5994461
Should quote this one as well:

    
    
       (3)  With sufficient thrust, pigs fly just fine. However, this is
            not necessarily a good idea. It is hard to be sure where they
            are going to land, and it could be dangerous sitting under them
            as they fly overhead.
    

[https://tools.ietf.org/html/rfc1925](https://tools.ietf.org/html/rfc1925)

