C++14 What You Need To Know

Download as doc, pdf, or txt
Download as doc, pdf, or txt
You are on page 1of 9

The C++14 Standard: What You Need to Know

By Mark Nelson, September 16, 2014

5 Comments

New, useful features that make the language safer and more convenient.

Voting on the new C++14 standard completed in August with approval of the document. All
that remains before we can say it is officially complete is publication by the ISO. In this
article, I visit the high points of the new standard, demonstrating how the upcoming changes
will affect the way you program, particularly when using the idioms and paradigms of what is
termed Modern C++.

The committee seems intent on keeping the standards process in a higher gear than in
decades past. This means that C++14, having had just three years since the last standard, is
a somewhat constrained release. Far from being disappointing, this is a boon for
programmers because it means implementers are able to push out compliance with the new
features in real time. Yes, you can start using C++14 features today — nearly all of them if
you are flexible in your tool chain.

At this point you can get a free copy of the draft proposal here. Unfortunately, when the final
standard is published, ISO will have it paywalled.

Shortening the time-frame between releases is working to help compiler writers keep up with
the language changes in something closer to real time. With just three years between
releases, there are fewer changes to adjust to.

The examples in this article were mostly tested with clang 3.4, which has great coverage of
C++14 features; g++ has a somewhat smaller list of features covered; and Visual C++
seems to be trailing the pack.

C++14: What's Important

What follows are descriptions of the C++14 changes that will have significant impact in your
coding work, along with working examples and discussions of when and why you would
employ the features.

Return type deduction

The capabilities auto are expanded in this release. C++ itself continues to be typesafe, but
the mechanics of type safety are increasingly being performed by the compiler instead of the
programmer.

In C++11, programmers starting using auto for declarations. This was keenly appreciated
for things like iterator creation, when the fully qualified type name might be horrendous.
Newly minted C++ code was much easier to read:

?
1 for ( auto ii = collection.begin() ; ...

This code is still completely typesafe — the compiler knows what type begin() returns in
that context, so there is no question about what type ii is, and that will be checked every
place it is used.
In C++14, the use of auto is expanded in a couple of ways. One that makes perfect sense
is that of return type deduction. If I write a line of code like this inside a function:

?
1 return 1.4;

It is obvious to both me and the compiler that the function is returning a double. So in C+
+14, I can define the function return type as auto instead of double:

?
1 auto getvalue() {

The details of this new feature are pretty easy to understand. For example, if a function has
multiple return paths, they need to have the same type. Code like this:

?
1 auto f(int i)
2 {
3 if ( i < 0 )
4 return -1;
5 else
6 return 2.0
7 }

might seem like it should obviously have a deduced return type of double, but the
standard prohibits this ambiguity, and the compiler property complains:

error_01.cpp:6:5: error: 'auto' in return type deduced as


'double' here but deduced as 'int' in
earlier return statement
return 2.0
^
1 error generated.
There several good reasons why deducing the return type is a plus for your C++ programs.
First, there are times when you need to return a fairly complex type, such as an iterator,
perhaps when searching into a standard library container. The auto return type makes the
function easier to write properly, and easier to read. A second (maybe less obvious) reason is
that using an auto return type enhances your ability to refactor. As an example, consider
this program:

?
1 #include <iostream>
2 #include <vector>
3 #include <string>
4
5 struct record {
6 std::string name;
7 int id;
8 };
9
10 auto find_id(const std::vector<record> &people,
11 const std::string &name)
12 {
13 auto match_name = [&name](const record& r) -> bool {
14 return r.name == name;
15
};
16 auto ii = find_if(people.begin(), people.end(), match_name );
17 if (ii == people.end())
18 return -1;
19 else
20 return ii->id;
21 }
22
23
int main()
24
{
25
std::vector<record> roster = { {"mark",1},
26
27
{"bill",2},
28 {"ted",3}};
29 std::cout << find_id(roster,"bill") << "\n";
30 std::cout << find_id(roster,"ron") << "\n";
}

In this example, I'm not saving many brain cells by


having find_id() return auto instead of int. But consider what happens if I decide
that I want to refactor my record structure. Instead of using an integral type to identify
the person in the record object, maybe I have a new GUID type:

?
1 struct record {
2 std::string name;
3 GUID id;
4 };

Making that change to the record object will cause a series of cascading changes in things
like the return types of functions. But if my function uses automatic return type deduction,
the compiler will silently make the change for me.

Any C++ programmer who has worked on a large project is familiar with this issue. Making a
change to a single data structure can cause a seemingly endless series of iterations through
the code base, changing variables, parameters, and return types. The increased use
of auto does a lot to cut through this bookkeeping.

Note that in the example above, and in the rest of this article, I create and use a named
lambda. I suspect that most users of lambdas with functions like std::find_if() will
define their lambdas as anonymous inline objects, which is a very convenient style. Due to
limited page width, I think it is a little easier to read code in your browser when lambdas are
defined apart from their usage. So this is not necessarily a style you should emulate, you
should just appreciate that it is somewhat easier to read. In particular, it will be much easier
if you are light on lambda experience.

Turning back to auto, an immediate consequence of using it as a return type is the reality of
its doppelgänger, decltype(auto), and the rules it will follow for type deduction. You
can now use it to capture type information automatically, as in this fragment:

?
1 template<typename Container>
2 struct finder {
3 static decltype(Container::find) finder1 = Container::find;
4 static decltype(auto) finder2 = Container::find;
5 };

Generic Lambdas
Another place where auto has insinuated itself is in the definitions of lambda parameters.
Defining lambda parameters with an auto type declaration is the loose equivalent of
creating a template function. The lambda will be instantiated in a specific embodiment based
on the deduced types of the arguments.

This is convenient for creating lambdas that can be reused in different contexts. In the simple
example below, I've created a lambda used as a predicate in a standard library function. In
the C++11 world, I would have needed to explicitly instantiate one lambda for adding
integers, and a second for adding strings.

With the addition of generic lambdas, I can define a single lambda with generic parameters.
Although the syntax doesn't include the keyword template, this is still clearly a further
extension of C++ generic programming:

?
#include <iostream>
1
#include <vector>
2
3 #include <string>
4 #include <numeric>
5
6 int main()
7 {
8 std::vector<int> ivec = { 1, 2, 3, 4};
9 std::vector<std::string> svec = { "red",
10 "green",
11 "blue" };
12 auto adder = [](auto op1, auto op2){ return op1 + op2; };
13 std::cout << "int result : "
14 << std::accumulate(ivec.begin(),
15 ivec.end(),
16 0,
17 adder )
18 << "\n";
19 std::cout << "string result : "
20 << std::accumulate(svec.begin(),
21 svec.end(),
22 std::string(""),
23 adder )
24 << "\n";
25 return 0;
26 }

Which produces the following output:

?
1 int result : 10
2 string result : redgreenblue

Even if you are instantiating anonymous inline lambdas, employing generic parameters is still
useful for the reasons I discussed earlier in this article. When your data structures change, or
functions in your APIs get signature modifications, generic lambdas will adjust with
recompilation instead of requiring rewrites:

?
1 std::cout << "string result : "
2 << std::accumulate(svec.begin(),
3 svec.end(),
4 std::string(""),
5 [](auto op1,auto op2){ return op1+op2; } )
6 << "\n";
Initialized Lambda Captures

In C++11, we had to start adjusting to the notion of a lambda capture specification. That
declaration guides the compiler during the creation of the closure: an instance of the function
defined by the lambda, along with bindings to variables defined outside the lambda's scope.

The C++14 Standard: What You Need to Know


By Mark Nelson, September 16, 2014

5 Comments

New, useful features that make the language safer and more convenient.

In the earlier example on deduced return types, I had a lambda definition that captured a
single variable, name, used as the source of a search string in a predicate:

?
1 auto match_name = [&name](const record& r) -> bool {
2 return r.name == name;
3 };
4 auto ii = find_if(people.begin(), people.end(), match_name );

This particular capture gives the lambda access to the variable by reference. Captures can
also be performed by value, and in both cases, the use of the variable behaves in a way that
fits with C++ intuition. Capture by value means the lambda operates on a local copy of a
variable; capture by reference means the lambda operates on the actual instance of the
variable from the outer scope.

All this is fine, but it comes with some limitations. I think the one that the committee felt it
needed to address was the inability to initialize captured variables using move-only
semantics.

What does this mean? If we expect that lambda is going to be a sink for a parameter, we
would like to capture the outer variable using move semantics. As an example, consider how
you would get a lambda to sink a unique_ptr, which is a move-only object. A first
attempt to capture by value fails:

?
1 std::unique_ptr<int> p(new int);
2 *p = 11;
3 auto y = [p]() { std::cout << "inside: " << *p << "\n";};

This code generates a compiler error because unique_ptr does not generate a copy
constructor — it specifically wants to ban making copies.

Changing this so that p is captured by reference compiles fine, but it doesn't have the desired
effect of sinking the value by moving the value into the local copy. Eventually, you could
accomplish this by creating a local variable and calling std::move() on your captured
reference, but this is a bit inefficient.

The fix for this is a modification of the capture clause syntax. Now, instead of just declaring a
capture variable, you can do an initialization. The simple case that is used as an example in
the standard looks like this:
?
1 auto y = [&r = x, x = x+1]()->int {...}

This captures a copy of x and increments the value simultaneously. The example is easy to
understand, but I'm not sure it captures the value of this new syntax for sinking move-only
variables. A use case that takes advantage of this shown here:

?
1 #include <memory>
2 #include <iostream>
3
4 int main()
5 {
6 std::unique_ptr<int> p(new int);
7 int x = 5;
8 *p = 11;
9 auto y = [p=std::move(p)]() { std::cout << "inside: " << *p << "\n";};
10 y();
11 std::cout << "outside: " << *p << "\n";
12 return 0;
13 }

In this case, the captured value p is initialized using move semantics, effectively sinking the
pointer without the need to declare a local variable:

inside: 11
Segmentation fault (core dumped)

That annoying result is what you expect — the code attempts to dereference p after it was
captured and moved into the lambda.

The [[deprecated]] Attribute

The first time I saw the use of the deprecated attribute in Java, I admit to a bit of language
envy. Code rot is a huge problem for most programmers. (Ever been praised for deleting
code? Me neither.) This new attribute provides a systematic way to attack it.

Its use is convenient and simple — just place the [[deprecated]] tag in front of a
declaration — which can be a class, variable, function, or a few other things. The result looks
like this:

?
1 class
2 [[deprecated]] flaky {
3 };

When your program uses a deprecated entity, the compiler's reaction is left up to the
implementer. Clearly, most people are going to want to see some sort of warning, and also to
be able to turn that warning off at will. As an example, clang 3.4 gave this warning when
instantiating a deprecated class:

?
1 dep.cpp:14:3: warning: 'flaky' is deprecated [-Wdeprecated-declarations]
2 flaky f;
3 ^
4 dep.cpp:3:1: note: 'flaky' declared here
5 flaky {
6
^

Note that the syntax of C++ attribute-tokens might seem a bit unfamiliar. The list of
attributes, including [[deprecated]], comes after keywords like class or enum, and
before the entity name.

This tag has an alternate form that includes a message parameter. Again, it is up to the
implementer to decide what to do with this message. clang 3.4 apparently ignores the
message. The output from this fragment:

?
1 class
2 [[deprecated]] flaky {
3 };
4
5 [[deprecated("Consider using something other than cranky")]]
6 int cranky()
7 {
8 return 0;
9 }
10
11 int main()
12 {
13 flaky f;
14 return cranky();
15 }

does not contain the error message:

dep.cpp:14:10: warning: 'cranky' is deprecated [-


Wdeprecated-declarations]
return cranky();
^
dep.cpp:6:5: note: 'cranky' declared here
int cranky()
^
Binary Literals and Digit Separators

These two new features aren't earth-shaking, but they do represent nice syntactic
improvements. Small changes like these give us some incremental improvements in the
language that improve readability, and hence, reduce bug counts.

C++ programmers can now create binary literals, adding to the existing canon of decimal,
hex, and the rarely used octal radices. Binary literals start with the prefix 0b and are
followed by binary digits.

In the U.S. and UK, we are accustomed to using commas as digit separators in written
numbers, as in: $1,000,000. These digit separators are there purely for the convenience of
readers, providing syntactic cues that make it easier for our brains to process long strings of
numbers.

The committee added digit separators to C++ for exactly the same reasons. They won't affect
the evaluation of a number, they are simply present to make it easier to read and write
numbers through chunking.
What character to use for a digit separator? Virtually every punctuation character already has
an idiosyncratic use in the language, so there are no obvious choices. The final election was
to use the single quote character, making the million dollar value render in C++
as: 1'000'000.00. Remember that the separators don't have any effect on the
evaluation of the constant, so this value would be identical to 1'0'00'0'00.00.

An example combining the use of both new features:

?
1 #include <iostream>
2
3 int main()
4 {
5 int val = 0b11110000;
6 std::cout << "Output mask: "
7 << 0b1000'0001'1000'0000
8 << "\n";
9 std::cout << "Proposed salary: $"
10 << 300'000.00
11 << "\n";
12 return 0;
13 }

This program gives the unsurprising output:

Output mask: 33152


Proposed salary: $300000
The remainder

Some additional features in the C++14 specification don't require quite as much exposition.

Variable templates are an extension of templates to variables. The example used everywhere
is an implementation of variable pi<T>. When implemented as a double, the variable will
return 3.14, when implemented as an int, it might return 3, and "3.14" or perhaps "pi" as
an std::string. This would have been a great feature to have when <limits> was
being written.

The syntax and semantics of variable templates are nearly identical to those for class
templates — you should have no trouble using them without any special study.

The restrictions on constexpr functions have been relaxed, allowing, for example,
multiple returns, internal case and if statements, loops, and more. This expands the scope
of things that are done at compile time, a trend that really took wing when templates were
introduced.

Additional minor features include sized deallocations and some syntax tidying.

What Next?

The C++ committee clearly feels pressure to keep the language current through
improvements, and it is already working on at least one more standard in this decade, C+
+17.

Possibly more interesting is the creation of several spin-off groups that can create technical
specifications, documents that won't rise to the level of a standard but will be published and
endorsed by the ISO committee. Presumably these can be issued at a more rapid clip. The
eight areas currently being worked include:

 File system
 Concurrency
 Parallelism
 Networking
 Concepts, the AI of C++ — always one round of specification away

Success of these technical specifications will have to be judged by adoption and use. If we
find that all the implementers line up behind them, then this new track for standardization
will be a success.

C/C++ has held up well over the years. Modern C++, which we might mark as starting with
C++11, has taken dramatic strides in making the language easier to use and safer without
making concessions in the areas of performance. For certain types of work it is hard to think
of any reasonable alternative to C or C++. The C++ 14 standard doesn't make any jumps as
large as that in the C++11 release, but it keeps the language on a good path. If the
committee can keep its current level of productivity for the rest of the decade, C++ should
continue to be the language of choice when performance is the defining goal.

Mark Nelson is a frequent contributor to Dr. Dobb's and lead author of the Data Compression
Book.

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy