Teach yourself C++ in 45 years

Best practice for programming and indeed life

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

The 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.


Built Fri Sep 23 12:17:08 UTC 2022

#include "gtest/gtest.h"
//
#include <algorithm>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <ranges>
#include <string>
#include <string_view>
TEST(cpp11, string_recipes) {
  /**
  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
  - starts_with
  - ends_with
  - split_string
  - contains
  */

  // Remove leading and trailing whitespace
  const auto trim_whitespace = [](const std::string path) {
    const size_t start = path.find_first_not_of(" ");
    const size_t end = path.find_last_not_of(" ");
    return path.empty() ? "" : path.substr(start, 1 + end - start);
  };

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

  // Remove non-printable characters
  const auto remove_control_characters = [](const std::string path) {
    std::string result;
    std::copy_if(path.cbegin(), path.cend(), back_inserter(result),
                 [](const auto &c) { return std::isprint(c); });
    return result;
  };

  EXPECT_EQ(remove_control_characters(""), "");
  EXPECT_EQ(remove_control_characters("hello"), "hello");
  EXPECT_EQ(remove_control_characters("hel  lo"), "hello");

  // Use the lower case
  const auto to_lowercase = [](std::string path) {
    std::transform(path.begin(), path.end(), path.begin(),
                   [](char &c) { return std::tolower(c); });
    return path;
  };

  EXPECT_EQ(to_lowercase(""), "");
  EXPECT_EQ(to_lowercase("hello"), "hello");
  EXPECT_EQ(to_lowercase("Hello"), "hello");
  EXPECT_EQ(to_lowercase(" HeLlO0   THERE"), " hello0   there");

  // Use the uppercase
  const auto to_uppercase = [](std::string path) {
    // If the first thing you do is make a copy of a parameter, then let the
    // compiler do it
    std::transform(path.begin(), path.end(), path.begin(),
                   [](char &c) { return std::toupper(c); });
    return path;
  };

  EXPECT_EQ(to_uppercase(""), "");
  EXPECT_EQ(to_uppercase("hello"), "HELLO");
  EXPECT_EQ(to_uppercase("Hello"), "HELLO");
  EXPECT_EQ(to_uppercase(" HeLlO0   "), " HELLO0    ");

  // Use Python style
  const auto to_snakecase = [](std::string path) {
    std::transform(path.begin(), path.end(), path.begin(),
                   [](char &c) { return c == ' ' ? '_' : c; });
    return path;
  };

  EXPECT_EQ(to_snakecase(""), "");
  EXPECT_EQ(to_snakecase("   "), "___");
  EXPECT_EQ(to_snakecase("hello there"), "hello_there");

  // Compound tests
  EXPECT_EQ(to_uppercase(to_snakecase("hello there")), "HELLO_THERE");
  EXPECT_EQ(to_uppercase(to_snakecase(trim_whitespace(" hello there "))),
            "HELLO_THERE");

  // You can also break down other bits of a path using C++17's filesystem::path
  const auto get_file_name = [](const std::string path) {
    return std::filesystem::path{path}.filename();
  };

  EXPECT_EQ(get_file_name(""), "");
  EXPECT_EQ(get_file_name("blah/file.jpg"), "file.jpg");
  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/"), "");

  const auto get_file_extension = [](const std::string path) {
    return std::filesystem::path{path}.extension();
  };

  EXPECT_EQ(get_file_extension(""), "");
  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/"), "");

  // Avoid clumsy escape characters with raw strings, very useful with regular
  // expressions
  const std::string 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"));

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

    for (const auto word : std::views::split(words, delim))
      vec.push_back(std::string{word.begin(), word.end()});

    return vec;
  };

  constexpr std::string_view words{
      "A SYSTEM IS NO BETTER THAN ITS SENSORY ORGANS"};
  EXPECT_EQ(split_string(words).size(), 9uz);

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

#include <algorithm>
#include <list>
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, 5);
}

#include <type_traits>
TEST(cpp11, const_everything) {
  /**
  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);
}

#include <type_traits>
TEST(cpp11, classes_and_type_traits) {
  /**
  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.
  */

  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 instance of 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 with "explicit"
    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.
    */

    void func3(
        /* we are not allowed to change the signature because of override */)
        override final{};

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

  class base2 {
    // This doesn't have a virtual destructor
  };

  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.
  */

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

  EXPECT_EQ(x1, x2);

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

  EXPECT_EQ(finger, "Don't");

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

  /**
  Quite an interesting proposition: if you declare everything auto then you
  cannot leave anything uninitialised.
  */
  [[maybe_unused]] const auto d{3uz};

  // Some other uses of auto... what on earth?
  // auto
  // const auto &
  // auto &
  // auto &&
  // decltype(auto)
}

#include <algorithm>
#include <string>
#include <vector>
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
  const auto sentence = [] { return "I am a first-class citizen"; };
  const std::string s1 = sentence();
  EXPECT_EQ(s1.front(), 'I');

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

  // And an in-place definition
  std::vector<float> d{0.0, 0.1, 0.2};
  std::for_each(d.begin(), d.end(), [](auto &i) { ++i; });

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

#include <future>
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 complicated = []() -> int { return 1; };
  auto f = std::async(std::launch::async, complicated);

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

  EXPECT_EQ(f1, 1);
}

#include <exception>
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());
}

#include <utility>
#include <vector>
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);
}

#include <numeric>
#include <vector>
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);
}

#include <thread>
#include <vector>
TEST(cpp11, call_once) {
  /**
  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 int reg1{0x5692a5b6};
  const int reg2{0x5692'a5b6};
  EXPECT_EQ(reg1, reg2);

  const double reg3{1'000.000'01};
  EXPECT_EQ(reg3, 1000.00001);

  const uint32_t netmask{0b11111111'11111111'11111111'00000000};
  EXPECT_EQ(netmask, 0xffffff00);
}

#include <deque>
#include <optional>
TEST(cpp17, optional_types) {
  /**
  Optional types overcome the problem of defining a "not initialised" value
  -- say, -1 -- hich 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);
}

#include <any>
#include <numeric>
#include <vector>
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);
}

#include <algorithm>
#include <filesystem>
#include <vector>
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());
}

#include <execution>
#include <numeric>
#include <vector>
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);
}

#include <algorithm>
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]];
  }
}

#include <iomanip>
#include <string>
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());
}

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

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

  const auto sum = std::ranges::copy(vec1, begin(vec2));

  EXPECT_EQ(vec1, vec2);
}

#include <numeric>
#include <vector>
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/
  */

  // You can print containers easily with fmtlib
  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);
}

#include <string>
TEST(cpp20, deprecated_with_comment) {
  /**
  Mark things as deprecated before you remove them.
  */

  struct derived {
    std::string 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_;
    }
  };

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

#include <map>
#include <string>
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, 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 -lfmt -lgtest -lgtest_main

Compiler output

test.cxx: In member function 'virtual void cpp20_ranges_and_views_Test::TestBody()':
test.cxx:733:14: warning: variable 'sum' set but not used [-Wunused-but-set-variable]
  733 |   const auto sum = std::ranges::copy(vec1, begin(vec2));
      |              ^~~
test.cxx: In member function 'virtual void cpp20_deprecated_with_comment_Test::TestBody()':
test.cxx:778:12: warning: ignoring return value of 'auto cpp20_deprecated_with_comment_Test::TestBody()::derived::func1()', declared with attribute 'nodiscard': 'I have no side-effects' [-Wunused-result]
  778 |   a.func1(); // Throwing away the result of a const method
      |            ^
test.cxx:767:50: note: declared here
  767 |     [[nodiscard("I have no side-effects")]] auto func1() { return member_; }
      |                                                  ^~~~~
test.cxx:782:42: warning: 'auto cpp20_deprecated_with_comment_Test::TestBody()::derived::func2() const' is deprecated: This is neither efficient nor thread-safe [-Wdeprecated-declarations]
  782 |   [[maybe_unused]] const auto c = a.func2(); // this is deprecated
      |                                   ~~~~~~~^~
test.cxx:771:5: note: declared here
  771 |     func2() const {
      |     ^~~~~

Test output

Running main() from ./googletest/src/gtest_main.cc
[==========] Running 25 tests from 4 test suites.
[----------] Global test environment set-up.
[----------] 12 tests from cpp11
[ RUN      ] cpp11.string_recipes
[       OK ] cpp11.string_recipes (0 ms)
[ RUN      ] cpp11.range_based_for_loops
[       OK ] cpp11.range_based_for_loops (0 ms)
[ RUN      ] cpp11.const_everything
[       OK ] cpp11.const_everything (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.threading
[       OK ] cpp11.threading (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.call_once
[       OK ] cpp11.call_once (0 ms)
[----------] 12 tests from cpp11 (2 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)

[----------] 7 tests from cpp17
[ 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)
[----------] 7 tests from cpp17 (0 ms total)

[----------] 4 tests from cpp20
[ 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)
[----------] 4 tests from cpp20 (0 ms total)

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

CI info

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