0% found this document useful (0 votes)
88 views132 pages

C++ Tips to Reduce Extra Objects

Uploaded by

Gamindu Udayanga
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
88 views132 pages

C++ Tips to Reduce Extra Objects

Uploaded by

Gamindu Udayanga
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

11

C++ Performance Tips:


Cutting Down on Unnecessary Objects
Kathleen Baker & Prithvi Okade
2

Motivation for this talk


• We work on a Chromium based C++ project (clang, no exceptions
enabled) with over 100 engineers.
• Over the past few years, while reviewing pull requests, we noticed
certain patterns of "extra objects" being created.
• This talk summarizes the most common patterns we've observed.
• This presentation is geared toward being guidelines of “correct”
code.
3

Contents
• Introduction
• Basic strategies to avoid extra objects
• Strategies for std::string and std::vector
• Common STL types and containers
• Associative containers
• Transparent comparators
• Moving data to compile time
4

What are extra objects?


Consider this code:
int main() { Hello World!!
const std::string str{"Hello World!!\n"};
const std::string str2{str};
std::cout << str2;
}

If our goal was to print the string, then the following code would suffice:
int main() {
std::cout << "Hello World!!\n";
}
We don’t need str and str2 to achieve our goal of printing the string.
Even with compiler optimization, the second code snippet generates much less code.
We consider these objects which are not necessary to achieve our goal as extra objects.
Not all scenarios for “extra” objects are so straightforward to detect.
5

Why are extra objects a problem?


C++ is a value (copy) semantic language by default.

MyClass c; c and c1 are different objects.


MyClass c1 = c;
This statement calls the “copy constructor” to create a new object.

void* operator new(size_t n) { The print statements in operator new and


void* p = malloc(n); operator delete will show us when
printf("operator new: n: %zu, p = %p\n", n, p); objects are created and destroyed in our
return p;
} examples.

void operator delete(void* p) noexcept {


printf("operator delete: p = %p\n", p);
free(p);
}
6

Why are extra objects a problem?


C++ is a value (copy) semantic language by default.

MyClass c; c and c1 are different objects.


MyClass c1 = c;
This statement calls the “copy constructor” to create a new object.

int main() { ==== Before initial string ======


puts("==== Before initial string ======"); operator new: n: 32, p = 0x5afba82c42b0
const std::string s("This is a hello world string!!"); ==== Before copy constructor ======
puts("==== Before copy constructor ======"); operator new: n: 32, p = 0x5afba82c42e0
const std::string s1 = s; ==== Before end ======
puts("==== Before end ======"); operator delete: p = 0x5afba82c42e0
} operator delete: p = 0x5afba82c42b0

The copy constructor of the string is called and that calls “operator new” to allocate memory.

Allocating memory is a costly runtime operation. Avoiding that is beneficial.


7

Why are extra objects a problem?


C++ is a value (copy) semantic language by default.

MyClass c;
MyClass c1 = std::move(c); This statement calls the “move constructor” to create a new object.

int main() {
puts("==== Before initial string ======"); ==== Before initial string ======
std::string s("This is a hello world string!!"); operator new: n: 32, p = 0x5f0c2817b2b0
puts("==== Before move constructor ======"); ==== Before move constructor ======
const std::string s1 = std::move(s); ==== Before end ======
puts("==== Before end ======"); operator delete: p = 0x5f0c2817b2b0
}

The “move constructor” of string “does not” allocate memory, but just swaps memory.

Even though “move” creates a new object, it is not “costly” for runtime.
8

Why are extra objects a problem?


C++ is a value (copy) semantic language by default.

std::string GetStr() {
return "This is a hello world string!!";
}
==== Before GetStr() ======
operator new: n: 32, p = 0x5e83f9b412b0
int main() {
==== Before end ======
puts("==== Before GetStr() ======");
operator delete: p = 0x5e83f9b412b0
const std::string s = GetStr();
puts("==== Before end ======");
}

This is in-place construction, and no extra objects are being created.

Creating objects in-place is preferrable over moving, which is preferrable over copying.
9

Why are extra objects a problem?


Let’s consider std::vector:

int main() { operator new: n: 24, p = 0x5770406a22a0


std::vector<int> v{1, 2, 3, 4, 5, 6}; ==== Before copy constructor ======
puts("==== Before copy constructor ======"); operator new: n: 24, p = 0x5770406a32d0
const std::vector<int> v1(v); ==== Before end ======
puts("==== Before end ======"); operator delete: p = 0x5770406a32d0
} operator delete: p = 0x5770406a22a0

std::vector’s copy constructor is “costly” for runtime, since it allocates memory.

int main() {
std::vector<int> v{1, 2, 3, 4, 5, 6}; operator new: n: 24, p = 0x61b6931952a0
puts("==== Before move constructor ======"); ==== Before move constructor ======
const std::vector<int> v1(std::move(v)); ==== Before end ======
puts("==== Before end ======"); operator delete: p = 0x61b6931952a0
}

Move constructor doesn’t allocate memory.


10

Why are extra objects a problem?


Let’s consider std::list:
int main() { operator new: n: 24, p = 0x562dc956e2a0
std::list<int> l{1, 2, 3}; operator new: n: 24, p = 0x562dc956f2d0
puts("==== Before copy constructor ======"); operator new: n: 24, p = 0x562dc956f2f0
const std::list<int> l1(l); ==== Before copy constructor ======
puts("==== Before end ======"); operator new: n: 24, p = 0x562dc956f310 std::list’s copy constructor
} operator new: n: 24, p = 0x562dc956f330 is even more expensive.
operator new: n: 24, p = 0x562dc956f350
==== Before end ======
operator delete: p = 0x562dc956f310
operator delete: p = 0x562dc956f330
operator delete: p = 0x562dc956f350
operator delete: p = 0x562dc956e2a0
operator delete: p = 0x562dc956f2d0
operator delete: p = 0x562dc956f2f0

int main() { operator new: n: 24, p = 0x55d161e262a0


std::list<int> l{1, 2, 3}; operator new: n: 24, p = 0x55d161e272d0 Move constructor is better
puts("==== Before move constructor ======"); operator new: n: 24, p = 0x55d161e272f0 because it doesn’t allocate
const std::list<int> l1(std::move(l)); ==== Before move constructor ====== memory.
puts("==== Before end ======"); ==== Before end ======
} operator delete: p = 0x55d161e262a0
operator delete: p = 0x55d161e272d0
operator delete: p = 0x55d161e272f0
11

Why are extra objects a problem?


• The object’s creation can cause costly operations at runtime
• Costly operations can be memory allocation, making operating system calls,
running costly algorithms, etc.
• Can cause more code to be executed at runtime.
• Can need more stack / heap space at runtime.

How much performance benefit will I get?

Please measure your scenario!


12

For which types are extra objects ok?


It’s fine to create extra objects for:
• Plain old data types, e.g. int, float, etc.
• Objects that are “small” in size and do “non-costly” operations in their
constructors.
• std::string_view, std::span, std::initializer_list, std::mdspan, range view types.

As a rule of thumb, we can consider objects C++ STL considers to be lightweight as


“small” objects.
• std::string_view, std::span are 16 bytes in 64 bit.
• std::mdspan is 24 bytes in 64 bit.
• If your user defined type doesn’t have any costly copy operations and is
<=24bytes (64 bit) we can consider it “small”.
13

Compiler Warnings Clang-Tidy Checks


-Wexit-time-destructors performance-unnecessary-value-param
-Wglobal-constructors performance-unnecessary-copy-initialization
-Wpessimizing-move performance-for-range-copy
-Wrange-loop-construct modernize-pass-by-value
performance-inefficient-vector-operation
performance-noexcept-move-constructor
modernize-use-emplace
-Wall
-Wextra
-Weverything
14

Basic Strategies to Avoid


Extra Temporary Objects
2/8
Non-trivial type as read-only argument to function 15

void Foo(std::string s) { int main() {


// Code that only reads `s`. std::string s("This is a hello world string");
puts("==== Before Foo call ======"); operator new: n: 32, p = 0x5a78e62a82a0
} ==== Before Foo call ======
Foo(s); operator new: n: 32, p = 0x5a78e62a92e0
void* operator new(size_t n) { operator delete: p = 0x5a78e62a92e0
void* p = malloc(n); ==== After Foo call ======
puts("==== After Foo call ======"); operator delete: p = 0x5a78e62a82a0
printf("operator new: n: %zu, p = %p\n", n, p); }
return p;
}

void operator delete(void* p) noexcept { void Foo(const std::string& s) {


printf("operator delete: p = %p\n", p); // Code that only reads `s`.
free(p); }
}
int main() {
std::string s("This is a hello world string");
It is better to use const & in this case: puts("==== Before Foo call ======");
operator new: n: 32, p = 0x577ff6ef62a0
==== Before Foo call ======
Foo(s); ==== After Foo call ======
operator delete: p = 0x577ff6ef62a0
puts("==== After Foo call ======");
}

As a rule of thumb pass “non-trivial” “read-only” objects as const reference in arguments to functions.

It is better to pass read-only “string”s as std::string_view.


There are some nuances when using std::string_view that we will consider in later slides.
Non-trivial type as read-only argument to function 16

void Foo(std::string s) { When clang-tidy check is used: --checks=performance-unnecessary-value-param


// Code that only reads `s`.
} warning: the parameter 'str' is copied for each invocation but only used as a const reference;
consider making it a const reference [performance-unnecessary-value-param]
void Foo(std::string str) {}
^
const &

void Foo(std::string_view s) {} struct B {


B() = default;
B(const B&) = default;
B(B&&) noexcept = default;
};

void Foo(B b) {}

struct B { warning: the parameter 'b' is copied for each invocation but only used as a const reference;
B() = default; consider making it a const reference [performance-unnecessary-value-param]
void Foo(B b) {}
B(const B&);
^
B(B&&) noexcept = default; const &
};
From this documentation:
B::B(const B&) = default; The check is only applied to parameters of types that are expensive to copy which means they are not
trivially copyable or have a non-trivial copy constructor or destructor.
void Foo(B b) {}
Returning non-trivial member object 17

struct A final {
// Constructor struct A will be the placeholder for “non-trivial” object that we
A() { puts("A()"); } will use for most of this presentation.
A(int, int) { puts("A(int, int)"); }

// Destructor The print statements in its special member functions help us


~A() { puts("~A()"); } in understanding whether copies are being made.
// Copy constructor
A(const A&) { puts("A(const A&)"); } A(const A&)

// Move constructor Better


A(A&&) noexcept { puts("A(A&&)"); }
A(A&&)
// Copy assignment operator Best
A& operator=(const A&) {
puts("A& operator=(const A&)");
A(int, int) OR A()
return *this;
}
We want in-place construction without copies or moves.
// Move assignment operator
A& operator=(A&&) noexcept {
puts("A& operator=(A&&)");
return *this;
}
};
Returning non-trivial member object 18

class MyClass final { int main() { A(int, int)


public: MyClass obj; ==== Before a_not_great ====
MyClass() : a_(10, 10) {} std::cout << "==== Before a_not_great ====\n"; A(const A&)
{ ~A()
const A& a() const { return a_; } const auto a = obj.a_not_great(); ==== After a_not_great ====
} ~A()
A a_not_great() const { return a_; } std::cout << "==== After a_not_great ====\n";
}
private:
A a_;
int main() { A(int, int)
}; MyClass obj; ==== Before a() ====
std::cout << "==== Before a() ====\n"; ==== After a() ====
{ ~A()
const auto& a = obj.a();
(void)a; // Suppress warning.
}
std::cout << "==== After a() ====\n";
}

When returning a non-trivial member variable from a member function, consider returning as const&.
Returning non-trivial member object 19

class MyClass final { int main() { A(int, int)


public: MyClass obj; ==== Before const auto& a ====
MyClass() : a_(10, 10) {} std::cout << "==== Before const auto& a ====\n"; ==== After const auto& a ====
{ ~A()
const A& a() const { return a_; } const auto& a = obj.a();
(void)a; // Suppress warning.
A a_not_great() const { return a_; } }
std::cout << "==== After const auto& a ====\n";
private: }
A a_;
}; int main() { A(int, int)
MyClass obj; ==== Before const auto a ====
std::cout << "==== Before const auto a ====\n"; A(const A&)
{ ~A()
const auto a = obj.a(); ==== After const auto a ====
} ~A()
std::cout << "==== After const auto a ====\n";
}

When the returned type is not held as const&, the copy constructor is still called to create an extra object.
Returning non-trivial member object 20

Return const& for a non-trivial type of member object being returned from a class's const member function
class MyClass final { int main() {
public: MyClass obj;
MyClass() : a_(10, 10) {}
const auto a = obj.a_not_great();
const A& a() const { return a_; }
const auto& a_ref = obj.a();
A a_not_great() const { return a_; } (void)a_ref; // Suppress warning.

private: const auto a_copy = obj.a();


A a_;
}; MyClass obj2 = obj;

// Suppress warnings.
(void)a_copy;
(void)obj2;
}

When clang-tidy check is used: --checks=performance-unnecessary-copy-initialization


warning: the const qualified variable 'a_copy' is copy-constructed from a const reference; consider
making it a const reference [performance-unnecessary-copy-initialization]
const auto a_copy = obj.a();
^
&
warning: local copy 'obj2' of the variable 'obj' is never modified; consider avoiding the copy
[performance-unnecessary-copy-initialization]
MyClass obj2 = obj;
^
const &
Range based for loop 21

std::vector<A> GetVec(int n) { int main() { A(int, int)


std::vector<A> vec; const auto vec = GetVec(2); A(int, int)
[Link](n); std::cout << "===== Before `auto` traversal ====\n"; ===== Before `auto` traversal ====
for (int i = 0; i < n; ++i) { { A(const A&)
vec.emplace_back(i, i); for (const auto obj : vec) { ~A()
} // Do stuff;
A(const A&)
return vec; }
~A()
} }
std::cout << "=== Before `const auto&` traversal ===\n"; === Before `const auto&` traversal ===
{ === Before `auto&&` traversal ===
for (const auto& obj : vec) { ===== Before end ====
// Do stuff; ~A()
} ~A()
}
std::cout << "=== Before `auto&&` traversal ===\n";
{
for (auto&& obj : vec) { // Forwarding reference.
// Do stuff; `obj` is of type A&.
}
}
std::cout << "===== Before end ====\n";
}

Use const auto& (or auto&&) in range based for loop traversal for non-trivial object container.
Range based for loop 22

int main() { When we use:


const auto vec = GetVec(2); -Wrange-loop-construct -Werror
std::cout << "===== Before `auto` traversal ====\n";
{
error: loop variable 'obj' creates a copy from type 'const
for (const auto obj : vec) { A' [-Werror,-Wrange-loop-construct]
// Do stuff; for (const auto obj : vec) {
} ^
} note: use reference type 'const A &' to prevent copying
std::cout << "=== Before `const auto&` traversal ===\n"; for (const auto obj : vec) {
{ ^~~~~~~~~~~~~~~~
for (const auto& obj : vec) { &
// Do stuff;
}
} -Wrange-loop-construct is included in -Wall
std::cout << "=== Before `auto&&` traversal ===\n";
{
for (auto&& obj : vec) { // Forwarding reference.
// Do stuff; `obj` is of type A&.
}
}
std::cout << "===== Before end ====\n";
}
Range based for loop 23

Finding issues with -Wrange-loop-construct

struct A { error: loop variable 'a' creates a copy from type 'A const' [-Werror,-Wrange-loop-
void Foo() const {} construct]
void Bar() {} for (const auto a : a_vec) {
std::string str; ^
}; note: use reference type 'A const &' to prevent copying
for (const auto a : a_vec) {
^~~~~~~~~~~~~~
int main() { &
std::vector<std::string> vec;
for (auto v : vec) {
std::cout << v << '\n'; When clang-tidy check is used:
} --checks=performance-for-range-copy
// Vector of non-trivially
// copy constructable objects. warning: loop variable is copied but only used as const reference; consider making it a
std::vector<A> a_vec; const reference [performance-for-range-copy]
for (const auto a : a_vec) { for (auto v : vec) {
[Link](); ^
} const &
for (auto a : a_vec) { warning: the loop variable's type is not a reference type; this creates a copy in each
[Link](); iteration; consider making this a reference [performance-for-range-copy]
} for (const auto a : a_vec) {
} ^
&
24

Structured Bindings and


Lambdas
3/8
Structured binding 25

struct B { int main() { A(int, int)


A a{1, 1}; B b; A(const A&)
int i = 0; [[maybe_unused]] const auto [a, _] = b; ~A()
}; } ~A()

int main() { A(int, int)


B b; ~A()
[[maybe_unused]] const auto& [a, _] = b;
}

const auto creates extra object.

const auto& leads to no temporary objects.

Consider using const & for read-only structured binding variables.


Explicitly move-ing object out of function 26

A Foo() { A(int, int) A Foo() { A(int, int)


A a{10, 10}; A(A&&) A a{10, 10}; ~A()
return std::move(a); ~A() return a;
} ~A() }

int main() { int main() {


std::ignore = Foo(); std::ignore = Foo();
} }

Explicit std::move calls defeats NRVO (Named Return Value Optimization)

Don’t use std::move in such cases.

NRVO is not “required” by standard. But most compilers implement it for such scenarios.

A Foo() { A(int, int)


return {10, 10}; ~A()
}
This is guaranteed copy elision from C++17.
int main() {
std::ignore = Foo();
} Also known as:
a) Deferred Temporary materialization.
b) Unmaterialized value passing.
Explicitly move-ing object out of function 27

A Foo() { A(int, int)


A a{10, 10}; A(A&&)
return std::move(a); ~A()
} ~A()

int main() {
std::ignore = Foo(); When we use: -Wpessimizing-move is included
} -Wpessimizing-move -Werror in -Wall

error: moving a local object in a return statement prevents copy elision [-Werror,-
Wpessimizing-move]
return std::move(a);
^
note: remove std::move call here
return std::move(a);
Scenario for explicit move on return 28

struct B { A Foo() { ---- Start of Foo ----


A a{1, 1}; std::cout << "---- Start of Foo ----\n"; A(int, int)
int i = 0; B b; ---- Before structured binding ----
}; std::cout << "---- Before structured binding ----\n"; A(const A&)
auto& [a, _] = b; ~A()
~A()
return a;
}

int main() {
std::ignore = Foo();
}

A Foo() { ---- Start of Foo ----


std::cout << "---- Start of Foo ----\n"; A(int, int)
B b; ---- Before structured binding ----
std::cout << "---- Before structured binding ----\n"; A(A&&)
auto& [a, _] = b; ~A()
~A()
return std::move(a);
}

int main() {
std::ignore = Foo();
}

Without explicit std::move, copy constructor gets called in this scenario.


Scenario for explicit move on return 29

struct B { A Foo() { A(int, int)


A a{1, 1}; B b; A(const A&)
int i = 0; return b.a; ~A()
}; } ~A()

int main() {
std::ignore = Foo();
}

A Foo() { A(int, int)


B b; A(A&&)
return std::move(b.a); ~A()
} ~A()

int main() {
std::ignore = Foo();
}

Without explicit std::move, copy constructor gets called in this scenario.


Lambda captures 30

int main() { A()


A a; ===== Before fn =====
std::cout << "===== Before fn =====\n"; A(const A&)
auto fn = [a]() { === Before fn2 = fn ===
// Do stuff. A(const A&)
=== After fn2 = fn ===
};
~A()
std::cout << "=== Before fn2 = fn ===\n";
~A()
auto fn2 = fn; ~A()
std::cout << "=== After fn2 = fn ===\n";
}

int main() { A()


A a; ===== Before fn =====
std::cout << "===== Before fn =====\n"; === Before fn2 = fn ===
auto fn = [&a]() { === After fn2 = fn ===
(void)a; // Suppress warning. ~A()
};
std::cout << "=== Before fn2 = fn ===\n";
auto fn2 = fn; Capture by reference to avoid copy.
(void)fn2; // Suppress warning.
std::cout << "=== After fn2 = fn ===\n";
}
Lambda captures 31

struct A {
A() { puts("A()"); }
~A() { puts("~A()"); }
A(const A&) { puts("A(const A&)"); }
A& operator=(const A&) {
puts("A& operator=(const A&)");
return *this;
}
A(A&&) noexcept { puts("A(A&&)"); }
A& operator=(A&&) noexcept {
puts("A& operator=(A&&)");
return *this;
}

auto GetCapture() {
return [*this]() { std::cout << "Inside lambda in GetCapture\n"; };
}
auto GetCapture1() {
return [this]() { std::cout << "Inside lambda in GetCapture1\n"; };
}
auto GetCapture2() {
return [&]() { std::cout << "Inside lambda in GetCapture2\n"; };
}
auto GetCapture3() {
return [=]() { std::cout << "Inside lambda in GetCapture3\n"; };
}
};
Lambda captures 32

struct A { int main() {


// Special member functions. A a;
std::cout << "==== Before [Link]() ====\n";
auto GetCapture() { {
return [*this]() { auto ln = [Link]();
std::cout << "Inside lambda in GetCapture\n"; }; ln();
} }
auto GetCapture1() { std::cout << "==== Before a.GetCapture1() ====\n";
return [this]() { {
std::cout << "Inside lambda in GetCapture1\n"; }; auto ln = a.GetCapture1();
} ln();
auto GetCapture2() { }
return [&]() { std::cout << "==== Before a.GetCapture2() ====\n";
std::cout << "Inside lambda in GetCapture2\n"; }; {
} auto ln = a.GetCapture2();
auto GetCapture3() { ln();
return [=]() { }
std::cout << "Inside lambda in GetCapture3\n"; }; std::cout << "==== Before a.GetCapture3() ====\n";
} { A()
}; auto ln = a.GetCapture3(); ==== Before [Link]() ====
ln(); A(const A&)
}
Inside lambda in GetCapture
std::cout << "==== Before end ====\n";
~A()
} ==== Before a.GetCapture1() ====
*this will make a copy of the object. Inside lambda in GetCapture1
==== Before a.GetCapture2() ====
Inside lambda in GetCapture2
==== Before a.GetCapture3() ====
Inside lambda in GetCapture3
==== Before end ====
~A()
Lambda captures 33

struct A {
int a = 10;

auto GetCapture() {
return [*this]() {
std::cout << "Inside lambda in GetCapture: a: " << a << '\n';
}; error: implicit capture of 'this' with a capture default of '=' is
} deprecated [-Werror,-Wdeprecated-this-capture]
auto GetCapture1() {
return [this]() { std::cout << "Inside lambda in GetCapture3: a: " << a << '\n';
std::cout << "Inside lambda in GetCapture1: a: " << a << '\n';
};
}
auto GetCapture2() { Implicit capture of this via [=] was deprecated in
return [&]() { C++20 (P0806R2)
std::cout << "Inside lambda in GetCapture2: a: " << a << '\n';
};
}
auto GetCapture3() {
return [=]() {
std::cout << "Inside lambda in GetCapture3: a: " << a << '\n';
};
}
};
Lambda captures 34

template <typename... Args> int main() { A()


auto Foo(Args&&... args) { A a; ==== Before Foo() ====
return [args...]() {}; std::cout << "==== Before Foo() ====\n"; A(const A&)
} { ~A()
Foo(a); ==== Before Foo2() ====
template <typename... Args> } A(const A&)
auto Foo2(Args&&... args) { std::cout << "==== Before Foo2() ====\n"; ~A()
return [... args = args]() {}; { ==== Before Foo3() ====
} Foo2(a); A(const A&)
} ~A()
template <typename... Args> std::cout << "==== Before Foo3() ====\n"; ==== Before Foo4() ====
auto Foo3(Args&&... args) { { ==== Before Foo5() ====
return [... args = std::forward<Args>(args)]() {}; Foo3(a); ==== Before end ====
} } ~A()
std::cout << "==== Before Foo4() ====\n";
template <typename... Args> {
auto Foo4(Args&&... args) { Foo4(a);
return [&args...]() {}; }
} std::cout << "==== Before Foo5() ====\n";
{
template <typename... Args> Foo5(a);
auto Foo5(Args&&... args) { }
return [... args = &args]() {}; std::cout << "==== Before end ====\n";
} }

Use reference capture with variadic arguments to remove extra copies.


35

Strategies for std::string


and std::vector
4/8
36
string_view instead of string
Use std::string_view to stop creating std::string at runtime.
void Foo(const std::string& s) { int main() {
Foo: s: Hello
std::cout << "Foo: s: " << s << '\n'; // Created at runtime.
} const std::string str("Hello");
FooBetter: s: Hello
void FooBetter(std::string_view s) { // Creates temporary string.
FooBetter: s: Hello
std::cout << "FooBetter: s: " << s << '\n'; Foo("Hello");
FooBetter: s: Hell
}
// No temporary string created.
FooBetter("Hello"); // Allows conversion from `const char*`.
FooBetter(str); // Allows conversion from `std::string`.
FooBetter(
{str.c_str(), [Link]() - 1}); // Allows conversion from {const char*, len}.
}

std::string_view can handle std::string, const char* and {const char*, len} as arguments without the need to create different
functions for each. It does not do any heap allocation.

int main() { std::string_view can also be used to create the string at compile time instead
// Created at runtime. of runtime.
const std::string str("Hello");
// Created at compile time.
static constexpr std::string_view kStr("Hello");
}
37
string_view instead of string
Use std::string_view to stop creating std::string at runtime.
void Foo(const std::string& s) { // Platform function which expects null terminated string.
std::cout << "Foo: s: " << s << '\n'; void FooPlatform(const char* p);
}
void Foo(const std::string& s) {
void FooBetter(std::string_view s) { FooPlatform(s.c_str());
std::cout << "FooBetter: s: " << s << '\n'; }
}

Cannot use std::string_view in this case, since it may not be null terminated.

There isn’t a C++ standard “null-terminated-string-view-type”.

Chromium has base::cstring_view which is a null-terminated-string-view type.

Similar types can be used as replacement in this scenario.

Check out this presentation by Jasmine Lopez & Prithvi Okade in


CppCon 2024 for more details on string_view.
38
string_view instead of string
What about the following cases?
struct A { These are “sink” scenarios.
A(const std::string& str) : str_(str) {}
// Other functions.
void SetStr(const std::string& str) { str_ = str; }
// Other functions.
std::string str_;
};

Instead of using std::string_view, we can follow the cpp core guideline: F.18: For "will-move-from"
parameters, pass by X&& and std::move the parameter.
struct A {
A(std::string&& str) : str_(std::move(str)) {}
// Other functions.
void SetStr(std::string&& str) { str_ = std::move(str); }
// Other functions.
std::string str_;
};
39
string_view instead of string
Sink scenario:
struct A { When the following clang-tidy check is used:
A(const std::string& str) : str_(str) {} --checks=modernize-pass-by-value
// Other functions.
void SetStr(const std::string& str) { str_ = str; } warning: pass by value and use std::move [modernize-pass-by-value]
// Other functions. A(const std::string& str) : str_(str) {}
std::string str_; ^~~~~~~~~~~~~~~~~~
}; std::string std::move( )

From the documentation for this check:


Currently, only constructors are transformed to make use of pass-by-value. Contributions that handle other situations are
welcome!
40
std::string::operator+ can cause extra strings
const std::string s = std::string{"Hello there. Good morning!"} + operator new: size: 32
" Hope you are doing great!" + operator new: size: 64
" How's the weather in Denver?"; operator delete
std::cout << s << '\n'; operator new: size: 128
operator delete
Hello there. Good morning! Hope you are doing great!
How's the weather in Denver?
operator delete

const std::string s = operator new: size: 88


MyStrCat({"Hello there. Good morning! ", Hello there. Good morning!
"Hope you are doing great!", Hope you are doing great! How's the weather in Denver?
" How's the weather in Denver?"}); operator delete
std::cout << s << '\n';

std::string MyStrCat(std::initializer_list<std::string_view> strs) { Only one string is created with MyStrCat


size_t len = 0;
for (const auto str : strs) {
len += [Link]();
CppCon 2017 Lightning talk by Jorg Brown
} about absl::StrCat.
std::string final_str;
final_str.reserve(len + 1); Chromium has base::StrCat.
for (const auto str : strs) {
final_str += str;
}
return final_str;
}
41
std::string::operator+ can cause extra strings
For "small" strings, no memory is allocated on the heap although temporary strings do get created

const std::string s = std::string{"Hello"} + " " + "World" + "!"; Hello World!


std::cout << s << '\n';

const std::string s = MyStrCat({"Hello", " ", "World", "!"}); Hello World!


std::cout << s << '\n';

MyStrCat is still optimal because it only creates one


string

+ is only optimal if there is a single concatenation and either one of the parameters is std::string.

int main() {
std::string s; // Fill it in.
const auto sr = s + "right";
const auto sl = "left" + s;
}
42
Use string_view::substr to remove possible memory allocation
static constexpr std::string_view kHttp("[Link] int main() {
static constexpr std::string_view kHttps("[Link] const std::string str("[Link]

std::string RemoveScheme(const std::string& s) { std::cout << "========= Before substr ================\n";


if (s.starts_with(kHttp)) { const auto scheme_removed = RemoveScheme(str);
return [Link]([Link]()); std::cout << "Scheme removed: " << scheme_removed << '\n';
}
if (s.starts_with(kHttps)) { std::cout << "========= After string substr ================\n";
return [Link]([Link]());
} const auto scheme_removed_sv = RemoveSchemeBetter(str);
return {}; std::cout << "Scheme removed better: " << scheme_removed_sv << '\n';
}
std::cout << "========= After string_view substr ================\n";
std::string_view RemoveSchemeBetter(std::string_view s) { }
if (s.starts_with(kHttp)) {
return [Link]([Link]());
} operator new: size: 25
if (s.starts_with(kHttps)) { ========= Before substr ================
return [Link]([Link]()); operator new: size: 17
} Scheme removed: [Link]/category/news/
return {}; ========= After string substr ================
} Scheme removed better: [Link]/category/news/
========= After string_view substr ================
operator delete
operator delete
If we don’t intend to modify the result of substr() we
can use std::string_view::substr() to remove possible Ensure the underlying string memory is valid when the
memory allocation. std::string_view is used.
sizeof(A): 4 43

Use reserve for vector --- After `vec` creation ---


operator new: size: 4
A(0)
operator new: size: 8
Use reserve if we know the size of the vector in advance A(1)
A(A&&): 0
~A()
int main() { operator delete
constexpr int kTestSize = 4; operator new: size: 16
std::cout << "sizeof(A): " << sizeof(A) << '\n'; A(2)
std::vector<A> vec; A(A&&): 0
std::cout << "--- After `vec` creation ---\n"; A(A&&): 1
for (int i = 0; i < kTestSize; ++i) { ~A()
vec.emplace_back(i); ~A()
} operator delete
A(3)
}
~A()
~A()
~A()
~A()
operator delete

int main() {
constexpr int kTestSize = 4;
sizeof(A): 4
--- After `vec` creation ---
reserve ensures
std::cout << "sizeof(A): " << sizeof(A) << '\n'; operator new: size: 16 there’s a single
--- After `[Link](kTestSize)` ---
std::vector<A> vec;
std::cout << "--- After `vec` creation ---\n"; A(0) allocation and hence
[Link](kTestSize);
A(1)
A(2)
no temporaries
std::cout << "--- After `[Link](kTestSize)` ---\n";
for (int i = 0; i < kTestSize; ++i) {
A(3) during resize.
~A()
vec.emplace_back(i); ~A()
} ~A()
} ~A()
operator delete
44

Use reserve for vector


Use reserve if we know the size of the vector in advance

int main() { When clang-tidy check is used:


std::vector<A> vec;
std::cout << "--- After `vec` creation ---\n";
--checks=performance-inefficient-vector-operation
for (int i = 0; i < 4; ++i) {
vec.emplace_back(i);
} warning: 'emplace_back' is called inside a loop; consider pre-allocating the
} container capacity before the loop [performance-inefficient-vector-operation]
for (int i = 0; i < 4; ++i) {
vec.emplace_back(i);
^
45

Use span to stop forcing a vector creation.


void Foo(const std::vector<A>& v) {
// Use v.
}

void FooBetter(std::span<const A> v) {


// Use v.
}

int main() { A(int, int)


// Cannot be `constexpr` since A constructor is not `constexpr`. A(int, int)
const A arr[] = {{1, 2}, {3, 4}, {5, 6}}; A(int, int)
std::cout << "===== Before Foo =====\n"; ===== Before Foo =====
A(const A&)
// Temporary vector being created. A(const A&)
{ Foo({arr, arr + 3}); } A(const A&)
~A()
std::cout << "===== Before FooBetter =====\n"; ~A()
{ FooBetter(arr); } ~A()
std::cout << "===== Before end =====\n"; ===== Before FooBetter =====
} ===== Before end =====
~A()
~A()
Using std::span instead of std::vector allows the ~A()
function to be used with C-array without the need to
create a vector.
46

Use span to stop forcing a vector creation.


void Foo(const std::vector<A>& v) {
// Use v.
}
This allows the function to also work with other contiguous
void FooBetter(std::span<const A> v) { containers like vector, array, initializer_list.
// Use v.
}

int main() {
std::vector<A> v = {{1, 2}, {3, 4}, {5, 6}};
FooBetter(v);
std::array<A, 3> a = {A{1, 2}, A{3, 4}, A{5, 6}}; This creates “extra” objects as we will see in the
FooBetter(a);
std::initializer_list<A> l = {A{1, 2}, A{3, 4}, A{5, 6}}; later slides.
FooBetter(l);
FooBetter({{A{1, 2}, A{3, 4}, A{5, 6}}});
}
47
Use explicit std::move when a non-temporary object needs to be created.
Here’s an example for std::vector.
int main() { ==== Before non-move push_back ====
std::vector<A> vec; A(int, int)
[Link](2); A(const A&)
std::cout << "==== Before non-move push_back ====\n"; ~A()
{ ==== Before move push_back ====
A a(10, 10); A(int, int)
// Assume we update `a` based on some conditions. A(A&&)
vec.push_back(a); ~A()
} ==== Before end ====
std::cout << "==== Before move push_back ====\n"; ~A()
{ ~A()
A a(10, 10);
// Assume we update `a` based on some conditions.
vec.push_back(std::move(a)); // `move` is better here.
}
std::cout << "==== Before end ====\n";
}

In this scenario, since we cannot remove the “extra” “non-trivial” object, it is best to use std::move to “steal”
resources and gain performance.
48

Temporary Objects in Common


STL types and Containers
5/8
std::initializer_list 49

int main() { A(int, int) Three objects are created and then copied into the
std::vector<A> v{{1, 1}, {2, 2}, {3, 3}}; A(int, int)
} A(int, int) vector
A(const A&)
A(const A&)
A(const A&)
~A()
~A()
~A()
~A()
~A()
~A()

int main() { A(int, int)


constexpr int kTestSize = 3; A(int, int)
std::vector<A> v; A(int, int)
[Link](kTestSize); ~A()
for (int i = 0; i < kTestSize; ++i) { ~A()
v.emplace_back(i, i); ~A()
}
}

reserve / emplace_back removes the need for temporary objects and instead does in-place construction.
std::pair 50

A(int, int)
int main() { A(int, int)
const std::pair<A, A> pa{{1, 1}, {2, 2}}; A(const A&)
} A(const A&)
~A()
~A()
~A()
~A()

C++23 ensures move construction instead of copy A(int, int)


construction. A(int, int)
A(A&&)
A(A&&)
int main() { ~A() int main() {
const auto pa = std::make_pair(A{1, 1}, A{2, 2}); ~A() const std::pair pa{A{1, 1}, A{2, 2}};
} ~A() }
~A()
make_pair also ensures move construction instead of
Also has same output with move construction.
copy construction.

To remove the extra objects and do in-place


construction, we need to use std::piecewise_construct.

int main() { A(int, int)


const std::pair<A, A> pa{std::piecewise_construct, A(int, int)
std::forward_as_tuple(1, 1), ~A()
std::forward_as_tuple(2, 2)}; ~A()
}
std::tuple A(int, int)
51

int main() { A(int, int)


const std::tuple<A, A> t{{1, 1}, {2, 2}}; A(const A&)
} A(const A&)
~A()
~A()
~A()
~A()

int main() { A(int, int) int main() {


const auto t = std::make_tuple(A{1, 1}, A{2, 2}); A(int, int) const std::tuple t{A{1, 1}, A{2, 2}};
} A(A&&) }
A(A&&)
make_tuple ensures move construction instead of copy ~A() Also has same output with move construction.
~A()
construction. ~A()
~A()

There is no in-place construction for tuple. So, move instead of copy constructor is the best we can do.

This omission was discussed in this stackoverflow post.


std::optional 52

int main() { A(int, int)


// std::optional<A> oa(10, 10); // Compilation ERROR. A(A&&)
const std::optional<A> oa = A{10, 10}; ~A()
} ~A()

int main() { A(int, int)


const auto oa = std::make_optional<A>(10, 10); ~A()
}

int main() { A(int, int)


const std::optional<A> oa(std::in_place, 10, 10); ~A()
}

Use in_place_t constructor or make_optional to do “in-place” construction.


std::optional: Deferred creation 53

int main() { ===== Before assign ======


std::optional<A> oa; A(int, int)
puts("===== Before assign ======"); A(A&&)
oa = A{1, 1}; ~A()
puts("===== After assign ======"); ===== After assign ======
} ~A()

int main() { ===== Before emplace ======


std::optional<A> oa; A(int, int)
puts("===== Before emplace ======"); ===== After emplace ======
[Link](1, 1); ~A()
puts("===== After emplace ======");
}

emplace is better than assignment.


int main() { ===== Before emplace ======
std::optional<A> oa; A(int, int)
puts("===== Before emplace ======"); ===== Before 2nd emplace ======
[Link](1, 1); ~A()
puts("===== Before 2nd emplace ======"); A(int, int)
[Link](2, 2); ===== After 2nd emplace ======
puts("===== After 2nd emplace ======"); ~A()
}

emplace destroys current object, before in-place construction.


std::expected 54

std::expected<A, bool> Foo() { A(int, int)


return A{10, 10}; A(A&&)
} ~A()
~A()
int main() {
std::ignore = Foo();
}

std::expected<A, bool> Foo() { A(int, int)


return std::expected<A, bool>{std::in_place, 10, 10}; ~A()
}

int main() {
std::ignore = Foo();
}

in_place_t constructor removes the temporary object.

For a non-trivial type being used in "success" type of std::expected, use in_place_t constructor to create the
object in place.
std::unexpected 55

Error type of “std::expected”:


struct Error final { std::expected<int, Error> Unexpected() { Error(int, int)
Error(int, int) { puts("Error(int, int)"); } return std::unexpected{Error{10, 10}}; Error(Error&&)
} Error(Error&&)
~Error() { puts("~Error()"); } ~Error()
int main() { ~Error()
Error(const Error&) { puts("Error(const Error&)"); } std::ignore = Unexpected(); ~Error()
Error(Error&&) noexcept { puts("Error(Error&&)"); } }

Error& operator=(const Error&) {


puts("Error& operator=(const Error&)"); std::expected<int, Error> Unexpected() { Error(int, int)
return *this; return std::unexpected<Error>{std::in_place, 10, 10}; Error(Error&&)
} } ~Error()
~Error()
Error& operator=(Error&&) noexcept { int main() {
puts("Error& operator=(ErrorA&&)"); std::ignore = Unexpected();
return *this; }
}
};
std::expected<int, Error> Unexpected() { Error(int, int)
return std::expected<int, Error>{std::unexpect, 10, 10}; ~Error()
}
std::unexpect_t constructor removes the
temporary object. int main() {
std::ignore = Unexpected();
}

For a non-trivial type being used in "error" type of std::expected, use unexpect_t constructor to create the error
object in place.
std::variant 56

int main() { A(int, int)


std::variant<A, int> v{A{10, 10}}; A(A&&)
} ~A()
~A()

int main() { A(int, int)


std::variant<A, int> v{std::in_place_type<A>, 10, 10}; ~A()
}

int main() { A(int, int)


std::variant<A, int> v{std::in_place_index<0>, 10, 10}; ~A()
}

std::in_place_type or std::in_place_index constructor removes the temporary object.

For a non-trivial type being used in variant, use std::in_place_type or std::in_place_index constructor to
create the object in place.
std::variant: Changing value type 57

int main() { A(int, int)


std::variant<int, A> v; A(A&&)
v = A{10, 10}; ~A()
~A()
}

int main() { A(int, int)


std::variant<int, A> v; ~A()
[Link]<A>(10, 10);
}

emplace is better for changing types for a variant.

For a non-trivial type being used in variant, use emplace to change the object type contained in the variant.
std::variant: Changing value of existing type 58

int main() { A(int, int)


std::variant<A, int> v{std::in_place_type<A>, 0, 0}; ------ Before assignment ------
A(int, int)
std::cout << "------ Before assignment ------\n";
A& operator=(A&&)
v = A{10, 10}; ~A()
std::cout << "------ After assignment ------\n"; ------ After assignment ------
} ~A()

int main() { A(int, int)


std::variant<A, int> v{std::in_place_type<A>, 0, 0}; ------ Before emplace ------
~A()
std::cout << "------ Before emplace ------\n";
A(int, int)
[Link]<A>(10, 10); ------ After emplace ------
std::cout << "------ After emplace ------\n"; ~A()
}

emplace performs better assignment in this non-type changing scenario too.


emplace causes the destructor of the object contained inside std::variant to always get called.
std::to_array 59

int main() { A(int, int)


A(int, int)
const auto arr = std::to_array<A>({{1, 2}, {3, 4}}); A(A&&)
} A(A&&)
~A()
~A()
~A()
~A()

A(int, int)
int main() { A(int, int)
const std::array<A, 2> arr = {A{5, 6}, A{7, 8}}; ~A()
} ~A()

A(int, int)
int main() { A(int, int)
// Uses deduction guide. ~A()
const std::array arr = {A{5, 6}, A{7, 8}}; ~A()

To create a std::array of non-trivial objects, consider using std::array constructor


instead of std::to_array.
Adding elements to vector 60

struct A final { int main() {


A(int a) : a_(a) { printf("A(%d)\n", a_); } A a(10);
A copy_a = a;
~A() { puts("~A()"); } A move_a = std::move(a);
}
A(const A& rhs) : a_(rhs.a_) { printf("A(const A&): %d\n", a_); }

A(A&& rhs) noexcept : a_(rhs.a_) { printf("A(A&&): %d\n", a_); } A(10)


A(const A&): 10
A& operator=(const A& rhs) { A(A&&): 10
a_ = rhs.a_; ~A()
printf("A& operator=(const A&): %d\n", a_); ~A()
return *this; ~A()
}

A& operator=(A&& rhs) noexcept {


a_ = rhs.a_;
printf("A& operator=(A&&): %d\n", a_);
return *this;
}

int a_ = 0;
};
Adding elements to vector 61

int main() { A(0)


constexpr int kTestSize = 2; A(A&&): 0
std::vector<A> vec; ~A()
[Link](kTestSize); A(1)
A(A&&): 1
for (int i = 0; i < kTestSize; ++i) { ~A()
vec.push_back(i); ~A()
} ~A()
}

int main() { A(0)


constexpr int kTestSize = 2; A(1)
std::vector<A> vec; ~A()
[Link](kTestSize); ~A()

for (int i = 0; i < kTestSize; ++i) {


vec.emplace_back(i);
}
}

For vector, emplace_back, allows in-place construction.

Use emplace_back instead of push_back


std::vector: Use emplace_back 62

int main() {
constexpr int kTestSize = 2;
std::vector<A> vec;
[Link](kTestSize);

for (int i = 0; i < kTestSize; ++i) {


vec.push_back(i);
}
}

When clang-tidy check is used:


--checks=modernize-use-emplace

Warning: use emplace_back instead of push_back [modernize-use-emplace]


vec.push_back(i);
^~~~~~~~~~
emplace_back(

This check also works for std::stack, std::queue, std::deque, std::forward_list, std::list, std::priority_queue.
Use emplace* functions for in-place construction 63

• std::deque: Use emplace_back/emplace_front instead of


push_back/push_front

• std::forward_list: Use emplace_after/emplace_front instead of


insert_after/push_front

• std::list: Use emplace_back/emplace_front/emplace instead of


push_back/push_front/insert

• std::stack/std::queue: Use emplace instead of push

• std::set: Use emplace instead of insert


64

Preventing Temporary Objects


in Associative Containers
6/8
65
For map, use emplace instead of operator[]
int main() {
std::map<std::string, int> m;
// This is insertion. It creates a string and moves that into place.
m["hello"] = 10;
}

int main() { A(10)


std::map<A, int> m; A(A&&): 10
m[10] = 10; ~A()
} ~A()

int main() { A(10) emplace does in-place construction.


std::map<A, int> m; ~A()
[Link](10, 10);
}

In this case the non-trivial object is the “key”.


66
For map, use emplace instead of operator[]
Let’s consider the case where the non-trivial object is value instead of key.

struct A final {
A(int a) : a_(a) { printf("A(%d)\n", a_); } int main() {
~A() { puts("~A()"); } std::map<int, A> m;
A(const A& rhs) : a_(rhs.a_) { printf("A(const A&): %d\n", a_); } m[10] = 10;
A(A&& rhs) noexcept : a_(rhs.a_) { printf("A(A&&): %d\n", a_); } }
A& operator=(const A& rhs) {
a_ = rhs.a_;
printf("A& operator=(const A&): %d\n", a_);
return *this;
}
A& operator=(A&& rhs) noexcept {
a_ = rhs.a_;
printf("A& operator=(A&&): %d\n", a_);
return *this;
}
int a_ = 0;
};
67
For map, use emplace instead of operator[]
Let’s consider the case where the non-trivial object is value instead of key.

struct A final {
A() { puts("A()"); }
A(int a) : a_(a) { printf("A(%d)\n", a_); }

// Special member functions.


};

int main() { A(10)


std::map<int, A> m; A()
// This code does not compile without the default A& operator=(A&&): 10
// constructor. ~A()
m[10] = 10; ~A()
}

int main() { A(10) Here emplace does in-


std::map<int, A> m; ~A()
place construction.
[Link](10, 10);
}

int main() { A(10) try_emplace also does


std::map<int, A> m; ~A()
m.try_emplace(10, 10);
the same for “value”
} object.
68
Special case for emplace versus try_emplace
Let’s consider the case where “non-trivial” type is the key and check behavior difference between emplace and try_emplace.

int main() { A(10)


std::map<A, int> m; A(A&&): 10
m[10] = 10; ~A()
} ~A()

int main() { A(10)


~A()
When key type is non-trivial object, then only emplace
std::map<A, int> m;
[Link](10, 10); allows in-place construction.
}

int main() { A(10)


std::map<A, int> m; A(A&&): 10
m.try_emplace(10, 10); ~A()
} ~A()

This paper (P2363) was accepted for C++26 and added template <typename K, typename... Args>
support for: std::pair<iterator, bool> try_emplace(K&& k, Args&&... args);

template <typename K>


mapped_type& operator[](K&& k);

This will allow try_emplace to behave same as emplace in this scenario and will also let operator[] to construct in-place.
69
emplace for constructors with multiple arguments
struct B final { std::map<B, B> m;
// Needed for operator[]. [Link](10, 10, 20, 20); // COMPILATION ERROR.
B() { std::cout << "B(): " << GetStr() << '\n'; }
B(int i, int j) : v_(i, j) { std::cout << "B(i, j): " << GetStr() << '\n'; }
~B() { puts("~B()"); } B(i, j): (10, 10)
B(const B& rhs) : v_(rhs.v_) { B(i, j): (20, 20)
std::cout << "B(const B&): " << GetStr() << '\n'; B(B&&): (10, 10)
}
int main() {
B(B&& rhs) noexcept : v_(std::move(rhs.v_)) { std::map<B, B> m; B(B&&): (20, 20)
std::cout << "B(B&&): " << GetStr() << '\n'; [Link](B{10, 10}, B{20, 20}); ~B()
} } ~B()
B& operator=(const B& rhs) { ~B()
v_ = rhs.v_; ~B()
std::cout << "B& operator=(const B&): " << GetStr() << '\n';
return *this;
} int main() { B(i, j): (10, 10)
B& operator=(B&& rhs) noexcept { std::map<B, B> m; B(i, j): (20, 20)
v_ = std::move(rhs.v_); [Link](std::piecewise_construct, ~B()
std::cout << "B& operator=(B&&): " << GetStr() << '\n';
return *this; std::forward_as_tuple(10, 10), ~B()
} std::forward_as_tuple(20, 20));
std::string GetStr() const { }
std::stringstream ss;
ss << "(" << v_.first << ", " << v_.second << ")"; This approach calls the std::piecewise_construct constructor of
return [Link]();
} std::pair and gets in-place construction.
auto operator<=>(const B&) const noexcept = default;

std::pair<int, int> v_;


}; int main() { B(i, j): (10, 10)
std::map<B, B> m; B(B&&): (10, 10)
m.try_emplace(B{10, 10}, 20, 20); B(i, j): (20, 20)
} ~B()
~B()
~B()
70
emplace/try_emplace for case of existing key.
int main() {
std::map<int, A> m;
std::cout << "===== Before emplace(10, 20) ====\n"; ===== Before emplace(10, 20) ====
[Link](10, 20); A(20)
std::cout << "===== Before emplace(10, 30) ====\n"; ===== Before emplace(10, 30) ====
[Link](10, 30); ===== Before try_emplace(10, 40) ====
std::cout << "===== Before try_emplace(10, 40) ====\n"; ===== After try_emplace(10, 40) ====
m.try_emplace(10, 40); ~A()
std::cout << "===== After try_emplace(10, 40) ====\n";
}

For an existing key, the value type object is not created. However, this is not
guaranteed by emplace specification. try_emplace guarantees that behavior.
71
insert_or_assign instead of operator[].
template <const char* name>
struct Type {
Type() { printf("%s(): (%d, %d)\n", name, i, j); }
Type(int i, int j) : i(i), j(j) {
printf("%s(i, j): (%d, %d)\n", name, i, j);
}
~Type() { printf("~%s()\n", name); }

Type(const Type& rhs) : i(rhs.i), j(rhs.j) { int main() { Key(): (0, 0)


printf("%s(const %s&): (%d, %d)\n", name, name, i, j); Key(i, j): (20, 20)
Key key;
}
Key key2(20, 20); Value(): (0, 0)
Type(Type&& rhs) noexcept : i(rhs.i), j(rhs.j) {
printf("%s(%s&&): (%d, %d)\n", name, name, i, j); Value val; Value(i, j): (30, 30)
} Value val2(30, 30); ~Value()
Type& operator=(const Type& rhs) { } ~Value()
i = rhs.i; ~Key()
j = rhs.j; ~Key()
printf("%s& operator=(const %s&): (%d, %d)\n", name, name, i, j);
return *this;
}
Type& operator=(Type&& rhs) noexcept {
i = std::move(rhs.i);
j = std::move(rhs.j);
printf("%s& operator=(%s&&): (%d, %d)\n", name, name, i, j);
return *this;
}
auto operator<=>(const Type&) const noexcept = default;
int i = 0, j = 0;
};
This creates two types, Key and
Value, so we can see when the map
static constexpr char kKey[] = "Key";
static constexpr char kValue[] = "Value"; key and value elements are created.
using Key = Type<kKey>;
using Value = Type<kValue>;
72
insert_or_assign instead of operator[].
int main() { Key(i, j): (10, 10)
std::map<Key, Value> m; ---- Before 1st[] ----
Key k{10, 10}; Value(i, j): (20, 20)
std::cout << "---- Before 1st[] ----\n"; Key(const Key&): (10, 10)
m[k] = Value{20, 20}; Value(): (0, 0)
std::cout << "---- Before 2nd[] ----\n"; Value& operator=(Value&&): (20, 20)
m[k] = Value{30, 30}; ~Value()
std::cout << "---- After [] ----\n"; ---- Before 2nd[] ----
} Value(i, j): (30, 30)
Value& operator=(Value&&): (30, 30)
~Value()
---- After [] ----
~Key()
~Value()
~Key()

int main() { Key(i, j): (10, 10)


std::map<Key, Value> m; ---- Before 1st ----
Using insert_or_assign
Key k{10, 10}; Value(i, j): (20, 20) improved the insertion case.
std::cout << "---- Before 1st ----\n"; Key(const Key&): (10, 10)
m.insert_or_assign(k, Value{20, 20}); Value(Value&&): (20, 20)
std::cout << "---- Before 2nd ----\n"; ~Value()
m.insert_or_assign(k, Value{30, 30}); ---- Before 2nd ---- If a single function does both
std::cout << "---- After both ----\n"; Value(i, j): (30, 30) “insert” and “assign” use
} Value& operator=(Value&&): (30, 30)
~Value() insert_or_assign instead of
---- After both ---- operator[].
~Key()
~Value()
~Key()
73

Using Transparent Comparators


to Avoid Extra Objects
7/8
std::set<std::string> 74

void* operator new(size_t n) {


void* p = malloc(n); We will use this overridden operator new / delete to “detect” string
printf("operator new: n: %zu, p = %p\n", n, p);
return p; creations.
}

void operator delete(void* p) noexcept { We will check the methods count, contains and find.
printf("operator delete: p = %p\n", p);
free(p);
}

int main() {
----- Using count -----
std::set<std::string> s;
operator new: n: 40, p = 0x5b138382f2b0
constexpr char kTestStr[] = "Hello, do you contain this string?";
operator delete: p = 0x5b138382f2b0
std::cout << "----- Using count -----\n";
Count: 0
----- Using contains -----
const auto n = [Link](kTestStr);
operator new: n: 40, p = 0x5b138382f2b0
operator delete: p = 0x5b138382f2b0
std::cout << "Count: " << n << std::endl;
contains: Not found
std::cout << "----- Using contains -----\n";
----- Using find -----
operator new: n: 40, p = 0x5b138382f2b0
const auto found = [Link](kTestStr);
operator delete: p = 0x5b138382f2b0
find: Not found
if (!found) {
std::cout << "contains: Not found\n";
}
std::cout << "----- Using find -----\n";

auto it = [Link](kTestStr);
if (it == [Link]()) {
std::cout << "find: Not found\n";
}
}
std::set<std::string> 75

int main() { operator new: n: 40, p = 0x57d53ad182a0


std::set<std::string> s; ----- Using count -----
const std::string test_str{"Hello, do you contain this string?"}; Count: 0
std::cout << "----- Using count -----\n"; ----- Using contains -----
contains: Not found
const auto n = [Link](test_str); ----- Using find -----
find: Not found
std::cout << "Count: " << n << std::endl; operator delete: p = 0x57d53ad182a0
std::cout << "----- Using contains -----\n";

const auto found = [Link](test_str);

if (!found) {
std::cout << "contains: Not found\n";
}
std::cout << "----- Using find -----\n";

auto it = [Link](test_str);
if (it == [Link]()) {
std::cout << "find: Not found\n";
}
}

As we see, if we don’t use const std::string&, the functions count, contains, find will create temporary objects.

This applies to all of const char*, const char [N] as argument.


std::set<std::string> 76

int main() { ----- Using count -----


std::set<std::string> s; operator new: n: 40, p = 0x5b138382f2b0
constexpr char kTestStr[] = "Hello, do you contain this string?"; operator delete: p = 0x5b138382f2b0
std::cout << "----- Using count -----\n"; Count: 0
----- Using contains -----
const auto n = [Link](kTestStr); operator new: n: 40, p = 0x5b138382f2b0
operator delete: p = 0x5b138382f2b0
std::cout << "Count: " << n << std::endl; contains: Not found
std::cout << "----- Using contains -----\n"; ----- Using find -----
operator new: n: 40, p = 0x5b138382f2b0
const auto found = [Link](kTestStr); operator delete: p = 0x5b138382f2b0
find: Not found
if (!found) {
std::cout << "contains: Not found\n";
}
std::cout << "----- Using find -----\n";

auto it = [Link](kTestStr);
if (it == [Link]()) {
std::cout << "find: Not found\n";
}
}
std::set<std::string>: transparent comparator 77

int main() { ----- Using count -----


std::set<std::string, std::less<>> s; Count: 0
constexpr char kTestStr[] = "Hello, do you contain this string?"; ----- Using contains -----
std::cout << "----- Using count -----\n"; contains: Not found
----- Using find -----
const auto n = [Link](kTestStr); find: Not found

std::cout << "Count: " << n << std::endl;


std::cout << "----- Using contains -----\n";

const auto found = [Link](kTestStr);

if (!found) {
std::cout << "contains: Not found\n";
}
std::cout << "----- Using find -----\n";

auto it = [Link](kTestStr);
if (it == [Link]()) {
std::cout << "find: Not found\n";
}
}
std::set<std::string>: transparent comparator 78

int main() { operator new: n: 40, p = 0x5cc0a549e2a0


std::set<std::string, std::less<>> s; ----- Using count -----
const std::string test_str{"Hello, do you contain this string?"}; Count: 0
std::cout << "----- Using count -----\n"; ----- Using contains -----
contains: Not found
const auto n = [Link](test_str); ----- Using find -----
find: Not found
std::cout << "Count: " << n << std::endl; operator delete: p = 0x5cc0a549e2a0
std::cout << "----- Using contains -----\n";

const auto found = [Link](test_str);

if (!found) {
std::cout << "contains: Not found\n";
}
std::cout << "----- Using find -----\n";

auto it = [Link](test_str);
if (it == [Link]()) {
std::cout << "find: Not found\n";
}
}

std::set<std::string> s;
Use std::less<> to get transparent comparison.

std::set<std::string, std::less<>> s;
Transparent comparator 79

Transparent comparators allow comparisons to happen without the need to create temporary objects in certain
scenarios.
They are identified by a typedef is_transparent.
Here's an example of std::less<void> specialization code from libc++ code.

template <>
struct _LIBCPP_TEMPLATE_VIS less<void> {
template <class _T1, class _T2>
_LIBCPP_CONSTEXPR_SINCE_CXX14 _LIBCPP_HIDE_FROM_ABI auto operator()(
_T1&& __t,
_T2&& __u) const
noexcept(noexcept(std::forward<_T1>(__t) < std::forward<_T2>(__u))) //
-> decltype(std::forward<_T1>(__t) < std::forward<_T2>(__u)) {
return std::forward<_T1>(__t) < std::forward<_T2>(__u);
}
typedef void is_transparent;
};

The default comparator for std::set<std::string> is std::less<Key> so std::less<std::string>.


Instead, using the transparent comparator std::less<void> allows for comparison with const char* and
const char [N] as well.
std::map<std::string, AnotherType> 80

int main() { ----- Using count -----


std::map<std::string, int> m; operator new: n: 40, p = 0x5f5b4b9c82b0
constexpr char kTestStr[] = "Hello, do you contain this string?"; operator delete: p = 0x5f5b4b9c82b0
std::cout << "----- Using count -----\n"; Count: 0
----- Using contains -----
const auto n = [Link](kTestStr); operator new: n: 40, p = 0x5f5b4b9c82b0
operator delete: p = 0x5f5b4b9c82b0
std::cout << "Count: " << n << std::endl; contains: Not found
std::cout << "----- Using contains -----\n"; ----- Using find -----
operator new: n: 40, p = 0x5f5b4b9c82b0
const auto found = [Link](kTestStr); operator delete: p = 0x5f5b4b9c82b0
find: Not found
if (!found) {
std::cout << "contains: Not found\n";
}
std::cout << "----- Using find -----\n";

auto it = [Link](kTestStr);
if (it == [Link]()) {
std::cout << "find: Not found\n";
}
}
std::map<std::string, AnotherType>: Transparent comparator 81

int main() { ----- Using count -----


std::map<std::string, int, std::less<>> m; Count: 0
constexpr char kTestStr[] = "Hello, do you contain this string?"; ----- Using contains -----
std::cout << "----- Using count -----\n"; contains: Not found
----- Using find -----
const auto n = [Link](kTestStr); find: Not found

std::cout << "Count: " << n << std::endl;


std::cout << "----- Using contains -----\n";

const auto found = [Link](kTestStr);

if (!found) {
std::cout << "contains: Not found\n";
}
std::cout << "----- Using find -----\n";

auto it = [Link](kTestStr);
if (it == [Link]()) {
std::cout << "find: Not found\n";
}
}

std::map<std::string, int> m;
Use std::less<> to get transparent comparison.

std::map<std::string, int, std::less<>> m;


std::unordered_set<std::string> 82

int main() { ----- Using count -----


std::unordered_set<std::string> s; operator new: n: 40, p = 0x5bbcb3a372b0
constexpr char kTestStr[] = "Hello, do you contain this string?"; operator delete: p = 0x5bbcb3a372b0
std::cout << "----- Using count -----\n"; Count: 0
----- Using contains -----
const auto n = [Link](kTestStr); operator new: n: 40, p = 0x5bbcb3a372b0
operator delete: p = 0x5bbcb3a372b0
std::cout << "Count: " << n << std::endl; contains: Not found
std::cout << "----- Using contains -----\n"; ----- Using find -----
operator new: n: 40, p = 0x5bbcb3a372b0
const auto found = [Link](kTestStr); operator delete: p = 0x5bbcb3a372b0
find: Not found
if (!found) {
std::cout << "contains: Not found\n";
}
std::cout << "----- Using find -----\n";

auto it = [Link](kTestStr);
if (it == [Link]()) {
std::cout << "find: Not found\n";
}
}
std::unordered_set<std::string>: Transparent comparator 83

int main() { ----- Using count -----


std::unordered_set<std::string, MyStringHash, std::equal_to<>> s; Count: 0
constexpr char kTestStr[] = "Hello, do you contain this string?"; ----- Using contains -----
std::cout << "----- Using count -----\n"; contains: Not found
----- Using find -----
const auto n = [Link](kTestStr); find: Not found

std::cout << "Count: " << n << std::endl;


std::cout << "----- Using contains -----\n"; struct MyStringHash final {
using hash_type = std::hash<std::string_view>;
const auto found = [Link](kTestStr); using is_transparent = void;

if (!found) { std::size_t operator()(const char* str) const {


std::cout << "contains: Not found\n"; return hash_type{}(str);
} }
std::cout << "----- Using find -----\n"; std::size_t operator()(std::string_view str) const {
return hash_type{}(str);
auto it = [Link](kTestStr); }
if (it == [Link]()) { std::size_t operator()(std::string const& str) const {
std::cout << "find: Not found\n"; return hash_type{}(str);
} }
} };

std::unordered_set<std::string> s;
Use MyStringHash and std::equal_to to get transparent comparison.

std::unordered_set<std::string, MyStringHash, std::equal_to<>> s;


std::unordered_map<std::string, AnotherType> 84

int main() { ----- Using count -----


std::unordered_map<std::string, int> m; operator new: n: 40, p = 0x596aeaadf2b0
constexpr char kTestStr[] = "Hello, do you contain this string?"; operator delete: p = 0x596aeaadf2b0
std::cout << "----- Using count -----\n"; Count: 0
----- Using contains -----
const auto n = [Link](kTestStr); operator new: n: 40, p = 0x596aeaadf2b0
operator delete: p = 0x596aeaadf2b0
std::cout << "Count: " << n << std::endl; contains: Not found
std::cout << "----- Using contains -----\n"; ----- Using find -----
operator new: n: 40, p = 0x596aeaadf2b0
const auto found = [Link](kTestStr); operator delete: p = 0x596aeaadf2b0
find: Not found
if (!found) {
std::cout << "contains: Not found\n";
}
std::cout << "----- Using find -----\n";

auto it = [Link](kTestStr);
if (it == [Link]()) {
std::cout << "find: Not found\n";
}
}
unordered_map<string, OtherType> : Transparent comparator 85

int main() { ----- Using count -----


std::unordered_map<std::string, int, MyStringHash, std::equal_to<>> m; Count: 0
constexpr char kTestStr[] = "Hello, do you contain this string?"; ----- Using contains -----
std::cout << "----- Using count -----\n"; contains: Not found
----- Using find -----
const auto n = [Link](kTestStr); find: Not found

std::cout << "Count: " << n << std::endl;


std::cout << "----- Using contains -----\n";

const auto found = [Link](kTestStr);

if (!found) {
std::cout << "contains: Not found\n";
}
std::cout << "----- Using find -----\n";

auto it = [Link](kTestStr);
if (it == [Link]()) {
std::cout << "find: Not found\n";
}
}

std::unordered_map<std::string, int> m;
Use MyStringHash and std::equal_to to get transparent comparison.

std::unordered_map<std::string, int, MyStringHash, std::equal_to<>> m;


Use transparent comparators for std::string for associative containers 86

std::set<std::string> s;

std::set<std::string, std::less<>> s;

std::map<std::string, int> m;

std::map<std::string, int, std::less<>> m;

std::unordered_set<std::string> s;

std::unordered_set<std::string, MyStringHash, std::equal_to<>> s;

std::unordered_map<std::string, int> m;

std::unordered_map<std::string, int, MyStringHash, std::equal_to<>> m;


87

Moving Data to Compile Time


8/8
88

Why move to compile time?


• Data is processed during compilation
• Objects don’t need to be constructed at runtime
89

Move “const” data structures to compile time if possible

Runtime const std::string / std::vector: Can be moved to compile time using std::string_view / std::array:
int main() { int main() {
const std::string str("hello"); static constexpr std::string_view kStr("hello");
const std::vector arr{1, 2, 3}; static constexpr char kStrArr[] = "hello";
} static constexpr int kCArr[] = {1, 2, 3};
static constexpr auto kArr = std::to_array({1, 2, 3});
}
90

Move “const” data structures to compile time if possible


Global const std::string / std::vector:
const std::string global_str("this is a really long string");
const std::vector global_arr{1, 2, 3};
-Wglobal-constructors -Wexit-time-destructors
int main() {} are included in -Weverything

When built with -Wglobal-constructors -Wexit-time-destructors


error: declaration requires an exit-time destructor [-Werror,-Wexit-time-destructors]
const std::string global_str("this is a really long string");
^
error: declaration requires a global destructor [-Werror,-Wglobal-constructors]
error: declaration requires an exit-time destructor [-Werror,-Wexit-time-destructors]
const std::vector global_arr{1, 2, 3};
^
error: declaration requires a global destructor [-Werror,-Wglobal-constructors]

These flags help in figuring out opportunities for moving global const objects to compile time.

constexpr std::string_view kStr("this is a really long string");


constexpr std::array kArr{1, 2, 3};

int main() {}
91

Move “const” data structures to compile time if possible


Global const std::string / std::vector:
const std::string global_str("this is a really long string");
const std::vector global_arr{1, 2, 3};

int main() {}

const std::string& GetStr() {


static const std::string global_str("this is a really long string");
return global_str;
}

const std::vector<int>& GetVec() {


static const std::vector global_arr{1, 2, 3};
return global_arr;
-Wexit-time-destructors also points out }
magic statics. int main() {}

error: declaration requires an exit-time destructor [-Werror,-Wexit-time-destructors]


static const std::string global_str("this is a really long string");
^
error: declaration requires an exit-time destructor [-Werror,-Wexit-time-destructors]
static const std::vector global_arr{1, 2, 3};
^
92

Move “const” data structures to compile time if possible


Transformation may need changes to usage interface.
// Header
const std::vector<int>& GetVec();

// Source file.
namespace { // Header
const std::vector global_arr{1, 2, 3}; std::span<const int> GetVec();
} // namespace
// Source file.
const std::vector<int>& GetVec() {
namespace {
return global_arr;
constexpr std::array kArr{1, 2, 3};
}
} // namespace

std::span<const int> GetVec() {


return kArr;
}
93
Moving User defined “const” data structures to compile time
// Header.
bool ContainsStr1(const std::string& str);

// Source file
namespace {
struct SomeClass {
std::string str1;
std::string str2;
int value;
};

const std::vector<SomeClass> global_arr{{"one", "two", 12}, {"three", "four", 34}};


} // namespace

bool ContainsStr1(const std::string& str) {


return std::ranges::any_of(
global_arr, [&str](const auto& str1) { return str1 == str; },
&SomeClass::str1);
}

error: declaration requires an exit-time destructor [-Werror,-Wexit-time-destructors]


const std::vector<SomeClass> global_arr{{"one", "two", 12}, {"three", "four", 34}};
^
error: declaration requires a global destructor [-Werror,-Wglobal-constructors]
94
Moving User defined “const” data structures to compile time
// Header.
bool ContainsStr1(const std::string& str);

// Source file
namespace {
struct SomeClass {
std::string str1;
std::string str2;
int value;
};

const std::vector<SomeClass>& GetArr() {


static const auto* global_arr = new std::vector<SomeClass>{{"one", "two", 12}, {"three", "four", 34}};
return *global_arr;
}
} // namespace

bool ContainsStr1(const std::string& str) {


return std::ranges::any_of(
GetArr(), [&str](const auto& str1) { return str1 == str; },
&SomeClass::str1);
}

absl::NoDestructor can also better “annotate” this “will not delete” scenario.
95
Moving User defined “const” data structures to compile time
// Header.
bool ContainsStr1(std::string_view str);

// Source file
namespace {
struct SomeClass {
const std::string_view str1;
const std::string_view str2;
const int value;
};

constexpr auto kArr =


std::to_array<SomeClass>({{"one", "two", 12}, {"three", "four", 34}});
} // namespace

bool ContainsStr1(std::string_view str) {


return std::ranges::any_of(
kArr, [str](auto str1) { return str1 == str; }, &SomeClass::str1);
}

In our code base, we have seen quite a few instances where user defined data
structures could be converted to compile time.
96

Concatenating string at compile time


#include "mystrcat.h" Hello, World!!

int main() {
constexpr std::string_view kHello = "Hello, "; This is a runtime string created which will always be the same.
constexpr std::string_view kWorld = "World!!";
const std::string result = MyStrCat({kHello, kWorld});
std::cout << result << std::endl;
}

MyCompileTimeStringJoiner can be used to do compile time concatenation.

#include "my_compile_time_string_joiner.h" Hello, World!!

int main() {
static constexpr std::string_view kHello = "Hello, ";
static constexpr std::string_view kWorld = "World!!";
static constexpr std::string_view kJoinResultStr =
MyCompileTimeStringJoinerV<kHello, kWorld>;
static_assert(kJoinResultStr == "Hello, World!!");
std::cout << kJoinResultStr << std::endl;
}
97

Concatenating string at compile time


#include "my_compile_time_string_joiner.h" template <std::string_view const&... Strs>
struct MyCompileTimeStringJoiner final {
int main() { // Join all strings into a single std::array of chars.
static constexpr std::string_view kHello = "Hello, "; static constexpr auto JoinImpl() noexcept {
static constexpr std::string_view kWorld = "World!!"; constexpr std::size_t len = ([Link]() + ... + 0);
static constexpr std::string_view kJoinResultStr =
MyCompileTimeStringJoinerV<kHello, kWorld>;
std::array<char, len + 1> arr{};
static_assert(kJoinResultStr == "Hello, World!!"); auto append = [i = 0, &arr](auto const s) mutable {
std::cout << kJoinResultStr << std::endl; for (auto c : s) {
} arr[i++] = c;
}
Hello, World!! };
(append(Strs), ...);
arr[len] = 0;
Copied from [Link]
return arr;
}
// Give the joined string static storage.
static constexpr auto kJoinedArray = JoinImpl();
// View as a std::string_view.
static constexpr std::string_view kJoinedArrayAsStringView = {
[Link](), [Link]() - 1};
Also check this C++ On Sea 2024 session by };
// Helper to get the value out.
Jason Turner for approaches to create compile template <std::string_view const&... Strs>
time strings. static constexpr auto MyCompileTimeStringJoinerV =
MyCompileTimeStringJoiner<Strs...>::kJoinedArrayAsStringView;
98
Global std::set, std::map: Can they move to compile time?
std::set<std::string, std::less<>> global_set{"one", "two", "three"};

std::map<std::string, int, std::less<>> global_map{{"one", 1}, {"two", 2}};

There is no standard compliant way to do this.

constexpr auto kSet =


base::MakeFixedFlatSet<std::string_view>({"one", "two", "three"});
constexpr auto kMap = base::MakeFixedFlatMap<std::string_view, int>(
{{"one", 1}, {"two", 2}, {"three", 3}});

Chromium has fixed_flat_map and fixed_flat_set which can be used to create compile time set / map equivalents.

std::flat_map and std::flat_set will be made constexpr in C++26. Once constexpr, some additional code can be
written to create instances at compile time.
99

Conclusion
100

Key Points
• We want in-place construction without copies or moves.
• Pass non-trivial objects by reference
• Use view types (std::string_view, std::span)
• Use in-place constructors for STL types
• Use emplace
• Use transparent comparators for std::string in associative
containers
• Move data to compile time
• Use clang-tidy checks and warnings
101

Thank you!
Special thanks to Chandranath Bhattacharyya!
102
References
• CppCon 2024: How to Use string_view in C++ - Basics, Benefits, and Best Practices -
Jasmine Lopez & Prithvi Okade.
• CppCon 2018: Jon Kalb “Copy Elision”.
• CppCon 2024: C++ RVO: Return Value Optimization for Performance in Bloomberg C++
Codebases - Michelle Fae D'Souza.
• CppCon 2017: Jorg Brown “The design of absl::StrCat...”
• Understanding The constexpr 2-Step - Jason Turner - C++ on Sea 2024.
• C++Now 2018: Jason Turner “Initializer Lists Are Broken, Let's Fix Them”.
• C++ Weekly - Ep 421 - You're Using optional, variant, pair, tuple, any, and expected
Wrong!
• Why is there no piecewise tuple construction?
• CppCon 2018: Andrei Alexandrescu “Expect the expected”.
• In-Place Construction for std::any, std::variant and std::optional: Bartlomiej Filipek
([Link]).
103
References
• is_transparent: How to search a C++ set with another type than its key: Jonathan
Boccara ([Link]).
• Overview of std::map’s Insertion / Emplacement Methods in C++17 - Fluent C++
• c++ - How to concatenate static strings at compile time? - Stack Overflow
• P2363R3: Extending associative containers with the remaining heterogeneous
overloads
• C++ Core Guidelines
• clang-tidy checks
• Diagnostic flags in Clang
• absl::StrCat
• absl::NoDestructor
• Chromium: fixed_flat_map.h, fixed_flat_set.h
Microsoft @ CppCon
Monday, Sept 15 Tuesday, Sept 16 Wednesday, Sept Thursday, Sept 18 Friday, Sept 19
17
Building Secure Applications: A What’s New for Visual Studio LLMs in the Trenches: MSVC C++ Dynamic Debugging: Reflection-based JSON in C++
Practical End-to-End Approach Code: Cmake Improvements and Boosting System How We Enabled Full at Gigabytes per Second
GitHub Copilot Agents Programming with AI Debuggability of Optimized
Code
Chandranath Bhattacharyya & Bharat Alexandra Kemper Ion Todirel Eric Brumer Daniel Lemire & Francisco Geiman
Kumar 09:00 – 10:00 14:00 – 15:00 14:00 – 14:30 Thiesen
16:45 – 17:45 09:00 – 10:00

What’s new in Visual Studio for C++ Performance Tips: It’s Dangerous to Go Alone: A Duck-Tape Chronicles:
C++ Developers in 2025 Cutting Down on Game Developer Tutorial Rust/C++ Interop
Unnecessary Objects
Augustin Popa & David Li Kathleen Baker & Prithvi Okade Michael Price Victor Ciura
14:00 – 15:00 15:15 – 16:15 16:45 – 17:45 13:30 – 14:30

Back to Basics: Code Review Connecting C++ Tools to AI


Agents Using the Model
Take our survey, win prizes
Context Protocol [Link]
Chandranath Bhattacharyya & Kathleen Ben McMorran
Baker 15:50 – 16:20
14:00 – 15:00 QR Code for
Welcome to v1.0 of the
survey goes
meta::[[verse]]! here
Inbal Levi
16:45 – 17:45
105

Questions?

[Link]
106

Appendix:
MyStrCat
StrCat: Another implementation 107

template <typename T>


auto GetLength(T&& elem) {
using Type = std::decay_t<T>;
constexpr auto IsCharPtr =
std::is_same_v<Type, char*> || std::is_same_v<Type, const char*>;
if constexpr (IsCharPtr) {
return strlen(elem);
} else {
return std::size(std::forward<T>(elem));
}
int main() {
}
char arr[] = "hello";
char* p_arr = arr;
template <typename T1, typename... Args>
const char* p_arr2 = " hey2 ";
requires std::is_constructible_v<std::string, T1&&> &&
const std::string str(" folks!!");
(std::is_constructible_v<std::string, Args &&> && ...)
std::cout << StrCat(p_arr, p_arr2,
std::string StrCat(T1&& first, Args&&... args) {
std::string_view{"world!!"},
// Determine final string length.
std::string{", how are"}, " you", str)
const auto final_size = GetLength(std::forward<T1>(first)) +
<< '\n';
(GetLength(std::forward<Args>(args)) + ... + 1u);
}
std::string ret;
[Link](final_size);
ret += std::forward<T1>(first);
hello hey2 world!!, how are you folks!!
((ret += std::forward<Args>(args)), ...);
return ret;
}
StrCat: Another implementation 108

template <typename>
using StringViewType = std::string_view;

template <typename... Args>


std::string StrCatImpl(StringViewType<Args>... args) {
// Determine final string length.
const auto final_size = ([Link]() + ... + 1u);
std::string ret;
[Link](final_size); int main() {
((ret += args), ...); char arr[] = "hello";
return ret; char* p_arr = arr;
} const char* p_arr2 = " hey2 ";
const std::string str(" folks!!");
template <typename... Args> std::cout << StrCat(p_arr, p_arr2,
auto StrCat(Args&&... args) std::string_view{"world!!"},
-> decltype(StrCatImpl<Args...>(std::forward<Args>(args)...)) { std::string{", how are"}, " you", str)
return StrCatImpl<Args...>(std::forward<Args>(args)...); << '\n';
} }

hello hey2 world!!, how are you folks!!


109

Appendix:
Transparent Comparators
User Defined Types
Transparent comparator for user-defined types 110

struct A final {
A(int a) : a_(a) { printf("A(%d)\n", a_); }
~A() { puts("~A()"); }

A(const A& rhs) : a_(rhs.a_) { printf("A(const A&): %d\n", a_); }


A(A&& rhs) noexcept : a_(rhs.a_) { printf("A(A&&): %d\n", a_); }

A& operator=(const A& rhs) {


a_ = rhs.a_; This is needed for code to compile for A to be used
printf("A& operator=(const A&): %d\n", a_); as key in set: std::set<A>
return *this;
}

A& operator=(A&& rhs) noexcept {


a_ = rhs.a_;
printf("A& operator=(A&&): %d\n", a_);
return *this;
}

auto operator<=>(const A&) const noexcept = default;


int a_ = 0;
};
std::set<UserType> 111

int main() { A(20)


std::set<A> s; ------ Using count ------
[Link](20); A(10)
std::cout << "------ Using count ------\n"; ~A()
Count: 0
const auto n = [Link](10);
------ Using contains ------
std::cout << "Count: " << n << '\n'; A(10)
std::cout << "------ Using contains ------\n"; ~A()
contains: Not found
const bool found = [Link](10); ------ Using find ------
A(10)
if (!found) { ~A()
std::cout << "contains: Not found\n"; Not Found
} ------ After find ------
std::cout << "------ Using find ------\n"; ~A()
auto it = [Link](10);
if (it == [Link]()) {

}
std::cout << "Not Found\n";
We will check the methods count, contains
std::cout << "------ After find ------\n"; and find.
}
std::set<UserType>: Transparent comparator 112

int main() { A(20)


std::set<A, std::less<>> s; ------ Using count ------
[Link](20); A(10)
std::cout << "------ Using count ------\n"; ~A()
Count: 0
const auto n = [Link](10);
------ Using contains ------
std::cout << "Count: " << n << '\n'; A(10)
std::cout << "------ Using contains ------\n"; ~A()
A(10)
const bool found = [Link](10); ~A()
contains: Not found
if (!found) { ------ Using find ------
std::cout << "contains: Not found\n"; A(10)
} ~A()
std::cout << "------ Using find ------\n"; A(10)
~A()
auto it = [Link](10);
Not Found
if (it == [Link]()) {
std::cout << "Not Found\n"; ------ After find ------
} ~A()
std::cout << "------ After find ------\n";
}
More objects are getting created with
transparent comparator.
std::set<UserType>: Transparent comparator 113

struct A final {
A(int a) : a_(a) { printf("A(%d)\n", a_); }
~A() { puts("~A()"); }

A(const A& rhs) : a_(rhs.a_) { printf("A(const A&): %d\n", a_); }


A(A&& rhs) noexcept : a_(rhs.a_) { printf("A(A&&): %d\n", a_); }

A& operator=(const A& rhs) {


a_ = rhs.a_;
printf("A& operator=(const A&): %d\n", a_);
return *this;
}

A& operator=(A&& rhs) noexcept {


a_ = rhs.a_;
printf("A& operator=(A&&): %d\n", a_);
return *this;
}

auto operator<=>(const A&) const noexcept = default; This allows A to compare with int.
auto operator<=>(int i) const noexcept { return a_ <=> i; }

int a_ = 0;
};
std::set<UserType>: Transparent comparator 114

int main() { A(20)


std::set<A, std::less<>> s; ------ Using count ------
[Link](20); A(10)
std::cout << "------ Using count ------\n"; ~A()
Count: 0
const auto n = [Link](10);
------ Using contains ------
std::cout << "Count: " << n << '\n'; A(10)
std::cout << "------ Using contains ------\n"; ~A()
A(10)
const bool found = [Link](10); ~A()
contains: Not found
if (!found) { ------ Using find ------
std::cout << "contains: Not found\n"; A(10)
} ~A()
std::cout << "------ Using find ------\n"; A(10)
~A()
auto it = [Link](10);
Not Found
if (it == [Link]()) {
std::cout << "Not Found\n"; ------ After find ------
} ~A()
std::cout << "------ After find ------\n";
}
More objects are getting created with
transparent comparator.
std::set<UserType>: Transparent comparator 115

int main() { A(20)


std::set<A, std::less<>> s; ------ Using count ------
[Link](20); Count: 0
std::cout << "------ Using count ------\n"; ------ Using contains ------
contains: Not found
const auto n = [Link](10);
------ Using find ------
std::cout << "Count: " << n << '\n'; Not Found
std::cout << "------ Using contains ------\n"; ------ After find ------
~A()
const bool found = [Link](10);

if (!found) {
std::cout << "contains: Not found\n";
}
std::cout << "------ Using find ------\n";

auto it = [Link](10);
if (it == [Link]()) {
std::cout << "Not Found\n";
}
std::cout << "------ After find ------\n";
}

With the operator <=> (int):


struct A final {
// << snipped >>
auto operator<=>(const A&) const noexcept = default;

auto operator<=>(int i) const noexcept { return a_ <=> i; }


// << snipped >>
};
std::map<UserType, T> 116

int main() { A(20)


std::map<A, int> m; ------ Using count ------
[Link](20, 20); A(10)
std::cout << "------ Using count ------\n"; ~A()
Count: 0
const auto n = [Link](10);
------ Using contains ------
std::cout << "Count: " << n << '\n'; A(10)
std::cout << "------ Using contains ------\n"; ~A()
contains: Not found
const bool found = [Link](10); ------ Using find ------
A(10)
if (!found) { ~A()
std::cout << "contains: Not found\n"; Not Found
} ------ After find ------
std::cout << "------ Using find ------\n"; ~A()
auto it = [Link](10);
if (it == [Link]()) {
std::cout << "Not Found\n";
}
std::cout << "------ After find ------\n";
}
std::map<UserType, T>: Transparent comparator 117

int main() { A(20)


std::map<A, int, std::less<>> m; ------ Using count ------
[Link](20, 20); Count: 0
std::cout << "------ Using count ------\n"; ------ Using contains ------
contains: Not found
const auto n = [Link](10);
------ Using find ------
std::cout << "Count: " << n << '\n'; Not Found
std::cout << "------ Using contains ------\n"; ------ After find ------
~A()
const bool found = [Link](10);

if (!found) {
std::cout << "contains: Not found\n";
}
std::cout << "------ Using find ------\n";

auto it = [Link](10);
if (it == [Link]()) {
std::cout << "Not Found\n";
}
std::cout << "------ After find ------\n";
}
std::unordered_set<UserType> 118

struct A final {
A(int a) : a_(a) { printf("A(%d)\n", a_); }
~A() { puts("~A()"); }

A(const A& rhs) : a_(rhs.a_) { printf("A(const A&): %d\n", a_); }


A(A&& rhs) noexcept : a_(rhs.a_) { printf("A(A&&): %d\n", a_); }

A& operator=(const A& rhs) {


a_ = rhs.a_;
printf("A& operator=(const A&): %d\n", a_);
return *this;
}

A& operator=(A&& rhs) noexcept {


a_ = rhs.a_;
printf("A& operator=(A&&): %d\n", a_);
return *this;
}

bool operator==(const A&) const noexcept = default; These are necessary for code to compile for A
int a_ = 0; to be used as key in unordered_set:
}; std::unordered_set<A>
template <>
struct std::hash<A> {
std::size_t operator()(const A& a) const noexcept {
return std::hash<int>{}(a.a_);
}
};
std::unordered_set<UserType> 119

int main() { A(20)


std::unordered_set<A> s; ------ Using count ------
[Link](20); A(10)
std::cout << "------ Using count ------\n"; ~A()
Count: 0
const auto n = [Link](10);
------ Using contains ------
std::cout << "Count: " << n << '\n'; A(10)
std::cout << "------ Using contains ------\n"; ~A()
contains: Not found
const bool found = [Link](10); ------ Using find ------
A(10)
if (!found) { ~A()
std::cout << "contains: Not found\n"; Not Found
} ------ After find ------
std::cout << "------ Using find ------\n"; ~A()
auto it = [Link](10);
if (it == [Link]()) {
std::cout << "Not Found\n";
}
std::cout << "------ After find ------\n";
}
std::unordered_set<UserType>: Transparent comparator 120

int main() { A(20)


std::unordered_set<A, std::hash<A>, std::equal_to<>> s; ------ Using count ------
[Link](20); A(10)
std::cout << "------ Using count ------\n"; ~A()
Count: 0
const auto n = [Link](10);
------ Using contains ------
std::cout << "Count: " << n << '\n'; A(10)
std::cout << "------ Using contains ------\n"; ~A()
contains: Not found
const bool found = [Link](10); ------ Using find ------
A(10)
if (!found) { ~A()
std::cout << "contains: Not found\n"; Not Found
} ------ After find ------
std::cout << "------ Using find ------\n"; ~A()
auto it = [Link](10);
if (it == [Link]()) {
std::cout << "Not Found\n";
}
std::cout << "------ After find ------\n";
}
std::unordered_set<UserType>: Transparent comparator 121

struct A final { template <>


A(int a) : a_(a) { printf("A(%d)\n", a_); } struct std::hash<A> {
~A() { puts("~A()"); } std::size_t operator()(const A& a) const noexcept {
return std::hash<int>{}(a.a_);
A(const A& rhs) : a_(rhs.a_) { printf("A(const A&): %d\n", a_); } }
A(A&& rhs) noexcept : a_(rhs.a_) { printf("A(A&&): %d\n", a_); } };

A& operator=(const A& rhs) {


a_ = rhs.a_;
printf("A& operator=(const A&): %d\n", a_);
return *this;
}

A& operator=(A&& rhs) noexcept {


a_ = rhs.a_;
printf("A& operator=(A&&): %d\n", a_);
return *this;
}

bool operator==(const A&) const noexcept = default;

int a_ = 0;
};
std::unordered_set<UserType>: Transparent comparator 122

struct A final { template <>


A(int a) : a_(a) { printf("A(%d)\n", a_); } struct std::hash<A> {
~A() { puts("~A()"); } std::size_t operator()(const A& a) const noexcept {
return std::hash<int>{}(a.a_);
A(const A& rhs) : a_(rhs.a_) { printf("A(const A&): %d\n", a_); } }
A(A&& rhs) noexcept : a_(rhs.a_) { printf("A(A&&): %d\n", a_); } std::size_t operator()(int i) const noexcept {
return std::hash<int>{}(i);
A& operator=(const A& rhs) { }
a_ = rhs.a_; using is_transparent = void;
printf("A& operator=(const A&): %d\n", a_); };
return *this;
}

A& operator=(A&& rhs) noexcept { These are needed to ensure “transparent operators”
a_ = rhs.a_; work properly for user defined types as “key” of
printf("A& operator=(A&&): %d\n", a_);
return *this; std::unordered_set.
}

bool operator==(const A&) const noexcept = default;

bool operator==(int i) const noexcept { return a_ == i; }

int a_ = 0;
};
std::unordered_set<UserType>: Transparent comparator 123

int main() { A(20)


std::unordered_set<A, std::hash<A>, std::equal_to<>> s; ------ Using count ------
[Link](20); A(10)
std::cout << "------ Using count ------\n"; ~A()
Count: 0
const auto n = [Link](10);
------ Using contains ------
std::cout << "Count: " << n << '\n'; A(10)
std::cout << "------ Using contains ------\n"; ~A()
contains: Not found
const bool found = [Link](10); ------ Using find ------
A(10)
if (!found) { ~A()
std::cout << "contains: Not found\n"; Not Found
} ------ After find ------
std::cout << "------ Using find ------\n"; ~A()
auto it = [Link](10);
if (it == [Link]()) {
std::cout << "Not Found\n";
}
std::cout << "------ After find ------\n";
}
std::unordered_set<UserType>: Transparent comparator 124

int main() { A(20)


std::unordered_set<A, std::hash<A>, std::equal_to<>> s; ------ Using count ------
[Link](20); Count: 0
std::cout << "------ Using count ------\n"; ------ Using contains ------
contains: Not found
const auto n = [Link](10);
------ Using find ------
std::cout << "Count: " << n << '\n'; Not Found
std::cout << "------ Using contains ------\n"; ------ After find ------
~A()
const bool found = [Link](10);

if (!found) {
std::cout << "contains: Not found\n";
}
std::cout << "------ Using find ------\n";

auto it = [Link](10);
if (it == [Link]()) {
std::cout << "Not Found\n";
}
std::cout << "------ After find ------\n"; template <>
}
struct std::hash<A> {
std::size_t operator()(const A& a) const noexcept {
struct A final { return std::hash<int>{}(a.a_);
// << snipped >> }
bool operator==(const A&) const noexcept = default; std::size_t operator()(int i) const noexcept {
return std::hash<int>{}(i);
bool operator==(int i) const noexcept { return a_ == i; } }
// << snipped >> using is_transparent = void;
}; };
std::unordered_map<UserType, T> 125

int main() { A(20)


std::unordered_map<A, int> m; ------ Using count ------
[Link](20, 20); A(10)
std::cout << "------ Using count ------\n"; ~A()
Count: 0
const auto n = [Link](10); ------ Using contains ------
A(10)
std::cout << "Count: " << n << '\n'; ~A()
std::cout << "------ Using contains ------\n"; contains: Not found
------ Using find ------
const bool found = [Link](10); A(10)
~A()
if (!found) { Not Found
std::cout << "contains: Not found\n"; ------ After find ------
} ~A()
std::cout << "------ Using find ------\n";

auto it = [Link](10);
if (it == [Link]()) {
std::cout << "Not Found\n";
}
std::cout << "------ After find ------\n";
}
std::unordered_map<UserType, T>: Transparent comparator 126

int main() { A(20)


std::unordered_map<A, int, std::hash<A>, std::equal_to<>> m; ------ Using count ------
[Link](20, 20); Count: 0
std::cout << "------ Using count ------\n"; ------ Using contains ------
contains: Not found
const auto n = [Link](10); ------ Using find ------
Not Found
std::cout << "Count: " << n << '\n'; ------ After find ------
std::cout << "------ Using contains ------\n"; ~A()

const bool found = [Link](10);

if (!found) {
std::cout << "contains: Not found\n";
}
std::cout << "------ Using find ------\n";

auto it = [Link](10);
if (it == [Link]()) {
std::cout << "Not Found\n";
}
std::cout << "------ After find ------\n";
}
127

Appendix:
Compile Time set/map
Compile time set 128

template <typename Range, typename Comp = std::less<>> int main() {


constexpr bool IsSortedAndUnique(const Range& range) { static constexpr CompileTimeMap kCompMap(
return std::ranges::adjacent_find(range, std::not_fn(Comp{})) == {std::pair<int, int>{3, 3}, std::pair<int, int>{1, 2}});
std::ranges::end(range); for (const auto& [key, value] : [Link]()) {
} std::cout << '(' << key << ", " << value << ") ";
}
void TriggerCompileError(std::string_view); std::cout << '\n';
for (const auto i : {1, 2, 4}) {
template <typename Key, typename Value, size_t N> const auto found = [Link](i);
class CompileTimeMap { std::cout << i << (found ? " found\n" : " not found\n");
public: }
using PairType = std::pair<Key, Value>; }
constexpr CompileTimeMap(std::array<PairType, N>&& data)
: data_(std::move(data)) {
std::ranges::sort(data_); (1, 2) (3, 3)
if (!IsSortedAndUnique(data_)) { 1 found
TriggerCompileError("Non-unique"); 2 not found
} 4 not found
}
constexpr CompileTimeMap(PairType (&&arr)[N])
: data_(std::to_array(std::move(arr))) {
std::ranges::sort(data_);
if (!IsSortedAndUnique(data_)) {
TriggerCompileError("Non-unique");
}
}

constexpr bool Contains(const Key& elem) const {


return std::ranges::binary_search(data_, elem, std::less<>{},
&PairType::first);
}

constexpr std::span<const PairType> AsSpan() const { return data_; }

private:
std::array<PairType, N> data_;
};
Compile time map constexpr std::optional<Value> GetValue(const Key& elem) const {
const auto [start, end] =
129

template <typename Range, typename Comp = std::less<>> std::ranges::equal_range(data_, elem, std::less<>{},


constexpr bool IsSortedAndUnique(const Range& range) { &PairType::first);
return std::ranges::adjacent_find(range, std::not_fn(Comp{})) == if (start == end) {
std::ranges::end(range); // Not found.
} return std::nullopt;
}
void TriggerCompileError(std::string_view); return start->second;
}
template <typename Key, typename Value, size_t N>
class CompileTimeMap { constexpr std::span<const PairType> AsSpan() const { return data_; }
public:
using PairType = std::pair<Key, Value>; private:
constexpr CompileTimeMap(std::array<PairType, N>&& data) std::array<PairType, N> data_;
: data_(std::move(data)) { };
std::ranges::sort(data_);
if (!IsSortedAndUnique(data_)) {
int main() { (1, 2) (3, 3)
TriggerCompileError("Non-unique");
static constexpr CompileTimeMap kCompMap( 1 found
} {std::pair<int, int>{3, 3}, std::pair<int, int>{1, 2}}); 2 not found
} for (const auto& [key, value] : [Link]()) { 4 not found
constexpr CompileTimeMap(PairType (&&arr)[N]) std::cout << '(' << key << ", " << value << ") "; 1 => 2
: data_(std::to_array(std::move(arr))) { } 2 not found
std::ranges::sort(data_); std::cout << '\n'; 4 not found
if (!IsSortedAndUnique(data_)) { for (const auto i : {1, 2, 4}) { 3 => 3
TriggerCompileError("Non-unique"); const auto found = [Link](i);
} std::cout << i << (found ? " found\n" : " not found\n");
}
}
for (const auto i : {1, 2, 4, 3}) {
const auto value_opt = [Link](i);
constexpr bool Contains(const Key& elem) const { if (!value_opt) {
return std::ranges::binary_search(data_, elem, std::less<>{}, std::cout << i << " not found\n";
&PairType::first); } else {
} std::cout << i << " => " << *value_opt << '\n';
}
}
}
Compile time map 130

template <typename Key, typename Value, size_t N>


consteval auto MakeCompMap(std::pair<Key, Value> (&&arr)[N]) { void TestStringIntMap() {
return CompileTimeMap(std::move(arr)); static constexpr auto kCompMap =
} MakeCompMap<std::string_view, int>({{"three", 3}, {"one", 2}});
for (const auto& [key, value] : [Link]()) {
std::cout << '(' << key << ", " << value << ") ";
void TestIntIntMap() { }
static constexpr auto kCompMap = MakeCompMap<int, int>({{3, 3}, {1, 2}}); std::cout << '\n';
for (const auto& [key, value] : [Link]()) { for (const auto i : {"four", "five", "three"}) {
std::cout << '(' << key << ", " << value << ") "; const auto found = [Link](i);
} std::cout << i << (found ? " found\n" : " not found\n");
std::cout << '\n'; }
for (const auto i : {1, 2, 4}) { for (const auto i : {"four", "five", "three"}) {
const auto found = [Link](i); const auto value_opt = [Link](i);
std::cout << i << (found ? " found\n" : " not found\n"); if (!value_opt) {
} std::cout << i << " not found\n";
for (const auto i : {1, 2, 4, 3}) { } else {
const auto value_opt = [Link](i); std::cout << i << " => " << *value_opt << '\n';
if (!value_opt) { }
std::cout << i << " not found\n"; }
} else { }
std::cout << i << " => " << *value_opt << '\n';
}
} (1, 2) (3, 3)
int main() {
} 1 found
TestIntIntMap();
TestStringIntMap(); 2 not found
} 4 not found
1 => 2
2 not found
4 not found
3 => 3
(one, 2) (three, 3)
four not found
five not found
three found
four not found
five not found
three => 3
131

Appendix:
-Wlarge-by-value-copy
-Wlarge-by-value-copy 132

#include <iostream>
#include <string>
When compiled with -Wlarge-by-value-
struct A { copy=24
int a, b, c, d; This flags sizes > 24.
int e, f, g;
};
error: '' is a large (28 bytes) pass-by-value
struct B { argument; pass it by reference instead ? [-Werror,-
std::string a; Wlarge-by-value-copy]
std::string b; void Foo(A) {}
}; ^

void Foo(A) {}
It only catches PODs, so does not catch
void Foo(B) {} Foo(B).
int main() {
std::cout << "sizeof(A): " << sizeof(A) << '\n'; // 28 It is “not” on by default.
std::cout << "sizeof(B): " << sizeof(B) << '\n'; // 48
std::cout << "sizeof(std::string): " << sizeof(std::string) << '\n'; // 24
std::cout << "sizeof(std::string_view): " << sizeof(std::string_view)
<< '\n'; // 16
}

You might also like