Wed Nov 30 22:26:22 UTC 2022

Teach yourself C++ in 45 years

This isn’t an exhaustive C++ reference – see DevDocs – but an exploration of regularly-used features or those that have piqued my interest.

This code is mostly developed and formatted in Godbolt, which provides rapid feedback and a deeper insight into your code. To add a new example, I create a gtest to demonstrate it then simply add it to test.cxx. On commit, the examples are compiled and run in a GitLab CI pipe and the C++ and test results are deployed as this HTML page.

See the build pipeline and Doxygen documentation for this repo.


#include "gtest/gtest.h"
#include <algorithm>
#include <any>
#include <deque>
#include <exception>
#include <execution>
#include <filesystem>
#include <future>
#include <iomanip>
#include <iostream>
#include <list>
#include <numeric>
#include <optional>
#include <ranges>
#include <string>
#include <string_view>
#include <thread>
#include <type_traits>
#include <utility>
#include <vector>

/**
Common string manipulations so you don't get tempted to use a third-party
library.

- trim_whitespace
- remove_control_characters
- to_lowercase
- to_uppercase
- to_snakecase
- get_file_name
- get_file_extension
- get_stem (the file name without the extension)
- starts_with
- ends_with
- split_string
- contains
*/

/// Remove leading and trailing whitespace
std::string_view trim_whitespace(const std::string_view path) {
  const size_t start = path.find_first_not_of(" ");
  const size_t end = path.find_last_not_of(" ");
  const size_t diff = end - start;
  return diff > 0uz ? path.substr(start, diff + 1) : "";
}

TEST(cpp11, trim_whitespace) {
  EXPECT_EQ(trim_whitespace(""), "");
  EXPECT_EQ(trim_whitespace(" "), "");
  EXPECT_EQ(trim_whitespace("                 "), "");
  EXPECT_EQ(trim_whitespace("     file.jpg    "), "file.jpg");
  EXPECT_EQ(trim_whitespace("file.jpg    "), "file.jpg");
  EXPECT_EQ(trim_whitespace("     file.jpg"), "file.jpg");
  EXPECT_EQ(trim_whitespace("     one two    "), "one two");
  EXPECT_EQ(trim_whitespace(std::string{"     one two    "}), "one two");
  EXPECT_EQ(trim_whitespace(std::string_view{"     one two    "}), "one two");
}

/// Remove non-printable characters
std::string remove_control_characters(const std::string_view str) {
  // Filter function
  const auto func = [](const char c) { return std::isprint(c); };

  // Filter input string
  auto printable = str | std::views::filter(func);

  return {printable.begin(), printable.end()};
}

TEST(cpp20, remove_control_characters) {
  EXPECT_EQ(remove_control_characters(""), "");
  EXPECT_EQ(remove_control_characters("    "), "    ");
  EXPECT_EQ(remove_control_characters("hello"), "hello");
  EXPECT_EQ(remove_control_characters("hel  lo"), "hello");
  EXPECT_EQ(remove_control_characters("hel  lo"), "hello");
  EXPECT_EQ(remove_control_characters(std::string{"hel  lo"}), "hello");
  EXPECT_EQ(remove_control_characters(std::string_view{"hel lo"}), "hello");
  EXPECT_EQ(remove_control_characters("8=FIX.4.49=14835=D"),
            "8=FIX.4.49=14835=D");
}

/// Helper routine to transform a string with a function
constexpr std::string transform_view(const std::string str, const auto func) {
  const auto transformed = str | std::views::transform(func);
  return {transformed.begin(), transformed.end()};
}

/// Toggle case
constexpr char toggle_case(const char c, const bool to_upper) {

  constexpr std::string_view the_lower_case = "abcdefghijklmnopqrstuvwxyz";
  constexpr std::string_view the_upper_case = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  assert(std::size(the_lower_case) == std::size(the_upper_case));

  for (auto i = 0uz; i < std::size(the_upper_case); ++i) {
    if (to_upper) {
      if (c == the_lower_case[i])
        return the_upper_case[i];
    } else {
      if (c == the_upper_case[i])
        return the_lower_case[i];
    }
  }

  return c;
}

/// constexpr version of std::toupper
constexpr char toupper(const char c) {
  return toggle_case(c, true);
}

/// constexpr version of std::tolower
constexpr char tolower(const char c) {
  return toggle_case(c, false);
}

TEST(cpp11, case_tests_common) {
  constexpr auto do_nothing = [](const char c) { return c; };
  static_assert(transform_view("", do_nothing) == "");
  static_assert(transform_view("hello", do_nothing) == "hello");
  static_assert(transform_view(std::string{"hello"}, do_nothing) == "hello");

  static_assert(tolower('a') == 'a');
  static_assert(tolower('C') == 'c');
  static_assert(tolower('Z') == 'z');
  static_assert(tolower('0') == '0');

  static_assert(toupper('a') == 'A');
  static_assert(toupper('C') == 'C');
  static_assert(toupper('z') == 'Z');
  static_assert(toupper('0') == '0');

  static_assert(toggle_case('a', true) == 'A');
  static_assert(toggle_case('C', true) == 'C');
  static_assert(toggle_case('z', true) == 'Z');
  static_assert(toggle_case('0', true) == '0');

  static_assert(toggle_case('a', false) == 'a');
  static_assert(toggle_case('C', false) == 'c');
  static_assert(toggle_case('z', false) == 'z');
  static_assert(toggle_case('0', false) == '0');
}

/// Use the lower case
constexpr std::string to_lowercase(const std::string str) {
  return transform_view(str, [](const char c) { return tolower(c); });
}

/// Use the upper case
constexpr std::string to_uppercase(const std::string str) {
  return transform_view(str, [](const char c) { return toupper(c); });
}

/// Use the Python case
constexpr std::string to_snakecase(const std::string str) {
  constexpr auto func = [](const char c) { return c == ' ' ? '_' : c; };
  return transform_view(str, func);
}

TEST(cpp11, case_tests1) {
  static_assert(to_lowercase("") == "");
  static_assert(to_lowercase("hello") == "hello");
  static_assert(to_lowercase("Hello") == "hello");
  static_assert(to_lowercase(" HeLlO0   THERE") == " hello0 there");
  static_assert(to_lowercase(std::string{" HeLlO0   THERE"}) ==
                " hello0    there");

  static_assert(to_uppercase("") == "");
  static_assert(to_uppercase("hello") == "HELLO");
  static_assert(to_uppercase("Hello") == "HELLO");
  static_assert(to_uppercase(" HeLlO0   THERE") == " HELLO0 THERE");
  static_assert(to_uppercase(std::string{" HeLlO0   THERE"}) ==
                " HELLO0    THERE");
}

TEST(cpp11, case_tests2) {
  static_assert(to_snakecase("") == "");
  static_assert(to_snakecase("   ") == "___");
  static_assert(to_snakecase("hello there") == "hello_there");
  static_assert(to_snakecase(std::string{"hello there"}) == "hello_there");
}

/// You can aslo break down a path using C++17's std::filesystem
std::string get_file_name(const std::string_view path) {
  return std::filesystem::path{path}.filename();
};

/// Note string_view makes the data immutable, but prefixing const means you
/// also can't reassign path
std::string get_file_extension(const std::string_view path) {
  return std::filesystem::path{path}.extension();
};

std::string get_stem(const std::string_view path) {
  return std::filesystem::path{path}.stem();
};

TEST(cpp17, filename_operations) {
  EXPECT_EQ(get_file_name(""), "");
  EXPECT_EQ(get_file_name("     "), "     ");
  EXPECT_EQ(get_file_name("a b c"), "a b c");
  EXPECT_EQ(get_file_name("/etc/hello/file.jpg"), "file.jpg");
  EXPECT_EQ(get_file_name("file.jpg"), "file.jpg");
  EXPECT_EQ(get_file_name("file.jpg/"), "");
  EXPECT_EQ(get_file_name("blah/file.jpg"), "file.jpg");
  EXPECT_EQ(get_file_name(std::string{"blah/file.jpg"}), "file.jpg");
  EXPECT_EQ(get_file_name(std::string_view{"blah/file.jpg"}), "file.jpg");

  EXPECT_EQ(get_file_extension(""), "");
  EXPECT_EQ(get_file_extension("     "), "");
  EXPECT_EQ(get_file_extension("     .wav"), ".wav");
  EXPECT_EQ(get_file_extension("blah/file.jpg"), ".jpg");
  EXPECT_EQ(get_file_extension("/etc/hello/file.SVG"), ".SVG");
  EXPECT_EQ(get_file_extension("file.123"), ".123");
  EXPECT_EQ(get_file_extension("file.jpg/"), "");
  EXPECT_EQ(get_file_extension(std::string{"file.jpg/"}), "");
  EXPECT_EQ(get_file_extension(std::string_view{"file.jpg/"}), "");

  EXPECT_EQ(get_stem(""), "");
  EXPECT_EQ(get_stem("     "), "     ");
  EXPECT_EQ(get_stem("blah/file.jpg"), "file");
  EXPECT_EQ(get_stem("/etc/hello/file.SVG"), "file");
  EXPECT_EQ(get_stem("yeah.jpg/"), "");
  EXPECT_EQ(get_stem("blah.123"), "blah");
  EXPECT_EQ(get_stem(std::string{"blah.123"}), "blah");
  EXPECT_EQ(get_stem(std::string_view{"blah.123"}), "blah");
}

/// Something that has eluded computer science for decades, and now C++20 has
/// solved it
std::vector<std::string> split_string(std::string_view sentence) {
  constexpr std::string_view delim{" "};
  std::vector<std::string> words;

  std::ranges::transform(std::views::split(sentence, delim),
                         std::back_inserter(words), [](const auto word) {
                           return std::string{word.begin(), word.end()};
                         });

  return words;
}

TEST(cpp20, split_string) {
  constexpr std::string_view sentence{
      "A SYSTEM IS NO BETTER THAN ITS SENSORY ORGANS"};
  const auto words = split_string(sentence);
  EXPECT_EQ(std::size(words), 9uz);
  EXPECT_TRUE(split_string("").empty());
  EXPECT_TRUE(split_string(std::string{}).empty());
  EXPECT_FALSE(split_string(std::string_view{"1 2"}).empty());
}

TEST(cpp20, check_substrings) {
  // Avoid clumsy escape characters with raw strings, very useful with regular
  // expressions
  constexpr std::string_view quotation{
      R"(It's a little like wrestling a gorilla:
you don't quit when you're tired, you quit when the gorilla is tired
- Robert Strauss)"};

  // C++20
  EXPECT_TRUE(quotation.starts_with("It's"));
  EXPECT_TRUE(quotation.ends_with("Strauss"));

  // contains() for strings - C++23
  EXPECT_TRUE(quotation.contains("gorilla"));
  EXPECT_FALSE(quotation.contains("mandrill"));
}

TEST(cpp11, range_based_for_loops) {
  /**
  I really find it painful to go back to old style for-loops. All those
  clumsy explicit iterator declarations can be cleaned up beautifully with
  `auto`; in fact, we can drop the iterators altogether and avoid that weird
  *i dereferencing idiom. Note you don't have access to the current index
  (until C++2a), which isn't necessarily a bad thing if you have aspirations
  of parallelisation.
  */

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

  // Eek!
  for (std::list<int>::iterator i = v1.begin(); i != v1.end(); ++i)
    ++*i;

  EXPECT_EQ(v1.front(), 2);

  // Better...
  for (auto i = v1.begin(); i != v1.end(); ++i)
    ++*i;

  EXPECT_EQ(v1.front(), 3);

  // Now we're talking!
  for (auto &i : v1)
    ++i;

  EXPECT_EQ(v1.front(), 4);

  // Wait...
  const auto increment = [](auto &i) { ++i; };
  std::for_each(v1.begin(), v1.end(), increment);
  EXPECT_EQ(v1.front(), 5);

  // OMG - C++20
  std::ranges::for_each(v1, increment);
  EXPECT_EQ(v1.front(), 6);

  /**
  You can also pass a function object where you would a lambda to
  std::for_each, and do more complex things like store state between
  element But this does, of course, make parallelising the operation more
  complicated.
  */
  struct count_positive_elements {
    void operator()(const int i) {
      if (i > 0)
        ++count_;
    }

    size_t count_;
  };

  // Run the function object over the container
  const auto function_object =
      std::for_each(cbegin(v1), cend(v1), count_positive_elements{});

  // Get the result
  const auto count = function_object.count_;
  EXPECT_EQ(count, 5uz);
}

class base {
  /**
  Initialise class members in the header, I prefer the trailing underscore
  rather than "m_member".
  */

  // Note the implicit private scope for a class
  // private:
  int member_ = 0;

public:
  // Pure virtual: you cannot create an instance of the base class
  virtual void func3() = 0;

  /**
  Rule of 5: If a class requires a user-defined destructor, a user-defined
  copy constructor, or a user-defined copy assignment operator, it almost
  certainly requires all five.
  */

  // Require one constructor is used via "explicit", no others allowed
  explicit base() = default;
  base(const base &) = delete;
  base(base &&) noexcept = delete;
  base &operator=(const base &) = delete;
  base &operator=(base &&) noexcept = delete;

  // User defined destructors are noexcept by default
  virtual ~base() = default;
};

class derived : public base {
  /**
  Use virtual at the top level and then override in derived classes
  It stops you accidentally changing the signature or somebody else
  removing the base method. Mark methods as final once you've fixed all
  the bugs.
  */

  // We cannot change the signature because of override
  void func3() override final{};

  // Create type-safe typedefs with "using"
  using parent = base;
  void func4() { parent::func3(); }
};

class base2 {
  // Oops, we've forgotten to add a virtual destructor, see static_assert below
};

TEST(cpp11, classes_and_type_traits) {
  /**
  Not a modern feature, of course, but you should: *make everything constant*.
  You ought to 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.
  */

  struct S {
    size_t a{0};
    size_t b{0};
    size_t c{0};
  };

  const S s1;
  S s2;

  EXPECT_TRUE(std::is_const_v<const int>);
  EXPECT_FALSE(std::is_const_v<int>);
  EXPECT_TRUE(std::is_const<decltype(s1)>::value);
  EXPECT_FALSE(std::is_const<decltype(s2)>::value);

  /**
  Knowning that classes sometimes need a virtual destructor always
  seemed a bit nuanced and error prone. Now you can test it.

  Not my rules but a common opinion: structs hold data; but if there's any
  funtionality, then it should probably be a class.
  */
  EXPECT_TRUE(std::has_virtual_destructor<base>::value);
  EXPECT_FALSE(std::has_virtual_destructor<base2>::value);
}

TEST(cpp11, auto_type) {
  /**
  Type inference is a game changer. You can simplify complicated
  (or unknown) type declarations with auto, but it can be a balance of
  convenience over readability.
  */

  // And there are a few 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;

  EXPECT_EQ(y1, 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 z1 = 1;
  int &z2 = z1;
  auto z3 = z2;
  auto &z4 = z2;
  --z3;
  ++z4;

  // These assertions took ages to get right... don't write confusing code!
  EXPECT_EQ(z1, 2);
  EXPECT_EQ(z2, 2);
  EXPECT_EQ(z3, 0);
  EXPECT_EQ(z4, 2);

  // What's the underlying type of x2? int
  int x1 = 5;
  auto x2 = x1;
  EXPECT_EQ(x1, x2);
  static_assert(std::is_same_v<decltype(x2), int>);

  // What's the underlying type? Still int (const ignored)
  const int x3 = 5;
  auto x4 = x3;
  static_assert(std::is_same_v<decltype(x4), int>);

  // What's the underlying type? _Now_ it's const int
  decltype(auto) x6 = x3;
  static_assert(std::is_same_v<decltype(x6), const int>);

  // Here I don't know (or care) what the type is
  const std::vector moon{"Don't", "look", "at", "the", "finger"};
  const auto finger = moon.front();

  EXPECT_EQ(finger, "Don't");

  // Something to ponder: if you declare everything auto then you cannot leave
  // anything uninitialised.
  [[maybe_unused]] const auto d = 3uz;
}

TEST(cpp11, lambda_expressions) {
  /**
  Lambda expressions are like function pointers but with a much friendlier
  implementation. Call them 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.
  */

  // Let's create a lambda expression and call it
  constexpr auto sentence = [] { return "I am a first-class citizen"; };
  std::string_view s1 = sentence();
  EXPECT_EQ(s1.front(), 'I');

  // You can even call them directly, note you don't need the brackets here
  constexpr std::string_view s2 = [] {
    return "I am a second-class citizen";
  }();
  EXPECT_EQ(s2.back(), 'n');

  // And an in-place definition
  std::vector d{0.0, 0.1, 0.2};
  std::ranges::for_each(d, [](auto &i) { ++i; });

  EXPECT_EQ(d.front(), 1.0);
}

TEST(cpp11, exceptions) {
  /**
  A key feature of the language but I eschew adding exceptions to my own
  code; it's much easier to reason about errors where they occur.
  */
  const auto throw_it = []() { throw "cya!"; };

  EXPECT_ANY_THROW(throw_it());
}

TEST(cpp11, brace_initialisers) {
  /**
  There are many more ways to initialise and append to a container.
  */

  using container_t = std::vector<std::pair<int, int>>;

  container_t c{
      {1.1, 1},
      {2, 2},
  };

  c.front() = std::make_pair(1.3, 2);

  c.push_back({1.1, 1.2});
  c.emplace_back(1.1, 1.2);
  c.emplace_back(std::make_pair(1.1, 1.3));

  EXPECT_EQ(c.size(), 5);

  // Initialise more complex types
  const struct S2 {
    int x;

    struct {
      int u;
      int v;
      std::vector<int> a;
    } b;

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

  EXPECT_EQ(s1.x, 1);
  EXPECT_EQ(s1.b.a.at(0), 4);

  /**
  In C++17 the type of vector can be inferred from the init list.
  */
  const std::vector v1{1, 2, 3, 4, 5};
  EXPECT_TRUE(not v1.empty());

  const std::vector v2{'a', 'b', 'c', 'd', 'e'};
  EXPECT_TRUE(not v2.empty());
}

TEST(cpp11, narrowing) {
  /**
  Initialising a small type with a large type will generate an error if you
  use braces. You don't seem to be able to downgrade this a warning.
  */

  // Braces with the same size type
  const int d{1};
  EXPECT_EQ(d, 1);

  // Brackets hiding the narrowing
  const int e(1.1);
  EXPECT_EQ(e, 1);

  // This would throw an error
  // const int f{1.0};
  // EXPECT_EQ(f, 1);
}

TEST(cpp11, learn_the_standard_library) {
  /**
  The standard library often expresses intention much more eloquently than a
  regular for-loop. Note the namespace is omitted for the `count_if`
  parameters, see Koenig/argument-dependent lookup (ADL).
  */

  const std::vector vec{1, 2, 3, 4, 5, 6};
  const auto count = std::count_if(cbegin(vec), cend(vec),
                                   [](const auto &a) { return a < 3; });

  EXPECT_EQ(count, 2);
}

TEST(cpp11, threading) {
  /**
  Threading gives you the promise of speed at the expense of
  reasoning, complexity and debugging. But they are much more intuitive
  now than the old POSIX library.

  std::futures are particularly interesting and let you return the
  stuff you're interested in much more easily: define a routine as a lambda
  and run it in the background while the main routine gets on with something
  else; and when we're ready, we block to get the value.
  */

  // Spawn a task in the background, note we've declared a return type with ->
  const auto background_task = []() -> int { return 1; };
  auto f = std::async(std::launch::async, background_task);

  // Do things and then block
  const auto f1 = f.get();

  EXPECT_EQ(f1, 1);

  /**
  Listen very carefully, I shall say this only once... if you get this reference
  then you are a senior dev. You can call things only once using statics and
  lambdas (IIFE); or, use the well-named Standard Library function.
  */

  auto i = 0uz;
  std::once_flag flag;

  std::vector<std::thread> threads;
  for ([[maybe_unused]] const auto _ : {0, 1, 2, 3, 4}) {
    threads.emplace_back(
        std::thread([&]() { std::call_once(flag, [&i]() { ++i; }); }));
  }

  // Auto-joining jthreads are C++20
  for (auto &t : threads)
    t.join();

  EXPECT_EQ(i, 1);
}

size_t bird_count = 0;
TEST(cpp14, return_value_optimisation) {
  /**
  The destructor is called only once despite the nested constructors: see copy
  elision.
  */

  struct bird {
    ~bird() { ++bird_count; }
  };

  // Check count whilst b is still in scope
  {
    bird b = bird(bird(bird(bird(bird()))));

    EXPECT_EQ(bird_count, 0);
  }

  // Check again when it's gone out of scope and the destructor is called
  EXPECT_EQ(bird_count, 1);
}

TEST(cpp14, 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. You can even define things in binary if you like.
  */

  const auto reg1 = 0x5692a5b6;
  const auto reg2 = 0x5692'a5b6;
  EXPECT_EQ(reg1, reg2);

  const auto reg3 = 1'000.000'01;
  EXPECT_EQ(reg3, 1000.00001);

  // Using brace init to check for narrowing
  const uint32_t netmask{0b11111111'11111111'11111111'00000000};
  EXPECT_EQ(netmask, 0xffffff00);
}

TEST(cpp17, optional_types) {
  /**
  Optional types overcome the problem of defining a "not initialised" value
  -- say, -1 -- which will inevitably used to index an array and cause an
  explosion. Your functions can now effectively return a "no result".
  */

  //  Let's initialise a container with these data
  std::deque<std::optional<int>> 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
  // Note the const iterators
  const auto c = std::count_if(cbegin(options), cend(options),
                               [](const auto &o) { return o; });
  EXPECT_EQ(c, 4);
}

TEST(cpp17, heterogeneous_types) {
  /**
  std::tuple is like a pair but better, and offers arbitrary collections of
  heterogeneous types. You can retrieve values by index (which looks a bit
  odd) or even by type! I think it makes for quite strange code but you can
  hide much of it with auto.
  */

  const auto t = std::make_tuple("one", 2.0, 3);

  EXPECT_EQ(std::get<0>(t), "one");

  /**
  `std::any` is a little better thought out.
  */
  std::vector<std::any> martini(10);

  martini.at(5) = 1.0;
  martini.at(6) = "time";
  martini.at(7) = "place";
  martini.at(8) = "where";

  EXPECT_EQ(martini.size(), 10);
}

TEST(cpp17, filesystem) {
  /**
  C++17 has a perfectly good interface to the filesystem so you don't need to
  use something like Qt's `QFile`.
  */

  std::vector<std::string> files;
  const std::filesystem::path p{"."};
  std::ranges::for_each(
      std::filesystem::directory_iterator{p},
      [&files](const auto &file) { files.push_back(file.path()); });

  EXPECT_TRUE(not files.empty());
}

TEST(cpp17, parallel_execution_policy) {
  /**
  Quite an exciting prospect in C++17 is parallelising _existing_ for-loops
  simply by adding an execution policy parameter.
  */

  std::vector vec{1, 23, 4, 5, 6, 7, 8};
  const auto sum =
      std::reduce(std::execution::seq, vec.cbegin(), vec.cend(), int{});

  EXPECT_EQ(sum, 54);

  std::for_each(std::execution::unseq, vec.begin(), vec.end(),
                [](auto &x) { ++x; });

  EXPECT_EQ(vec.front(), 2);
}

TEST(cpp17, clamp_values) {
  /**
  This would've been really useful in a previous life but I've yet to use of
  it since! Documented as a reminder.
  */

  // uz is C++23
  const auto a{19uz};

  // Using decltype to refer to the type of another thing
  const decltype(a) b = std::clamp(a, 0uz, 16uz);
  const auto c = std::clamp(0, 4, 10);

  EXPECT_EQ(b, 16uz);
  EXPECT_EQ(c, 4);
}

TEST(cpp17, compiler_attributes) {
  /**
  Tell the compiler you meant to follow through.
  */

  const size_t in{0};
  size_t out{0};

  switch (in) {
  case 0:
    // Do the same thing as below
    [[fallthrough]];

  case 1:
    // Do a thing
    ++out;
    break;

  default:
    break;
  }

  EXPECT_EQ(out, 1);

  // Guide the compiler as to well-trodden path
  if (true) {
    [[likely]];
  } else {
    [[unlikely]];
  }
}

TEST(cpp17, maybe_unused_attribute) {
  /** If a variable is not used in all cases, mark with an attribute. Often
  happens where things aren't compiled in release.
  */

  [[maybe_unused]] std::string str;

#ifdef THIS_IS_NOT_DEFINED_IN_THIS_BUILD_BUT_I_DO_NOT_WANT_WARNING
  str = "special";
#endif

  // Use empty rather than size
  EXPECT_TRUE(str.empty());
}

TEST(cpp20, ranges_and_views) {
  /**
  An opportunity to simplify all the begin/end code that's been written since
  C++11.
  */

  const std::vector vec1{1, 2, 3, 4, 5, 6, 7, 8};
  std::vector<int> vec2(vec1.size());

  std::ranges::copy(vec1, begin(vec2));

  EXPECT_EQ(vec1, vec2);
}

TEST(cpp20, reduce_and_accumulate) {
  /**
  There's no need to specify a starting value with `reduce`, but there are
  some important considerations.

  https://blog.tartanllama.xyz/accumulate-vs-reduce/
  */

  const std::vector vec{1, 2, 3, 4, 5, 66};

  // Note the two ways to specify start and end iterators
  const auto sum1 = std::accumulate(vec.cbegin(), vec.cend(), 0);
  const auto sum2 = std::reduce(cbegin(vec), cend(vec));

  EXPECT_EQ(sum1, sum2);
}

TEST(cpp20, deprecated_with_comment) {
  /**
  Mark things as deprecated before you remove them.
  */

  struct A {
    std::string_view member_{"cya"};

    // Do not throw the result of the away
    [[nodiscard("I have no side-effects")]] auto func1() { return member_; }

    // Do not use this at all
    [[deprecated("This is neither efficient nor thread-safe")]] auto
    func2() const {
      return member_;
    }
  } a;

  [[maybe_unused]] const auto b = a.func1();
  a.func1(); // Throwing away the result of a const method

  EXPECT_FALSE(b.empty());

  [[maybe_unused]] const auto c = a.func2(); // this is deprecated
}

TEST(cpp20, contains_for_associative_containers) {
  /**
  Check a key exists without find or count. Feels much more natural than
  checking the end stop.
  */

  const std::map<std::string_view, int> m{
      {"one", 1},
      {"two", 2},
      {"three", 3},
  };

  EXPECT_TRUE(m.contains("one"));
  EXPECT_FALSE(m.contains("four"));
}

Compiler flags

–std=c++23 –all-warnings –extra-warnings -Wshadow -Wfloat-equal -Weffc++ -Wdelete-non-virtual-dtor -Warray-bounds -Wdeprecated-copy -Wimplicit-fallthrough -Og -lgtest -lgtest_main

Compiler output

test.cxx: In member function 'virtual void cpp20_deprecated_with_comment_Test::TestBody()':
test.cxx:866:10: warning: ignoring return value of 'auto cpp20_deprecated_with_comment_Test::TestBody()::A::func1()', declared with attribute 'nodiscard': 'I have no side-effects' [-Wunused-result]
  866 |   a.func1(); // Throwing away the result of a const method
      |   ~~~~~~~^~
test.cxx:856:50: note: declared here
  856 |     [[nodiscard("I have no side-effects")]] auto func1() { return member_; }
      |                                                  ^~~~~
test.cxx:870:42: warning: 'auto cpp20_deprecated_with_comment_Test::TestBody()::A::func2() const' is deprecated: This is neither efficient nor thread-safe [-Wdeprecated-declarations]
  870 |   [[maybe_unused]] const auto c = a.func2(); // this is deprecated
      |                                   ~~~~~~~^~
test.cxx:860:5: note: declared here
  860 |     func2() const {
      |     ^~~~~

Test output

Running main() from ./googletest/src/gtest_main.cc
[==========] Running 30 tests from 4 test suites.
[----------] Global test environment set-up.
[----------] 13 tests from cpp11
[ RUN      ] cpp11.trim_whitespace
[       OK ] cpp11.trim_whitespace (0 ms)
[ RUN      ] cpp11.case_tests_common
[       OK ] cpp11.case_tests_common (0 ms)
[ RUN      ] cpp11.case_tests1
[       OK ] cpp11.case_tests1 (0 ms)
[ RUN      ] cpp11.case_tests2
[       OK ] cpp11.case_tests2 (0 ms)
[ RUN      ] cpp11.range_based_for_loops
[       OK ] cpp11.range_based_for_loops (0 ms)
[ RUN      ] cpp11.classes_and_type_traits
[       OK ] cpp11.classes_and_type_traits (0 ms)
[ RUN      ] cpp11.auto_type
[       OK ] cpp11.auto_type (0 ms)
[ RUN      ] cpp11.lambda_expressions
[       OK ] cpp11.lambda_expressions (0 ms)
[ RUN      ] cpp11.exceptions
[       OK ] cpp11.exceptions (0 ms)
[ RUN      ] cpp11.brace_initialisers
[       OK ] cpp11.brace_initialisers (0 ms)
[ RUN      ] cpp11.narrowing
[       OK ] cpp11.narrowing (0 ms)
[ RUN      ] cpp11.learn_the_standard_library
[       OK ] cpp11.learn_the_standard_library (0 ms)
[ RUN      ] cpp11.threading
[       OK ] cpp11.threading (0 ms)
[----------] 13 tests from cpp11 (4 ms total)

[----------] 7 tests from cpp20
[ RUN      ] cpp20.remove_control_characters
[       OK ] cpp20.remove_control_characters (0 ms)
[ RUN      ] cpp20.split_string
[       OK ] cpp20.split_string (0 ms)
[ RUN      ] cpp20.check_substrings
[       OK ] cpp20.check_substrings (0 ms)
[ RUN      ] cpp20.ranges_and_views
[       OK ] cpp20.ranges_and_views (0 ms)
[ RUN      ] cpp20.reduce_and_accumulate
[       OK ] cpp20.reduce_and_accumulate (0 ms)
[ RUN      ] cpp20.deprecated_with_comment
[       OK ] cpp20.deprecated_with_comment (0 ms)
[ RUN      ] cpp20.contains_for_associative_containers
[       OK ] cpp20.contains_for_associative_containers (0 ms)
[----------] 7 tests from cpp20 (0 ms total)

[----------] 8 tests from cpp17
[ RUN      ] cpp17.filename_operations
[       OK ] cpp17.filename_operations (0 ms)
[ RUN      ] cpp17.optional_types
[       OK ] cpp17.optional_types (0 ms)
[ RUN      ] cpp17.heterogeneous_types
[       OK ] cpp17.heterogeneous_types (0 ms)
[ RUN      ] cpp17.filesystem
[       OK ] cpp17.filesystem (0 ms)
[ RUN      ] cpp17.parallel_execution_policy
[       OK ] cpp17.parallel_execution_policy (0 ms)
[ RUN      ] cpp17.clamp_values
[       OK ] cpp17.clamp_values (0 ms)
[ RUN      ] cpp17.compiler_attributes
[       OK ] cpp17.compiler_attributes (0 ms)
[ RUN      ] cpp17.maybe_unused_attribute
[       OK ] cpp17.maybe_unused_attribute (0 ms)
[----------] 8 tests from cpp17 (0 ms total)

[----------] 2 tests from cpp14
[ RUN      ] cpp14.return_value_optimisation
[       OK ] cpp14.return_value_optimisation (0 ms)
[ RUN      ] cpp14.digit_separators
[       OK ] cpp14.digit_separators (0 ms)
[----------] 2 tests from cpp14 (0 ms total)

[----------] Global test environment tear-down
[==========] 30 tests from 4 test suites ran. (5 ms total)
[  PASSED  ] 30 tests.

CI info

PRETTY_NAME="Ubuntu 22.10"
NAME="Ubuntu"
VERSION_ID="22.10"
VERSION="22.10 (Kinetic Kudu)"