Modules
Brief overview
Constraints and Concepts
Brief overview
Coroutines
Using stackless coroutines offers great performance, but they are quite complex to understand.
Three-way Comparison Operator <=>
<span>auto operator<=>(const C&)const=default;</span> will automatically generate comparison operators within class C, including<span>==, !=, <, <=, >, and >=</span>
If you need to customize<span>operator<=></span>, you must also implement<span>operator==</span>
The return value of the three-way comparison operator is of type std::strong_ordering, std::weak_ordering, or std::partial_ordering, which can only be compared with the integer literal 0.
struct Point
{
int x;
int y;
// Based on the distance to the origin (0,0)
std::partial_ordering operator<=>(const Point &other) const
{
auto distThis = std::hypot(x, y);
auto distOther = std::hypot(other.x, other.y);
// Floating-point <=> returns std::partial_ordering
return distThis <=> distOther;
}
bool operator==(const Point &other) const
{
auto distThis = std::hypot(x, y);
auto distOther = std::hypot(other.x, other.y);
return distThis == distOther;
}
};
int main()
{
Point p1{1, 2}, p2{1, 3}, p3{2, 1};
if (p1 < p2)
{
cout << "p1 < p2" << endl;
}
if (p1 == p3)
{
cout << "p1 == p3" << endl;
}
if (p2 != p3)
{
cout << "p2 != p3" << endl;
}
}
Abbreviated Function Templates
Abbreviated function templates refer to using auto or “constrained auto (i.e., concept auto)” in the function parameter list as a type placeholder, allowing for a more concise declaration of function templates; the compiler will automatically generate a fictional template parameter for each placeholder. It does not introduce new semantics, just a more compact syntax. The syntax and equivalent forms are as follows:
Unconstrained placeholder: void f(auto x); → template <class T> void f(T x);
Constrained placeholder: void f(std::integral auto x); → template <std::integral T> void f(T x);
Parameter pack placeholder: void f(auto... xs); → template <class... Ts> void f(Ts... xs);
Multiple parameters independently: void f(auto a, auto b); → template <class T, class U> void f(T a, U b);
Combined form: template <class T, std::integral U> void g(T x, U y, std::integral auto z); → The auto for the third parameter introduces a new template parameter template <class T, std::integral U, std::integral W> void g(T x, U y, W z);
source_location
A modern alternative to some traditional preprocessor macros (such as<span>__FILE__, __LINE__, __FUNCTION__</span>):
void log(std::string_view msg, const std::source_location& loc = std::source_location::current())
{
std::cout << loc.file_name() << ": " << loc.line() << " [" << loc.function_name() << "] " << msg << '\n';
}
span
Similar to std::string_view for std::string, std::span is a non-owning view of a contiguous memory segment (such as arrays, std::vector, etc.): it does not manage memory, only provides access to and manipulation of existing elements.
void print(std::span<int> s)
{
for (int x : s)
std::cout << x << ' ';
std::cout << "\nsize = " << s.size() << '\n';
}
int main()
{
int arr[] = {1, 2, 3, 4, 5};
std::vector<int> vec = {6, 7, 8};
print(arr);
print(vec);
print(std::span{vec}.subspan(1, 2));
}
numbers
Provides some mathematical constants, such as std::numbers::pi, std::numbers::e, etc.
bit
Some operations related to bit manipulation
Enumeration for determining byte order:
enum class endian
{
little = /* implementation-defined */,
big = /* implementation-defined */,
native = /* implementation-defined */,
};
template< class To, class From >
constexpr To bit_cast(const From& from) noexcept;
bit_cast reinterprets the type of from as To without changing the bits; From and To must have the same size.
There are also other function templates, such as checking if a number is a power of 2, calculating the largest power of 2 less than or equal to n, and counting how many bits are set in an unsigned integer.
syncstream
Provides a thread-safe output stream tool to avoid interleaved output in a multithreaded environment.
#include <iostream>
#include <fstream>
#include <syncstream>
#include <thread>
#include <vector>
void print(int id)
{
std::osyncstream out(std::cout);
out << "Thread " << id << '\n';
out.emit(); // Transfers its internal data to the final target, also called automatically on destruction
}
void print2file(int id, std::ofstream &of)
{
std::osyncstream out(of); // Binds to the same file stream
out << "Thread " << id << '\n';
}
int main()
{
std::vector<std::thread> v;
for (int i = 0; i != 10; ++i)
v.emplace_back(print, i);
for (auto &t : v)
t.join();
std::ofstream file("out.txt");
v.clear();
for (int i = 0; i != 10; ++i)
v.emplace_back(print2file, i, std::ref(file));
for (auto &t : v)
t.join();
}
ranges
Ranges extend and unify the traditional STL iterator/algorithm model, providing a higher level of abstraction that makes sequence processing simpler, safer, and more composable. Ranges treat ranges as a first-class concept, combined with views and pipelines, achieving a functional programming style that significantly enhances the expressiveness and maintainability of complex data processing.
The ranges library includesrange algorithms[1] (these algorithms are immediately applied to ranges) andrange adaptors[2] (these adaptors are lazily applied to views). Adaptors can be composed into pipelines, and operations will be executed when iterating over views.
First, let’s look at an example of a range adaptor:
#include <iostream>
#include <ranges>
int main()
{
auto const ints = {0, 1, 2, 3, 4, 5};
auto even = [](int i){ return 0 == i % 2; };
auto square = [](int i){ return i * i; };
for (int i : ints | std::views::filter(even) | std::views::transform(square))
std::cout << i << ' ';
}
In<span>ints | std::views::filter(even)</span>, std::views::filter is a global variable<span>inline constexpr _Filter filter;</span>, which first calls _Filter::operator()() to generate a temporary variable, then calls operator|() for that temporary variable, which in turn calls std::views::filter(even)(ints), and this function will call std::views::filter(ints, even), returning a std::ranges::filter_view. The transform is similar, so the loop in the above example can also be written as:
for (int i : std::views::transform(square)(std::views::filter(even)(ints)))
std::cout << i << ' ';
for (int i : std::views::transform(std::views::filter(ints, even), square))
std::cout << i << ' ';
The intermediate process is complex, but the final effect is excellent.
Ultimately, it is equivalent to iterating over a transform_view, which contains a filter_view, which in turn contains a reference to ints. The filtering and transforming operations will only be executed during iteration, which is known as lazy evaluation.
Now let’s look at an example of a range algorithm:
#include <iostream>
#include <ranges>
int main()
{
std::map<int, int> m {{1, 2}, {3, 4}, {5, 6}};
auto view = m | std::views::values;
if(std::ranges::find(view, 4) != view.end())
{
std::cout << "Found value 4\n";
}
if(std::ranges::find(m, 6, [](const auto &value){return value.second;}) != m.end())
{
std::cout << "Found value 6\n";
}
}
format
Supported starting from gcc13, can be replaced by the fmt library, with similar usage.
The most commonly used function is:
template <class... Args>
std::string format(std::format_string<Args...> fmt, Args&&... args);
Equivalent to<span>return std::vformat(fmt.get(), std::make_format_args(args...));</span>
The parameter fmt represents the object of the formatting string. The formatting string consists of the following parts:
- • Ordinary characters (except { and }), which are copied to the output as is
- • Escape sequences {{ and }}, which are replaced by { and } in the output
- • Replacement fields
The format of replacement fields without formatting specifications is { arg-id (optional) }; the format of replacement fields with formatting specifications is { arg-id (optional) : format-spec }.
arg-id is the index of the parameter used for formatting in args; if omitted, parameters are used in order (all arg-ids in the formatting string must either exist or be omitted; mixing manual and automatic indexing is an error); format-spec is the format specification defined by the std::formatter specialization for the corresponding parameter, and cannot start with }.
The format specifications for basic types and standard string types are calledstandard format specifications[3], and in most cases, their syntax is similar to the old % formatting, adding {} and using : instead of %, for example, “%03.2f” can be translated to “{:03.2f}”.
Some other functions:
std::string vformat(std::string_view fmt, std::format_args args);
Formats the parameters stored in args according to the format string fmt and returns the result as a string.
std::format_to
std::format_to_n
std::vformat_to
The first parameter is an output iterator, which writes the formatted result to the iterator pointed to by the first parameter; std::format_to_n can also specify the maximum output length.
template <class... Args>
std::size_t formatted_size(std::format_string<Args...> fmt, Args&&... args);
The parameters are the same as std::format, returning the length of the formatted string, which can be used to obtain the size of the destination buffer in advance.
To enable custom classes to support std::format formatting, you need to specialize the std::formatter template for the class, which can inherit from existing formatters, for example:
struct Point
{
int x;
int y;
};
template <>
struct std::formatter<Point> : std::formatter<std::string>
{
auto format(const Point &p, std::format_context &ctx) const -> decltype(ctx.out())
{
std::string temp_str = std::format("({}, {})", p.x, p.y);
// Call the base class's format function to handle this temporary string, thus supporting existing format specifications (like width, alignment, etc.)
return std::formatter<std::string>::format(temp_str, ctx);
}
};
If you need to define specific format specifiers for custom classes, you need to fully implement the parse and format functions.
std::jthread
std::jthread is similar to std::thread but adds two capabilities: one is automatic joining upon destruction, and the other is support for responsive stopping. An example of automatic joining:
class baz
{
public:
void operator()()
{
for (int i = 0; i < 5; ++i)
{
std::cout << "Thread executing\n";
++n;
std::this_thread::sleep_for(100ms);
}
}
private:
int n = 0;
};
int main()
{
baz b;
std::jthread t(b); // runs baz::operator() on a copy of object b
// t joins on destruction
}
The simplest example of stopping a thread:
using namespace std::literals::chrono_literals;
void f(std::stop_token stop_token, int value)
{
while (!stop_token.stop_requested())
{
std::cout << value++ << ' ' << std::flush;
std::this_thread::sleep_for(200ms);
}
std::cout << std::endl;
}
int main()
{
std::jthread thread(f, 1);
std::this_thread::sleep_for(3s);
// The destructor of jthread calls request_stop() and join().
}
jthread internally has a corresponding stop_source and stop_token; if the callable object’s first parameter is a stop_token, the jthread will automatically pass the current jthread’s stop_token during initialization.
You can also use external stop_source and stop_token:
using namespace std::literals::chrono_literals;
void f(std::stop_token stop_token, int value)
{
while (!stop_token.stop_requested())
{
std::cout << value++ << ' ' << std::flush;
std::this_thread::sleep_for(200ms);
}
std::cout << std::endl;
}
int main()
{
std::stop_source stop_source;
std::jthread thread(f, stop_source.get_token(), 1);
std::this_thread::sleep_for(3s);
stop_source.request_stop();
}
std::jthread::get_stop_source() retrieves the stop_source associated with the current jthread, std::jthread::get_stop_token() retrieves the stop_token associated with the current jthread, and std::jthread::request_stop() calls request_stop() on the corresponding stop_source.
jthread automatically calls request_stop() and join() upon destruction.
Responsive means that the thread itself checks whether a stop has been requested. If the thread is waiting for an external stop request, it can use a loop + sleep method or one of the overloaded wait functions of condition_variable_any:
int main()
{
std::jthread sleepy_worker(
[](std::stop_token stoken)
{
while (!stoken.stop_requested())
{
std::this_thread::sleep_for(100ms);
}
std::cout << std::this_thread::get_id() << " sleepy_worker done!\n";
});
std::jthread waiting_worker(
[](std::stop_token stoken)
{
std::mutex mutex;
std::unique_lock lock(mutex);
std::condition_variable_any().wait(lock, stoken, []{ return false; });
std::cout << std::this_thread::get_id() << " waiting_worker done!\n";
});
std::this_thread::sleep_for(400ms);
sleepy_worker.request_stop();
sleepy_worker.join();
// waiting_worker's destructor will call request_stop() and join the thread automatically.
}
template <class Lock, class Predicate>
bool wait(Lock& lock, std::stop_token stoken, Predicate pred);
Equivalent to
while (!stoken.stop_requested())
{
if (pred())
return true;
wait(lock); // stop_token being request_stop will cause wait to return
}
return pred();
Thread Coordination Tools
std::latch is a one-time thread barrier that allows one or more threads to wait until the counter reaches zero.
struct Job
{
const std::string name;
std::string product{"not worked"};
std::thread action{};
};
using namespace std::literals::chrono_literals;
int main()
{
Job jobs[]{{"Annika"}, {"Buru"}, {"Chuck"}};
std::latch work_done{std::size(jobs)};
std::latch start_clean_up{1};
auto work = [&](Job &my_job)
{
std::this_thread::sleep_for(1s);
my_job.product = my_job.name + " worked";
work_done.count_down();
start_clean_up.wait();
my_job.product = my_job.name + " cleaned";
};
std::cout << "Work is starting... ";
for (auto &job : jobs)
job.action = std::thread{work, std::ref(job)};
work_done.wait();
std::cout << "done:\n";
for (auto const &job : jobs)
std::cout << " " << job.product << '\n';
std::this_thread::sleep_for(1s);
std::cout << "Workers are cleaning up... ";
start_clean_up.count_down();
for (auto &job : jobs)
job.action.join();
std::cout << "done:\n";
for (auto const &job : jobs)
std::cout << " " << job.product << '\n';
}
std::barrier is a reusable thread barrier.
std::counting_semaphore is a semaphore, and std::binary_semaphore is a specialized binary semaphore.
template <std::ptrdiff_t LeastMaxValue>
class counting_semaphore;
using binary_semaphore = std::counting_semaphore<1>;
The template parameter LeastMaxValue indicates the minimum guaranteed upper limit, while the actual limit may be larger.
string::starts_with/ends_with and string_view::starts_with/ends_with
#include <assert.h>
#include <string>
#include <string_view>
int main()
{
using namespace std::literals::string_literals;
using namespace std::literals::string_view_literals;
const auto str = "Hello, C++20!"s; // std::literals::string_literals::operator""s()
assert(str.starts_with("He"sv) // std::literals::string_view_literals::operator""sv()
&& str.starts_with("He"s) // implicit conversion string to string_view
&& str.starts_with('H') && str.starts_with("He");
}
Heterogeneous Lookup in Associative Containers
Directly look at the example:
struct DebugStringHash
{
using is_transparent = void;
std::size_t operator()(const std::string &s) const
{
std::cout << "Hash called with <std::string>: " << s << std::endl;
return std::hash<std::string>{}(s);
}
std::size_t operator()(std::string_view sv) const
{
std::cout << "Hash called with <std::string_view>: " << sv << std::endl;
return std::hash<std::string_view>{}(sv);
}
std::size_t operator()(const char *str) const
{
std::cout << "Hash called with <const char*>: " << str << std::endl;
return std::hash<std::string_view>{}(str);
}
};
struct DebugStringEqual
{
using is_transparent = void;
bool operator()(const std::string &a, const std::string &b) const
{
std::cout << "Equal called with <std::string, std::string>: "
<< a << " vs " << b << std::endl;
return a == b;
}
bool operator()(const char *a, const std::string &b) const
{
std::cout << "Equal called with <const char*, std::string>: "
<< a << " vs " << b << std::endl;
return a == b;
}
bool operator()(std::string_view a, const std::string &b) const
{
std::cout << "Equal called with <string_view, std::string>: "
<< a << " vs " << b << std::endl;
return a == b;
}
};
int main()
{
std::unordered_map<std::string, int, DebugStringHash, DebugStringEqual> m = { {"apple", 1}, {"banana", 2}, {"oriange", 3} };
std::cout << "\n=== Lookup for std::string type key ===" << std::endl;
std::string key1 = "apple";
auto it = m.find(key1);
if (it != m.end())
{
std::cout << "Found: " << it->second << std::endl;
}
std::cout << "\n=== Lookup for std::string_view type key ===" << std::endl;
std::string_view key2 = "banana";
it = m.find(key2);
if (it != m.end())
{
std::cout << "Found: " << it->second << std::endl;
}
std::cout << "\n=== Lookup for const char* type key ===" << std::endl;
const char *key = "oriange";
it = m.find(key);
if (it != m.end())
{
std::cout << "Found: " << it->second << std::endl;
}
std::cout << "\n=== Access using operator[] ===" << std::endl;
m["apple"] = 4;
}
The output is
Hash called with <std::string>: apple
Hash called with <std::string>: banana
Hash called with <std::string>: oriange
=== Lookup for std::string type key ===
Hash called with <std::string>: apple
Equal called with <std::string, std::string>: apple vs apple
Found: 1
=== Lookup for std::string_view type key ===
Hash called with <std::string_view>: banana
Equal called with <string_view, std::string>: banana vs banana
Found: 2
=== Lookup for const char* type key ===
Hash called with <const char*>: oriange
Equal called with <const char*, std::string>: oriange vs oriange
Found: 3
=== Access using operator[] ===
Hash called with <std::string>: apple
Equal called with <std::string, std::string>: apple vs apple
is_transparent is a marker type indicating that the hash function or comparator supports heterogeneous lookup and can be any type. It seems the compiler calls different find based on this marker:
iterator find(const key_type& __x)
{ return _M_h.find(__x); }
template<typename _Kt>
auto find(const _Kt& __x) -> decltype(_M_h._M_find_tr(__x))
{ return _M_h._M_find_tr(__x); }
Because<span>std::basic_string<CharT,Traits,Allocator>::operator std::basic_string_view<CharT, Traits>() const noexcept;</span> exists, string can be directly converted to string_view, and string_view also has<span>constexpr std::basic_string_view<CharT,Traits>::basic_string_view(const CharT* s);</span> constructor, DebugStringHash and DebugStringEqual can be simplified to:
struct DebugStringHash
{
using is_transparent = void;
std::size_t operator()(std::string_view sv) const
{
return std::hash<std::string_view>{}(sv);
}
};
struct DebugStringEqual
{
using is_transparent = void;
bool operator()(std::string_view a, const std::string &b) const
{
return a == b;
}
};
std::bind_front
std::bind can bind parameters to any position and supports placeholder rearrangement, while std::bind_front can only fix the first few parameters:
int add(int a, int b, int c) { return a + b + c; }
struct Mul {
int operator()(int x, int y, int z) const { return x * y * z; }
};
int main() {
auto add10_20 = std::bind_front(add, 10, 20);
std::cout << add10_20(5) << "\n"; // 35
Mul m;
auto mul_2_3 = std::bind_front(&Mul::operator(), &m, 2, 3);
std::cout << mul_2_3(4) << "\n"; // 24
}
std::ssize
std::size() and std::ssize() can both be used to get the size of a container; the former returns an unsigned type, while the latter returns a signed type.
Unified Container Erasure: std::erase / std::erase_if
Previously, erasing elements from a container that meet specific conditions was quite cumbersome; for example, maps required manual traversal, and vectors needed to use the erase-remove idiom. Now, you can directly use std::erase and std::erase_if.
User-Defined Deduction Guides
User-Defined Deduction Guides, introduced in C++17, are features primarily used for template parameter deduction of class templates. They allow programmers to provide additional deduction rules for class templates, helping the compiler to more accurately deduce template parameters when constructing objects, especially when the constructor parameters are insufficient to directly deduce all template parameters. For example:
template <typename T>
struct DataWrapper
{
T data;
DataWrapper(const char *s) : data(s) {}
};
DataWrapper(const*) -> DataWrapper<std::string>;
If there are no custom guides, in<span>DataWrapper d("hello");</span>, the compiler cannot deduce the type of the template parameter unless explicitly specified as<span>DataWrapper<std::string> d("hello");</span>; however, with custom guides, you no longer need to manually specify the template parameter.
References
<span>[1]</span> Range Algorithms: https://cppreference.com/w/cpp/algorithm/ranges.html<span>[2]</span> Range Adaptors: https://en.cppreference.com/w/cpp/ranges.html#Range_adaptors<span>[3]</span> Standard Format Specifications: https://cppreference.cn/w/cpp/utility/format/spec<span>[4]</span> Complete Implementation of parse and format functions: https://cppreference.cn/w/cpp/utility/format/formatter