The second part of my series about C++17 details. Today I’d like to focus on features that clarify some tricky parts of the language. For example copy elision and expression evaluation order.

Intro

You all know this… C++ is a very complex language, and some (or most? :)) parts are quite confusing. One of the reasons for the lack of clarity might be a free choice for the implementation/compiler - for example to allow for more aggressive optimizations or be backward (or C) compatible. Sometimes, it’s simply a lack of time/effort/cooperation. C++17 reviews some of most popular ‘holes’ and addressed them. In the end, we get a bit clearer way of how things might work.

Today I’d like to mention about:

Evaluation order

Copy elision (optional optimization that seems to be implemented across all of the popular compilers)

Exceptions

Memory allocations for (over)aligned data

The Series

This post is a second in the series about C++17 features details.

The plan for the series

Just to recall:

First of all, if you want to dig into the standard on your own, you can read the latest draft here:

N4659, 2017-03-21, Working Draft, Standard for Programming Language C++ - the link also appears on the isocpp.org.

Compiler support: C++ compiler support

Moreover, I’ve prepared a list of concise descriptions of all of the C++17 language features:

It’s a one-page reference card, PDF.

There’s also a talk from Bryce Lelbach: C++Now 2017: C++17 Features

Stricter expression evaluation order

This one is tough, so please correct me if I am wrong here, and let me know if you have more examples and better explanations. I’ve tried to confirm some details on slack/Twitter, and hopefully I am not writing nonsenses here :)

Let’s try:

C++ doesn’t specify any evaluation order for function parameters. Dot.

For example, that’s why make_unique is not just a syntactic sugar, but actually it guarantees memory safety:

With make_unique :

foo ( make_unique < T >(), otherFunction ());

And with explicit new .

foo ( unique_ptr < T >( new T ), otherFunction ());

In the above code we know that new T is guaranteed to happen before unique_ptr construction, but that's all. For example, new T might happen first, then otherFunction() , and then unique_ptr constructor.

When otherFunction throws, then new T generates a leak (as the unique pointer is not yet created). When you use make_unique , then it’s not possible to leak, even when the order of execution is random. More of such problems in GotW #56: Exception-Safe Function Calls

With the accepted proposal the order of evaluation should be ‘practical.’

Examples:

in f(a, b, c) - the order of evaluation of a, b, c is still unspecified, but any parameter is fully evaluated before the next one is started. Especially important for complex expressions.

if I’m correct that fixes a problem with make_unique vs unique_ptr<T>(new T()) . As function argument must be fully evaluated before other arguments are.

- the order of evaluation of a, b, c is still unspecified, but any parameter is fully evaluated before the next one is started. Especially important for complex expressions. chaining of functions already work from left to right, but the order of evaluation of inner expressions might be different. look here: c++11 - Does this code from “The C++ Programming Language” 4th edition section 36.3.6 have well-defined behavior? - Stack Overflow. To be correct “The expressions are indeterminately sequenced with respect to each other”, see Sequence Point ambiguity, undefined behavior?.

now, with C++17, chaining of functions will work as expected when they contain such inner expressions, i.e., they are evaluated from left to right: a(expA).b(expB).c(expC) is evaluated from left to right and expA is evaluated before calling b…

is evaluated from left to right and is evaluated before calling b… when using operator overloading order of evaluation is determined by the order associated with the corresponding built-in operator:

so std::cout << a() << b() << c() is evaluated as a, b, c.



And from the paper:

the following expressions are evaluated in the order a, then b, then c:

1. a.b

2. a->b

3. a->*b

4. a(b1, b2, b3)

5. b @= a

6. a[b]

7. a << b

8. a >> b

And the most important part of the spec is probably:

The initialization of a parameter, including every associated value computation and side effect, is indeterminately sequenced with respect to that of any other parameter.

StackOverflow: What are the evaluation order guarantees introduced. by C++17?

More details in: P0145R3 and P0400R0. Not yet supported in Visual Studio 2017, GCC 7.0, Clang 4.0

Guaranteed copy elision

Currently, the standard allows eliding in the cases like:

when a temporary object is used to initialize another object (including the object returned by a function, or the exception object created by a throw-expression)

when a variable that is about to go out of scope is returned or thrown

when an exception is caught by value

But it’s up to the compiler/implementation to elide or not. In practice, all the constructors’ definitions are required. Sometimes elision might happen only in release builds (optimized), while Debug builds (without any optimization) won’t elide anything.

With C++17 we’ll get clear rules when elision happens, and thus constructors might be entirely omitted.

Why might it be useful?

allow returning objects that are not movable/copyable - because we could now skip copy/move constructors. Useful in factories.

improve code portability, support ‘return by value’ pattern rather than use ‘output params.’

Example:

// based on P0135R0 struct NonMoveable { NonMoveable ( int ); // no copy or move constructor: NonMoveable ( const NonMoveable &) = delete ; NonMoveable ( NonMoveable &&) = delete ; std :: array < int , 1024 > arr ; }; NonMoveable make () { return NonMoveable ( 42 ); } // construct the object: auto largeNonMovableObj = make ();

The above code wouldn’t compile under C++14 as it lacks copy and move constructors. But with C++17 the constructors are not required - because the object largeNonMovableObj will be constructed in place.

Defining rules for copy elision is not easy, but the authors of the proposal suggested new, simplified types of value categories:

glvalue - ‘A glvalue is an expression whose evaluation computes the location of an object, bit-field, or function. ‘

- ‘A is an expression whose evaluation computes the location of an object, bit-field, or function. ‘ prvalue - A prvalue is an expression whose evaluation initializes an object, bit-field, or operand of an operator, as specified by the context in which it appears

In short: prvalues perform initialization, glvalues produce locations.

Unfortunately, in C++17 we’ll get copy elision only for temporary objects, not for Named RVO (so it covers only the first point, not for Named Return Value Optimization). Maybe C++20 will follow and add more rules here?

More details: P0135R0, MSVC 2017: not yet. GCC: 7.0, Clang: 4.0.

Exception specifications part of the type system

Previously exception specifications for a function didn’t belong to the type of the function, but now it will be part of it.

We’ll get an error in the case:

void (* p )(); void (** pp )() noexcept = & p ; // error: cannot convert to // pointer to noexcept function struct S { typedef void (* p )(); operator p (); }; void (* q )() noexcept = S (); // error: cannot convert to // pointer to noexcept

One of the reasons for adding the feature is a possibility to allow for better optimization. That can happen when you have a guarantee that a function is for example noexcept .

Also in C++17 Exception specification is cleaned up: Removing Deprecated Exception Specifications from C++17 - it’s so-called ‘dynamic exception specifications’. Effectively, you can only use noexcept specifier for declaring that a function might throw something or not.

More details: P0012R1, MSVC 2017: not yet, GCC 7.0, Clang 4.0.

Dynamic memory allocation for over-aligned data

When doing SIMD or when you have some other memory layout requirements, you might need to align objects specifically. For example, in SSE you need a 16-byte alignment (for AVX 256 you need a 32-byte alignment). So you would define a vector4 like:

class alignas ( 16 ) vec4 { float x , y , z , w ; }; auto pVectors = new vec4 [ 1000 ];

Note: alignas specifier is available sice C++11.

In C++11/14 you have no guarantee how the memory will be aligned. So often you have to use some special routines like _aligned_malloc / _aligned_free to be sure the alignment is preserved. That’s not nice as it’s not working with C++ smart pointers and also make memory allocations/deletions visible in the code (we should stop using raw new and delete, according to Core Guidelines).

C++17 fixes that hole by introducing additional memory allocation functions that use align parameter:

void * operator new ( size_t , align_val_t ); void * operator new []( size_t , align_val_t ); void operator delete ( void *, align_val_t ); void operator delete []( void *, align_val_t ); void operator delete ( void *, size_t , align_val_t ); void operator delete []( void *, size_t , align_val_t );

now, you can allocate that vec4 array as:

auto pVectors = new vec4 [ 1000 ];

No code changes, but it will magically call:

operator new []( sizeof ( vec4 ), align_val_t ( alignof ( vec4 )))

In other words, new is now aware of the alignment of the object.

More details in P0035R4. MSVC 2017: not yet, GCC: 7.0, Clang: 4.0.

Summary

Today we’ve focused on four areas where C++ specification is now clearer. We have now ways to assume Copy Ellison will happen, some orders of operations are well defined now, operator new is now aware of the alignment of a type and also exceptions are part of the function declaration.

What are your picks for language clarification?

What are other ‘holes’ needed to be filled?

Next time we’ll address changes for templates and generic programming. So stay tuned!

Once again, remember to grab my C++17 Language Ref Card.