610 likes | 623 Views
How To Train Your Modern C++. (A Guided Tour In The Zoo Of Wild Language Constructs). Prologue. (What did I get myself into?). Name: Thierry Lavoie Current occupation: manager, static analysis technologies at Synopsys Credentials: Principal voter for Synopsys on WG21, PhD in static analysis.
E N D
How To Train Your Modern C++ (A Guided Tour In The Zoo Of Wild Language Constructs)
Prologue (What did I get myself into?)
Name: Thierry Lavoie • Current occupation: manager, static analysis technologies at Synopsys • Credentials: Principal voter for Synopsys on WG21, PhD in static analysis
First started writing C++ in 2000 • Standard C++ was new and shiny! • It was the emerging dominant language in the industry
Why do I care about C++? • I come from the video game industry, then the world of data structures • Performance matters!
Why this talk? • Because I deeply care about C++ • Because C++ is in a deep need of care!
C++ was born in 1985 (me too!) • First standardized as ISO/IEC 14882:1998, amended in 2003 as ISO/IEC 14882:2003 • References in this talk are from the latest working draft: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4700.pdf
Classes • Inheritance • Polymorphism • Templates • Memory model and RAII • Destructors • Exceptions • Overloading • … That’s how it all started!
R-value reference • Non-static data member initializers • Variadic templates • Initializer lists • Static assertions • Auto-typed variables • Lambda expressions • Declared type of an expression • Right angle brackets • Default template arguments for function templates • Solving the SFINAE problem for expressions • Alias templates • Extern templates • Null pointer constant • Extended friend declarations • Extending sizeof • Inline namespaces • Unrestricted unions • Range-based for • Strongly-typed enums • Forward declaration of enums • Standardized attribute syntax • Generalized constant expressions • Alignment support • Conditionally-support behavior • Delegating constructors • Inheriting constructors • Explicit conversion operators • New character types • Unicode character names in literals • Raw string literals • Standard layout types • Defaulted functions • Deleted functions • Explicit virtual overrides • Defining move special member functions • …
Static assertions Runtime assertions (from C): assert(integral_expression); C++11 has compile time assertions: static_assert(bool_constexpr, “compile time error message!”);
Static assertions #include <type_traits> constinti = 10; void f() { static_assert(i == 11, "global i is not equal to 10"); } struct A { virtual ~A(); }; struct B : public A { static_assert(!std::is_polymorphic<A>::value, "A shouldn't be polymorphic"); }; $ g++ -std=gnu++11 assertions.cpp assertions.cpp: In function ‘void f()’: assertions.cpp:6:3: error: static assertion failed: global i is not equal to 10 static_assert(i == 11, "global i is not equal to 10"); assertions.cpp: At global scope: assertions.cpp:14:3: error: static assertion failed: A shouldn't be polymorphic static_assert(!std::is_polymorphic<A>::value, "A shouldn't be polymorphic");
Auto-typed variables Variable declarations are allowed to use auto as the type of a variable. For example: auto pi = 3.1416; The type of the declared variable is deduced from the initializer expression according to the template argument deduction rules. In the previous example, the deduced type of the variable pi is double. Note that changing the literal in the initializer to the following: auto pi = 3.1416f; changes the type of pi to float.
Auto-typed variables The previous remark highlights a fundamental difference between explicit typing and auto-typing: with auto-typing, one must be careful about the exact type of the initializer expression. For example: float pi = 3.1416; declares a variable pi of type float, whereas auto pi = 3.1416; declares a variable pi of type double.
Auto-typed variables Let’s consider the following: A buzz(); auto var = buzz(); A* foo(); auto ptr = foo(); const A& bar(); auto ref = bar(); What’s the type of each variable in the previous example? Are every examples valid?
Auto-typed variables One of the type should be somewhat surprising… A buzz(); auto var = buzz(); // var is of type A A* foo(); auto ptr = foo(); // ptr is of type A* const A& bar(); auto ref = bar(); // ref is of type A (!?) Remember what we said about the type deduction rules?
Auto-typed variables The idea behind type deduction is to consider the following invented function: template <class U> void f(U u); // Adapted from clause 10.7.4.1, par.5 and to deduce the type U as if the following was called: auto var = <expression>; f(<expression>);
Auto-typed variables struct A {}; // Bonus question: what is the size of A? template<class U> void f(U u) { static_assert(std::is_same<U,A>::value, "U is not A"); // uses #include <type_traits> static_assert(std::is_same<U,const A&>::value, "U is not const A&"); } const A& bar() { const static A a; return a; } int main() { auto ref = bar(); f(ref); return 0; } $ g++ -std=gnu++11 t.cpp t.cpp: In instantiation of ‘void f(U) [with U = A]’: t.cpp:15:8: required from here t.cpp:8:3: error: static assertion failed: U is not const A& static_assert(std::is_same<U,const A&>::value, "U is not const A&");
Auto-typed variables Going back to our example: const A& bar(); auto ref = bar(); // ref is of type A Using our synthetic example, we have shown that A is the expected type if we follow the rules (precisely 17.9.2.1, par. 2.3). How to use auto and still get a variable of type const A&? By qualifying auto! const A& bar(); const auto& ref = bar(); // ref is of type const A& Remember this: it will be important in a moment!
R-value reference Before looking at this feature, let’s make a bold claim: this is the most important feature introduced in C++ since 1998! To understand the claim, consider the following: template<class T> void swap(T& a, T& b) { T tmp(a); a = b; b = tmp; } What is the main problem with this code? (Hint: it could do much more than what it claims to be doing)
R-value reference What we really want is the following: template<class T> void swap(T& a, T& b) { T tmp(std::move(a)); a = std::move(b); b = std::move(tmp); } But std::move doesn’t exist in C++03 because we can only express value-semantics! (To see why, think for a second about destructing objects)
R-value reference Let’s consider a more complex problem: vector<unsigned int> get_long_vector(unsigned int n) { vector<unsigned int> quad; quad.reserve(n); // Always reserve your vectors (when you can)! (It runs in 75s without reserve) quad.push_back(0); for(unsigned inti = 0; i < n; ++i) { quad.push_back(i + quad.back()); } return quad; } int main(intargc, char** argv) { const unsigned int SIZE = 100000000; vector<vector<unsigned int> > matrix; matrix.reserve(SIZE); for(unsigned inti = 0; i < SIZE; ++i) { matrix.push_back(get_long_vector(10)); } cout << matrix.back().back() << endl; return 0; } Compiled with C++03, this code runs in 43s. Compiled with C++11, this code runs in 31s; this is 39% faster! Why? Because C++03 has to copy the return value of get_long_vector!
R-value reference C++11 introduced the notion of r-value reference. An r-value reference is declared with &&: void handle_move(A&& a); Binding a temporary value to an r-value reference will extend its lifetime. This was possible in C++03 by binding a value to a const l-value reference (const A&), but the value was not modifiable. When used, an r-value reference behaves like a regular reference. Note, moving a primitive type is the same as copying it. The crux is in function overloading; consider the following from std::vector : void push_back (constvalue_type& val); void push_back (value_type&& val); // New in C++11 vec.push_back(getA());
R-value reference What if we have a local reference that we want to move? Use std::move: A a; vec.push_back(std::move(a)); std::move is simply a cast from an l-value reference to an r-value reference: template <class T> typenameremove_reference<T>::type&& move(T&& a) { return a; }
R-value reference New standard class functions: move constructor and move assignment operator: class A { A(A&& a); // move constructor A& operator=(A&& a); // move assignment operator }; There exist defaulted versions of these functions. It works in many cases, but you notably shouldn’t use them if your class owns raw pointers (like in basic data structures).
R-value reference This is just the surface of r-value references! I won’t have the time to cover forwarding references, how to write methods with this passed as an r-value reference, how to write efficient matrix multiplication by re-using the temporaries…I could do a 3 hour presentation on r-value references alone, and this is not the place to do it. What you should remember is that the STL in C++11 and beyond is already using the r-value reference feature and the move semantics. Just upgrading to C++11 should give you a boost in performance. And I hoped I convinced you to read more on this topic!
Lambda expressions Consider the following function from the STL: template< class ForwardIt, class UnaryPredicate >ForwardItremove_if( ForwardIt first, ForwardIt last, UnaryPredicate p ); And the following usage: vector<uint64_t> vec; struct Predicate { bool operator()(const uint64_t& x) const { return 3*x < x*x; } } p; std::remove_if(vec.begin(), vec.end(), p);
Lambda expressions C++11 has lambda expressions to create a function object in an expression. The syntax is: [<capture-list>] (<parameter-list>) -> <trailing-return-type> { <function-body> } The previous example becomes: vector<uint64_t> vec; std::remove_if(vec.begin(), vec.end(), [](const uint64_t& x) -> bool { return 3*x < x*x; }); A lambda-expression can also be used to initialize a variable: auto lambda = [](const uint64_t& x) -> bool { return 3*x < x*x; }; Note that lambda is not a free function; it is an auto-type object with an overloaded operator()
Lambda expressions Let’s take a look at the capture-list part of the expression: [<capture-list>] (<parameter-list>) -> <trailing-return-type> { <function-body> } The capture-list allows to capture variable accessible in the scope where the lambda expressions is defined: vector<uint64_t> vec; uint64_t a = 19; uint64_t b = 21; std::remove_if(vec.begin(), vec.end(), [a, &b](const uint64_t& x) -> bool { return a*b*x < x*x; }); Note that variables are captured by value by default; a variable must be preceded by & to be captured by reference.
Lambda expressions Always remember that lambda-expressions create objects in C++, not a function. In this initialization: auto lambda = [](const uint64_t& x) -> bool { return 3*x < x*x; }; In this example, lambda is the name of a variable, not the name of a function. A counter-intuitive consequence of this is the interdiction of recursive-lambdas (i.e., what looks like recursive function calls, but is not). See this paper to understand why: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0839r0.html Maybe in C++20…
Generalized constant expressions Constant doesn’t always mean the compiler will evaluate it at compile time. Consider this: struct S { static constint size; }; constint limit = 2 * S::size; constint S::size = 256; When is limit initialized? Why?
Generalized constant expressions The problem is the absence of reliable way to assert that an expression has a constant value. That’s why a new keyword was introduced : constexpr In C++11, a constexpr function needs to satisfy these constraints (from the original proposal): A function is a constant-expression function if • it returns a value (i.e., has non-voidreturn type); • its body consists of a single statement of the form return expr; • where after substitution of constant expression for the function parameters in expr, the resulting expression is a constant expression (possibly involving calls of previously defined constant expression functions); and • it is declared with the keyword constexpr
Generalized constant expressions struct S { constexprint twice(); private: static constexprintval; }; constexprint S::val = 7; constexprint S::twice() { return val + val; } Requirements for constexprare strict in C++11. Maybe the future will give us more capabilities.
Range-based for A classic iteration loop: vector<int> vec = get_big_vector(); for( vector<int>::iterator itr = vec.begin(); itr != vec.end(); ++itr) { // Where’s the inefficiency? do_something(*itr); } With C++11 improvements: vector<int> vec = get_big_vector(); for( auto itr = vec.begin(); itr != vec.end(); ++itr) { do_something(*itr); }
Range-based for Using only a range-based for: vector<int> vec = get_big_vector(); for( inti : vec) { do_something(i); } With some C++11 improvements: vector<int> vec = get_big_vector(); for( auto i : vec) { do_something(i); }
Range-based for The preferred way to do it with C++11: vector<int> vec = get_big_vector(); for( const auto& i : vec) { do_something(i); } You remembered what I said about auto right?
Null pointer constant A small change in how to assign and compare null pointers: A* ptr = nullptr; if(ptr == nullptr) {} This looks like a cosmetic change, but nullptr is not an integral type: it’s a pointer type. It’s preferred to use nullptr over NULL and 0.
Raw-literal strings const char* str = R"str(Is there any better way to explain what a raw-literal string is than to draw a slide with a raw-literal string? If you are an ounce like me , you have been dreaming of this day for a decade. It’s definitely not that I have a deep affection for slides in general, but I do love to have self-contained unit tests with complex input. But that's a story for another time...)str";
C++11 bis (Let’s not forget about the library)
Binary literals • decltype(auto) • Return type deduction for normal functions • Initialized lambda captures • Generic lambdas • Variable templates • Relaxing requirements on constexpr functions • Member initializers and aggregates • [[deprecated]] attribute • Single quotation mark as digit separator • C++ sized deallocation That’s all folks!
Single quotation mark as digit separator Convenient for big numbers: uint64_t i = 1’000’000’000’000;
Binary literals For all the bitmasks you want: uint64_t i = 0b01010101; uint64_t j = 0B01010101;
Relaxing requirements on constexpr functions constexpr functions were relaxed to (from N3652): • Allow declarations within constexpr functions, other than: • static or thread_local variables • uninitialized variables • Allow if and switch statements (but not goto) • Allow all looping statements: for (including range-based for), while, and do-while • Allow mutation of objects whose lifetime began within the constant expression evaluation.
Relaxing requirements on constexpr functions This is valid C++ 14! constexpr uint64_t slow_add(uint64_t n) { uint64_t sum = 0; for(uint64_t i = 0; i < n; ++i) { sum += i; } return sum; }
static_assert with no message • Disabling trigraph extension by default • typename in a template template parameter • New auto rules for direct-list-initialization • Fold expressions • U8 character literals • Nested namespace definition • Attributes for namespaces and enumerators • Allow constant evaluation for all non-type template arguments • Remove deprecated register storage class • Remove deprecated bool increment • Make exception specifications part of the type system • _has_include in preprocessor conditionals • Changes in inheriting constructors • [[fallthrough]], [[nodiscard]], [[maybe_unused]] attributes • Aggregate initialization of classes with base classes • constexpr lambda expressions • Differing begin and end types in range-based for • Lambda capture of *this • Direct-list initialization of enums • Hexadecimal floating-point literals • Using attributes namespaces without repetition • Dynamic memory allocation for over-aligned data • Template argument deduction for class templates • Non-type template parameters with auto type • Guaranteed copy elision • Stricter expression evaluation order • Requirement to ignore unknown attributes • constexpr if-statement • Inline variables • Structured bindings • Separate variable and conditions for if and switch • Matching template template parameters to compatible arguments • Removing deprecated dynamic exception specifications • Pack expansions in using-declarations
What was exciting in C++ in each revision? • Was C++11 enough? Should you upgrade to C++14 or C++17? • What needs to be next?