This post was originally written on Codeforces; relevant discussion can be found here.

People often say C++ is much more cumbersome to write than any sane language should be, but I think that is mainly due to lack of knowledge about how to write modern C++ code. Here’s how you can write code more smartly and succinctly in C++(20):

Negative indexing with end

Have you ever found yourself using a vector like a stack just because you have to also access the last few positions, or writing a sliding window code using a deque (where the size might be variable, but you want to access an element at some offset from the end)?

Chances are that you have written code like this:

while (stk.size() >= 3 && stk[stk.size() - 2] + stk[stk.size() - 1] > stk[stk.size() - 3]) {
  stk.pop_back();
}

A cleaner way of writing the same thing is this:

while (stk.size() >= 3 && end(stk)[-2] + end(stk)[-1] > end(stk)[-3]) {
  stk.pop_back();
}

This trick works not just for end, but for any iterator, provided that (roughly) there is no undefined behaviour like out of bounds accesses.

This can be done only with iterators which provide random access. For example, iterators for vectors but not for sets.

Free functions like len and str

Python’s len and str come in handy a lot of times, and they’re convenient to use because they’re free functions (that is, not member functions).

Fortunately, C++ has its own set of free functions that are quite helpful:

  1. size(v): It returns the unsigned size of \(v\), and does the same thing as v.size() That is, its numerical value is the size of \(v\), and type is some unspecified unsigned type (aliased to size_t). This also works for arrays (but not raw pointers).
  2. ssize(v): It returns the signed size of \(v\). This is quite useful, because it prevents bugs like a.size() - 1 on empty a. It doesn’t have a member function analogue, though. This also works for arrays (but not raw pointers). If you use sz macros, you can safely remove them from your code!
  3. begin(v), end(v), rbegin(v), rend(v): They do the same thing as what their corresponding member functions do. These also work for arrays (but not raw pointers).
  4. empty(v): This checks if \(v\) is empty or not. Analogous to v.empty()
  5. data(v): This returns a pointer to the block of data where it works.
  6. to_string(x): This returns the string representation of a datatype. They’re only defined for things like int and double though.

Variable declaration in loops/conditions

If you have used range-based for loops, you might have noticed that there is no support for indexing by default. So people usually either just switch to index based loop, or do the following:

int i = -1;
for (auto x : a) {
  i++;
  // do something
}
// or
{
  int i = -1;
  for (auto x : a) {
    i++;
    // do something
  }
}

However, it either pollutes the parent scope with the variable i and makes things buggy, or just looks ugly. C++20 has the following feature that makes it cleaner:

for (int i = -1; auto x : a) {
  i++;
  // do something
}

Similarly, if there is an object such that you only have to inspect its properties just once and it becomes useless later on, you can construct it in the init part of the if condition, like this:

if (auto x = construct_object(); x.good() && !x.bad()) {
  // do something
}

Unpacking structs

A lot of people write code that looks like this:

std::vector<std::tuple<int, char, std::string>> a;
// populate a
for (int i = 0; i < (int)a.size(); ++i) {
  int x = std::get<0>(a[i]);
  char y = std::get<1>(a[i]);
  std::string z = std::get<2>(a[i]);
}
// or
for (auto& A : a) {
  int x = std::get<0>(A);
  char y = std::get<1>(A);
  std::string z = std::get<2>(A);
}
// or
for (auto& A : a) {
  int x; char y; std::string z;
  std::tie(x, y, z) = A;
}

A cleaner and more consistent way of writing this can be:

for (auto [x, y, z] : a) {
}
// or
for (auto& A : a) {
  auto [x, y, z] = A;
}

If you want references instead of copies stored in x, y, z, then you can do this:

for (auto& [x, y, z] : a) {
}
// or
for (auto& A : a) {
  auto& [x, y, z] = A;
}

Note that this is not limited to for loops, or even to just tuples, and you can use this sort of unpacking (structured binding) for most structs (in particular, custom structs you make, or std::pair). In fact, if you have std::array<int, 3>, then this unpacking will give you its contents in three different variables.

Some more utilities

There are some lesser known functionalities that C++20 introduced, like starts_with and ends_with on strings. Some of the more useful ones can be found here.

Notably, there is also support for binary searching like we do in competitive programming, and the relevant information can be found in the language specific section of this post.

You can also print a number in binary, analogously to what you can do in Python. This can be done by constructing a bitset out of the integer and converting it to a string, or just by printing std::format("{:b}", a) (which is unfortunately not on CF yet).

emplace_back, emplace_front and emplace

Seriously, stop using a.push_back(make_pair(1, 2)) and a.push_back(make_tuple(1, 2, 3)) and even a.push_back({1, 2, 3}), and start using a.emplace_back(1, 2) instead.

The only reasonably common place this doesn’t work is when you have a container that stores std::array, so emplace_back doesn’t work; in that case a.push_back({1, 2, 3}) is fine.

The other variants: emplace_front and emplace do the same thing for their other counterparts (for example, in a deque, queue or priority_queue).

Lambdas

Lambdas in C++ are in fact cleaner and more intuitive (with more features) than in Python. Compared to both Python lambdas (being able to have more than 1 line of code) and local functions (having mutable states more intuitively and capturing parameters from an ancestor scope), in fact. This is because you can capture stuff more explicitly, have mutable state, can pass them as function objects, and even implement recursion using generic lambdas, though C++23 will have a feature called deducing this which will make a lot of things much simpler.

They can be passed around to almost all algorithms in the STL algorithms library, which is part of the reason why the STL is so powerful and generic.

They can also be used to make certain functions local, and avoid polluting the global namespace with function definitions. One more thing about them is that they are constexpr by default, which is not true for traditional functions.

Vector-like data structure without iterator invalidation

You probably know what we’re coming to at this point: std::deque. It is essentially std::vector (without \(O(1)\) move guarantees but whatever), but with no iterator invalidation on resizing/adding elements.

That is why it is ideal in a very important setting:

  • Making buffers of elements (so that we can use pointers to elements without worrying about what happens when the buffer is resized).

No more Python laughing in our face about having to worry about iterator invalidation!

Python style ranges and even more flexible range manipulation

Using temporary container in for loop

Suppose you want to do something for multiple arguments, but they aren’t indices. Most people would either duplicate code, or do something like this:

vector<char> a{'L', 'R', 'D', 'U'};
for (auto x : a) { /* do something */ }

But this has a disadvantage: it needs more characters and also makes it more error-prone to use vectors. Instead of this, you can do this:

for (auto x : {'L', 'R', 'D', 'U'}) { /* do something */ }

or even

for (auto x : "LRDU"s) { /* do something */ }

Note that the s at the end makes it a string literal which is why it works, and const char* doesn’t work because there is a \0 at the end of the literal. This can still allocate on the heap (not in this case, only for big strings due to SSO).

In case you explicitly want an allocation with a vector for some reason, you can use

for (auto x : std::vector{'L', 'R', 'D', 'U'})

or

for (auto x : std::vector<char>{'L', 'R', 'D', 'U'})

Ranges

C++20 has a feature called ranges, that allows you to do manipulations on, well, ranges.

Let’s think about how to implement this: For integers between 1 to 5, multiply them by 2, and print the last 3 elements in reversed order.

#include <bits/stdc++.h>

int main() {
  std::vector<int> a{1, 2, 3, 4, 5};
  for (auto& x : a) x *= 2;
  for (int i = 0; i < 3; ++i) std::cout << a[(int)a.size() - i - 1] << '\n';
}

However, this is prone to having bugs (what about out of bounds errors in some other scenario?), and doesn’t look intuitive at all.

Now consider this:

#include <bits/stdc++.h>

namespace R = std::ranges;
namespace V = std::ranges::views;

int main() {
    for (auto x : V::iota(1, 6) | V::transform([](int x) { return x * 2; }) | V::reverse | V::take(3))
        std::cout << x << '\n';
}

Here the intent is pretty clear. You take integers in \([1, 6)\), multiply them by 2, reverse them, and take the first 3. Now in this resulting range of numbers, you print each of them.

So let’s see what happens here. The | operator is defined for ranges and an operation on ranges that returns a range. For example, V::transform(range, lambda) gives a range where every element is transformed, and doing range | V::transform(lambda) does the same thing. In fact, for a lot of cases, this isn’t stored in memory at all (and operations are done while iterating on the fly), unless you have things like reversing a range.

One thing that you can finally do is split a string with delimiters in a nice way (which used to be a major point of ridicule directed towards C++ from a lot of Python users). This is as simple as using a split view in the ranges library. For more information, see this. Here’s an example taken verbatim from the cppreference page:

#include <iomanip>
#include <iostream>
#include <ranges>
#include <string_view>

int main() {
    constexpr std::string_view words{"Hello^_^C++^_^20^_^!"};
    constexpr std::string_view delim{"^_^"};
    for (const auto word : std::views::split(words, delim))
        std::cout << std::quoted(std::string_view{word.begin(), word.end()}) << ' ';
}

A more common example can be like this:

#include <bits/stdc++.h>

namespace R = std::ranges;
namespace V = std::ranges::views;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    std::string s(1000000, '0');
    for (int i = 0; i < 1000000 / 2; ++i) s[i * 2] = '1';
    for (const auto word : V::split(s, '1'))
        std::cout << std::string_view{begin(word), end(word)} << ' ';
}

Some ranges functionality is as follows:

  • converting to and from a range: conversion to a range is implicit for most STL containers. If r is a range, then for most STL containers, like std::vector, you can do something like std::vector<int> a(r.begin(), r.end()). C++23 has a feature R::to<ContainerType>(range) that will construct a container out of a range, and you could even do range | R::to<ContainerType>().
  • iota: This works similarly to Python’s range() but without the stride.
  • reverse: This works similarly to Python’s reversed()
  • take: This takes the first \(n\) elements of a range (unless you run out of elements by then)
  • drop: This takes everything but the first \(n\) elements of a range (unless you run out of elements by then)
  • counted: This takes \(n\) elements of a range starting from an iterator.
  • keys, values: keys takes a range whose elements are pair-like and returns a range of their first elements (so if we have (key, value) pairs, it will return keys), similar to .keys() in Python. Similarly, values returns a range of the values.
  • elements: if we have a range of tuple-like elements, this takes the \(n\)-th members of each of these tuples (where \(N\) is a compile-time constant).
  • all: though type conversions from containers to ranges are usually implicit, it takes a container and takes everything in that container.
  • Some upcoming C++23 features that will make it even more convenient to use ranges: to, zip, slide, stride, repeat, adjacent, pairwise, cartesian_product.

Note that this is not the only thing you can do with ranges. In fact, with almost all possible algorithms that you can find in the algorithms library that use a begin and an end iterator (like std::sort or std::partial_sum or std::all_of), you can find a ranges version here, where you can just pass the range/container name instead of the begin and end iterators you usually pass.

By the way, if you don’t want to wait for C++23, you can use some of the functionality from this code, which I used to use a couple of years ago when C++20 was not a thing.

Spoiler
template <typename T>
struct iterable;
template <typename T>
struct iterable<T&> {
    using type = T&;
};
template <typename T>
struct iterable<T&&> {
    using type = T;
};
template <typename T, std::size_t N>
struct iterable<T (&&)[N]> {
    using type = typename T::unsupported;
};
template <typename T>
struct iterator_from_iterable {
    using iterable = typename std::remove_reference<T>::type&;
    using type = decltype(std::begin(std::declval<iterable>()));
};
template <typename T>
struct iterable_traits {
    using raw_iterable = T;
    using raw_iterator = typename iterator_from_iterable<raw_iterable>::type;
    using wrapped_iterable = typename iterable<T>::type;
    using deref_value_type = decltype(*std::declval<raw_iterator>());
};

template <typename T>
struct Range {
   private:
    const T l, r, skip;

   public:
    struct It : public std::iterator<std::forward_iterator_tag, T> {
        T i;
        const T skip;
        explicit It(T _i, T _skip) : i(_i), skip(_skip) {}
        It& operator++() { return i += skip, *this; }
        const It& operator++(int) {
            auto temp = *this;
            return operator++(), temp;
        }
        T operator*() const { return i; }
        bool operator!=(const It& it) const { return (skip >= 0) ? (i < *it) : (i > *it); }
        bool operator==(const It& it) const { return (skip >= 0) ? (i >= *it) : (i <= *it); }
    };
    using iterator = It;
    using value_type = T;
    Range(T _l, T _r, T _skip = 1) : l(_l), r(_r), skip(_skip) {
#ifdef DEBUG
        assert(skip != 0);
#endif
    }
    Range(T n) : Range(T(0), n, T(1)) {}
    It begin() const {
        return It(l, skip);
    }
    It end() const {
        return It(r, skip);
    }
    It begin() {
        return It(l, skip);
    }
    It end() {
        return It(r, skip);
    }
};

template <typename... T>
struct zip {
   public:
    using value_type = std::tuple<typename iterable_traits<T>::deref_value_type...>;
    using wrapped_iterables = std::tuple<typename iterable_traits<T>::wrapped_iterable...>;
    using raw_iterators = std::tuple<typename iterable_traits<T>::raw_iterator...>;
    using sequence = std::index_sequence_for<T...>;
    struct It : public std::iterator<std::forward_iterator_tag, value_type> {
       public:
        explicit It(raw_iterators iterators) : iterators_(std::move(iterators)) {}
        bool operator==(const It& it) const { return any_eq(it, sequence()); }
        bool operator!=(const It& it) const { return !any_eq(it, sequence()); }
        value_type operator*() const { return deref(sequence()); }
        It& operator++() { return inc(sequence()), *this; }
        const It& operator++(int) {
            auto temp = *this;
            return operator++(), temp;
        }

       private:
        raw_iterators iterators_;
        template <std::size_t... I>
        bool any_eq(const It& it, std::index_sequence<I...>) const {
            return (... || (std::get<I>(iterators_) == std::get<I>(it.iterators_)));
        }
        template <std::size_t... I>
        value_type deref(std::index_sequence<I...>) const {
            return {(*std::get<I>(iterators_))...};
        }
        template <std::size_t... I>
        void inc(std::index_sequence<I...>) {
            (++std::get<I>(iterators_), ...);
        }
    };
    using iterator = It;
    explicit zip(T&&... iterables) : iterables_(std::forward<T>(iterables)...) {}
    It begin() { return begin_(sequence()); }
    It end() { return end_(sequence()); }
    It begin() const { return begin_(sequence()); }
    It end() const { return end_(sequence()); }

   private:
    wrapped_iterables iterables_;
    template <std::size_t... Int>
    iterator begin_(std::index_sequence<Int...>) {
        return iterator(std::tuple(std::begin(std::get<Int>(iterables_))...));
    }
    template <std::size_t... Int>
    iterator end_(std::index_sequence<Int...>) {
        return iterator(std::tuple(std::end(std::get<Int>(iterables_))...));
    }
};

template <typename... T>
zip(T&&...) -> zip<T&&...>;

template <typename T>
struct enumerate {
   public:
    using size_type = typename std::make_signed<std::size_t>::type;
    using wrapped_iterable = typename iterable_traits<T>::wrapped_iterable;
    using raw_iterator = typename iterable_traits<T>::raw_iterator;
    using value_type = std::pair<size_type, typename iterable_traits<T>::deref_value_type>;
    struct It : public std::iterator<std::forward_iterator_tag, value_type> {
        raw_iterator iter_;
        size_type start_;

       public:
        It(raw_iterator it, size_type start) : iter_(it), start_(start) {}
        bool operator==(const It& it) const { return iter_ == it.iter_; }
        bool operator!=(const It& it) const { return iter_ != it.iter_; }
        It& operator++() { return ++iter_, ++start_, *this; }
        const It operator++(int) {
            auto temp = *this;
            return operator++(), temp;
        }
        value_type operator*() const { return {start_, *iter_}; }
    };
    using iterator = It;
    explicit enumerate(T&& iterable, size_type start = 0) : iterable_(std::forward<T>(iterable)), start_(start) {}
    It begin() { return It(std::begin(iterable_), start_); }
    It end() { return It(std::end(iterable_), 0); }
    It begin() const { return It(std::begin(iterable_), start_); }
    It end() const { return It(std::end(iterable_), 0); }

   private:
    wrapped_iterable iterable_;
    size_type start_;
};

template <typename T>
enumerate(T&&) -> enumerate<T&&>;
template <typename T, typename Index>
enumerate(T&&, Index) -> enumerate<T&&>;

Finally, I’d like to thank ToxicPie9, meooow and magnus.hegdahl for discussing certain contents of this post, and other members of the AC server for giving their inputs too.