Last Update:
C++17 in details: language clarifications
Table of Contents
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
- Fixes and deprecation
- Language clarification (today)
- Templates
- Attributes
- Simplification
- Library changes - Filesystem
- Library changes - Parallel Algorithms
- Library changes - Utils
- Wrap up, Bonus - with a free ebook! :)
Documents & Links
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:
Download a free copy of my C++17 Cheat Sheet!
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 quaranteed 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
vsunique_ptr<T>(new T())
. As function argument must be fully evaluated before other arguments are.
- if I’m correct that fixes a problem with
- 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 andexpA
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.
- so
And from the paper:
the following expressions are evaluated in the order a, then b, then c:
- a.b
- a->b
- a->*b
- a(b1, b2, b3)
- b @= a
- a[b]
- a << b
- 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
- ‘Aglvalue
is an expression whose evaluation computes the location of an object, bit-field, or function. ‘prvalue
- Aprvalue
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.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: