Modern C++ migration

Welcome to the std::promised land

Auto

Type inference is a game changer. Essentially you can simplify complicated (or unknown) type declarations with auto. But it can be a balance of convenience over readability.

int x1 = 5; // Explicit
auto x2 = 5; // What's the underlying type?

std::vector<std::string> moon = {"Don't", "look", "at", "the", "finger"};
auto finger = moon.front();

And there are a few perfectly valid gotchas. Let's create a variable and a reference to it, updating y2 (below) also updates y1 as expected.

int y1 = 1;
int &y2 = y1;
y2 = 2;

But how does auto deal with references? Do you get another reference or a copy? (Hint: auto "decays" to the base type -- no consts, no refs).

int y1 = 1;
int &y2 = y1;
auto y3 = y2;
auto &y4 = y2;

Brace initialisers

These take a bit of getting used to but they do give you extra checks. For example the compiler coughs a "narrowing" warning for the following.

double wide{1.0}; float narrow{wide};

Initialiser lists

We used to create a vector and then push elements onto it (ignoring the potential copy overhead of resizing vectors). But with initialiser lists you can populate containers much more concisely.

Initialise a container.

std::list v1{1, 2, 3, 4, 5, 6};

Initialise a pair. You can also use them instead of std::make_pair.

std::list v1{1, 2, 3, 4, 5, 6};
std::pair<int, std::string> p1{1, "two"};

Initialise more complex types.

std::list v1{1, 2, 3, 4, 5, 6};
struct S {
int x;
struct Foo {
int i;
int j;
int a[3];
} b;
};

S s1 = {1, {2, 3, {4, 5, 6}}};

Range-based for loops

Clumsy explicit iterator declarations can be cleaned up with auto.

for (std::list::iterator i = v2.begin(); i != v2.end(); ++i)
*i += 1;

for (auto i = v2.begin(); i != v2.end(); ++i)
*i += 1;

In fact we can drop the iterators altogether and avoid that *i dereferencing idiom.

for (auto &i : v4)
i += 1;

Note that you don't have access to the current index (until C++20). Which isn't necessarily a bad thing.

Lambda expressions

Think function pointers but a much friendlier implementation. Call like a regular function or pass them as a parameter. You can also define them in-place so you don't have to go hunting for the implementation like you might if you passed a function name. Here's another new for-loop variation too. Note the use of std::cbegin() rather than the method.

const auto printer = []{ std::cout << "I am a first-class citizenn"; return; };

// Call like a function
printer();

// In-place lambda definition
const std::vector d{0.0, 0.1, 0.2};

std::for_each(std::cbegin(d), std::cend(d),
[](const auto &i) { std::cout << i << "n"; });

Threads

STL threads are much neater than the old POSIX library but futures are really interesting and let you return the stuff you're interested in much more easily.

Let's define a processor-heavy routine as a lambda. Here the return has been declared explicitly.

const auto complicated = []() -> int { return 1; };

And then push our complicated routine into the background and get on with something else. Note we don't need to define what f is thanks to auto. (It's actually a std::future.)

auto f = std::async(std::launch::async, complicated);

When we're ready, we block to get the value. We could change the return type of complicated() and nothing else needs to change.

std::cout << f.get() << "n";

Optional types

This overcomes the problem of defining a "not initialised" value (-1) which will inevitably used to index an array and cause an explosion. Your functions can now effectively return a "no result". Let's create a container with some default entries.

std::deque<std::optional> options{0, 1, 2, 3, 4};

Then make the one at the back undefined.

options.back() = {};

And count the valid entries with the help of a lambda expression.

const auto c = std::count_if(std::cbegin(options), std::cend(options),
[](const auto &o) { return o; });

Digit separators

If you're defining hardware interfaces then you'll probably have register maps defined as hexadecimals. Using digit separators can help improve readability in some cases.

int reg1 = 0x5692a5b6;
int reg2 = 0x5692'a5b6;
double reg3 = 1'000.000'001;

You can even define things in binary if it's clearer. And also specify the size of a type explicitly – a 32-bit integer, say – rather than letting the compiler decide.

const uint32_t netmask{0b11111111'11111111'11111111'00000000};

Type aliases

Create type-safe typedefs with using. Note the trailing cluster of angle-brackets are parsed correctly in C++11 (no need to insert spaces).

using container_t = std::vector>; container_t safe;

Raw strings

Avoid clumsy escape characters with raw strings.

const std::string regex{R"(
^                                             # start of string
(                                             # first group start
  (?:
    (?:[^?+*{}()[\]\\|]+                      # literals and ^, $
     | \\.                                    # escaped characters
     | \[ (?: \^?\\. | \^[^\\] | [^\\^] )     # character classes
          (?: [^\]\\]+ | \\. )* \]
     | \( (?:\?[:=!]|\?<[=!]|\?>)? (?1)?? \)  # parenthesis, with recursive content
     | \(\? (?:R|[+-]?\d+) \)                 # recursive matching
     )
    (?: (?:[?+*]|\{\d+(?:,\d*)?\}) [?+]? )?   # quantifiers
  | \|                                        # alternative
  )*                                          # repeat content
)                                             # end first group
$
)"};

Structured bindings

You might declare intermediate variables to make the first/second more meaningful below.

std::pair<std::string, std::string> chuckle{"to me", "to you"};
std::cout << chuckle.first << ", " << chuckle.second << "n";

But you can also do it in one expression with structured bindings.

auto [barry, paul] = chuckle;
std::cout << barry << ", " << paul << "n";

const everything

Not a modern feature of course, but: make everything constant. You should be prefixing const as a matter of course and then removing it when you have to: it’s much easier to reason about code when the data are immutable. In an ideal world everything would be constant -- like Haskell -- but it’s a balance of reason and getting things done.

Standard literals

using namespace std::complex_literals;
using namespace std::string_literals;
using namespace std::chrono_literals;

// auto deduces complex
auto z = 1i;

// auto deduces string
auto str = "hello world"s;

// auto deduces chrono::seconds
auto dur = 60s;

// Or if you just want them all
using namespace std::literals;

Tuples

Like pairs but better. Arbitrary collection of heterogeneous types. You can retrieve values by index (which looks a bit odd) or even by type!

std::tuple<std::string, double, int> h1{"one", 2.0, 3};
std::string << " " << std::get<0> << " " << std::get<1> << "n";

std::byte

When you really want something to be a byte and not something that looks a bit like a char.

std::byte b1{4};

Exchanging values

Replace that old "declare a temporary variable" idiom with an atomic update. std::exchange also returns the original value.

int current = 5;
int previous = std::exchange(current, 6);

Inline keyword

Just let the compiler decide what should be inlined. It will probably ignore you anyway.

Move semantics

This is a biggie that you exploit just by moving to C++11 and beyond. The compiler can now choose to move data where previously it would have copied it, potentially giving huge performance benefits.

Smart pointers

You no longer need to use new and delete explicitly. Smart pointers clean up after themselves when they go out of scope: Resource Allocation Is Initialistion (RAII).

RAII

Used by:

  • std::vector
  • std::string
  • std::thread

The STL also offers wrappers to manager RAII:

std::unique_ptr and std::shared_ptr to manage dynamically-allocated memory or, with a user-provided deleter, any resource represented by a plain pointer; std::lock_guard, std::unique_lock, std::shared_lock to manage mutexes.

See CPP Reference.

References

results matching ""

    No results matching ""