C++ essentials

C++14

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";

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
$
)"};

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.

C++17

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; });

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};

References

results matching ""

    No results matching ""