The C++ language many of us are using today goes back more than thirty years. You might be using some “newer” features, such as templates or the standard library, which have been standardized around 1998 – the previous millennium. Since 1998, C++ has seen two major international standards – C++ 11 and C++ 14, and work is in progress on another major revision to be published in 2017.
Over the last few years, C++ developers all over the world are transitioning to the new, modern C++. It’s not just a matter of language features or library APIs. It’s a matter of style and flavor, which makes C++ so successful across all major server, desktop, and mobile operating systems and processors. Conference talks use modern C++. Dozens of books are published every year using modern C++. Online forums like StackOverflow use modern C++ and code samples you see on your favorite software manuals use modern C++. You simply can’t be a C++ developer today without being able to write – or at least read and understand – the modern C++ syntax and style.
A year or so ago I came up with this Gist, illustrating a bunch of modern C++ 11/14 features in a single function, which might not even look like C++ to a 1990’s developer:
#include <iostream> #include <future> using namespace std; template <typename Fn, typename... Args> auto do_async_with_log(ostream& os, Fn&& fn, Args&&... args) -> future<decltype(fn(args...))> { os << "[TID=" << this_thread::get_id() << "] Starting to invoke function..." << endl; auto bound = bind(fn, forward<Args&&...>(args...)); return async([b=move(bound),&os]() mutable { auto result = b(); os << "[TID=" << this_thread::get_id() << "] ...invocation done, returning " << result << endl; return result; }); }
I’m not advocating that everyone learn template meta-programming and apply variadic templates at every corner. Some C++ language features have always been expert-only, or at least more suitable for library implementers than application developers. But here is a set of language and library features that will definitely make you more productive, and that you must learn to remain a relevant C++ developer in the 21st century (the links inline are to the isocpp.org C++ FAQ):
- Automatic type inference: auto local variables, auto function return types, decltype
- Range-based for
- initializer_list for STL collections
- Lambdas
- Class development goodies: delegating constructors, override and final specifiers, non-static data member initializers, defaulted and deleted functions
- Move semantics (at least on the basic level of understanding move constructors and assignment, and when to use std::move)
- noexcept
- Smart pointers (std::unique_ptr and std::shared_ptr)
- Concurrency library (std::thread, std::async, synchronization mechanisms, std::atomic)
Notice that a lot of features are missing from this list: variadic templates, type traits, unordered containers, universal references, and many others. I’m trying to be pragmatic, and I think the list above is really what every C++ developer has to know about modern C++. It’s also pretty much what I’m delivering at one-day modern C++ workshops.
To wrap it up — if you’re a C++ developer in 2015, and the C++ you’re using dates back to 1998, these are the things you need to learn, right now. You’ll thank me later.
In the lambda you capture the reference &os by reference. Is this guaranteed by the standard?
Should one not use an initialized capture such as &os = os?
Not sure what you mean. The ostream& has to be captured by reference because it is not copyable. Putting “&os” in the capture list tells the compiler that it should be captured by reference, no need for an initialized capture.
If the lambda is executed after do_async_with_log exits,
can you show that the standard guarantees that the captured reference os is not dangling?
No, that is definitely not guaranteed. However, it makes sense to expect that do_async_with_log would need to log into the stream provided by the caller. If that stream is destroyed before the async operation completes, it’s the caller’s fault. If necessary, you could work around this by requiring a shared_ptr.
I am not concerned that the stream no longer exist. Rather, does the lambda capture a reference to the stream, or “a reference to a reference” (a reference to &os) of the stream. In the later case, the program might lead to undefined behaviour.
No, capturing ‘os’ by reference will not produce a reference to a reference. There is no such thing.
My doubts stem from these passages in my copy of the standard:
“The reaching scope of a local lambda expression is the set of enclosing scopes up to and including the
innermost enclosing function and its parameters.”
” An entity that is designated by a simple-capture is said to be explicitly captured, and shall be this or a variable with automatic storage duration declared in the reaching scope of the local lambda expression.”
I’m not a language lawyer. But think about it reasonably: suppose you give my lambda a reference to an object backed by heap memory. If my lambda captures that object by reference, and you don’t deallocate said heap memory, the lambda can use that reference way past the original enclosing scope.
Somehow I cannot reply to your latest post.
What do you mean by capturing an object by reference?
The standard defines it as:
“An entity is captured by reference if it is implicitly or explicitly captured but not captured by copy.”
It gets better:
“It is unspecified whether additional unnamed non-static data members are declared in the closure type for entities captured by reference.”
It therefore conclude that the posted example might lead to undefined behavior and is non-portable. A simple fix would be to use the following capture: ([b=move(bound),&os = os].
Pingback: C++ Daily 2015-08-17 | blog.vladon.com
Hi
I’ve coded C++ for over 10 years and I couldn’t be happier to move to higher level languages like Scala and its lesser sister, Java & of course the excellent JVM runtime.
Even in 2015, the code above is full of boilerplate and low-level concepts leaking into the code, it’s frustrating.
You can do the above in maybe 2-3 lines of Scala, so why bother with all the typing? Performance is so close nowadays that it’s not worth optimizing so hard.
It’s SO much more of a pain to maintain huge codebases of C++ and have where some new guy in the company forget to free up some memory and core your app, than to sacrifice 10-20% of speed in some minor edge cases, so… why bother?
Thanks for trying anyway 🙂
Regards,
Nik
Nik, I appreciate your position, but I’m afraid you’re biased (like we all are). I don’t know the first thing about Scala, but I believe you it’s a nice language. I have a personal affection for Haskell, by the way. And still: C++ is still very much relevant and suitable for a bunch of scenarios today, cross-platform mobile code being among the more compelling reasons.
And just a minor thing about optimization… just a couple of days ago I was asked to optimize a vector dot product implementation written in C#. By simply switching to C++ (no fancy optimisations or code changes) the code ran 3x faster. An anecdote for sure, but a pretty convincing one.