cpp
Notes on Cherno C++ Tutorial
Notice
- If an object have pointer variable inside, write a copy constructor and use it.
- Default float type is
double
, usefloat a = 5.5f
. - Header or inline? (the later copys whole function body into the area where the function is called).
Assembly
1 | int main(int const argc, char const* argv[]) { |
1 | cmp edi, 5 // Compare argc to 5 |
How C++ Works
- A translation unit consists of a single
.cpp
file and its associated.h
files. - Compile Process Path: Source→Compile→Linker→Executables
-
Pre-process
Compiler parses all the macros (such as#include
) in source file.- Clang:
clang -x c++ -E hello.cpp -o hello.i
- GCC:
cpp hello.cpp > hello.i
- VS2015:
Project Settings
->Preprocessor
->Preprocess to a File
- Clang:
-
Compile & Assembly
- Clang:
clang++ -x c++-cpp-output -S hello.i -o hello.o
- GCC:
g++ -S hello.i && as hello.s -o main.o
- VS2015: Compile Only (
<C-F7>
)
- Clang:
-
Linker
Externally defined functions will be integrated in the link phase. Function declarations that never be called will be optimized away.The parameter of
ld
are platform specific (mainly depends on Clang/GCC version). Enable verbose to get the parameter of thecollect2
(which is an alias ofld
):
For Clang:1
2
3
4# Only run preprocess, compile, and assemble steps
clang++ -v -c hello.cpp -o hello.o
# For GCC:
g++ -v -c hello.cpp -o hello.oLd may looks messy, but you can significantly shorten that link line by removing some arguments. Here’s the minimal set I came up after some experimentations:
1
2
3
4
5
6
7
8ld -I/lib64/ld-linux-x86-64.so.2 \
-o hello \
/usr/lib64/Scrt1.o \
-L/usr/lib64/gcc/x86_64-pc-linux-gnu/14.2.1 \
hello.o \
-l stdc++ -l m -l gcc_s -l gcc -l c \
/sbin/../lib64/gcc/x86_64-pc-linux-gnu/14.2.1/crtendS.o \
/usr/lib64/crtn.o
Primitive Types (Fundamental Type)
- The difference between C++ data types are simply different allocated memory size.
- You can use
sizeof()
to see the data type size. - Trivial type: either a primitive type or a type composed of only trivial types.
trivial type can be copied and moved withmemcpy
,memmove
and constructed destructed without doing anything. Can be checked usingstd::is_trivial<Type>()
.
Different memory allocation size for C++ Data Type:
char |
1 byte |
short |
2 bytes |
int |
4 bytes |
long |
8 bytes (c++20 ), >= 4 bytes |
long long |
8 bytes |
float |
4 bytes |
double |
8 bytes |
void* |
8 bytes (64 bits), 4 bytes (32 bits) |
Functions
- Write formal parameters with
const
if possible. - Function is a function not in a class. whereas method is a function in a class.
- Don’t frequently divide your code into functions, calling a function requires creating an entire stack frame. This means we have to push parameters and so on into the stack, and also pull something called the return address from the stack, so that after the function executed the
PC
(Program counter) register could return to the address before the function call.
Conclusion: Jumping around in memory to execute function instructions comsumes additional time.
Header Files
Duplicate inclusion: You include header file b.h
in a.cpp
, but b.h
includes another header file c.h
, while you have already include c.h
before in a.cpp
)
Two ways:
#pragma once
-
Log.h 1
2
3
4
5
6
// some sentence...
Visual Studio Setup & Debug
-
Use
Show All Files
view underSolution Explorer
. -
By default VS2015 put intermediate files in debug directory
-
It’s recommand to set
Output Directory
into$(SolutionDir)bin\$(Platform)\$(Configuration)\
and setIntermediate Directory
into$(SolutionDir)bin\intermediate\$(Platform)\$(Configuration)\
-
The
watch view
in VS2015 allows you to specify the variables to be monitored,
Inmemory window
you can search by keyword&a
to show the address of variablea
-
The default value of uninitialized variables is
0xcccccccc
Pointers
- The pointer represents a memory address, generally the data type of the pointer is used to represent the type of the data at the target address.
- Pointer to Pointer:In memory, it may like this:
1
2
3
4
5
6
7
int main() {
char* buf{new char[8]}; // Allocate a space with 8 chars
std::memset(buf, 0, 8); // Fill it with zero,
char** ptr_to_ptr{&buf}; // pointer to pointer
delete[] buf; // Finally release the memory space.
}1
2
30x00B6FED4 &buf: 00 D0 FF F0
0x00D0FFF0 *(new char[8]): 00 00 00 00 00 00 00 00
0x???????? &ptr_to_ptr: 00 B6 FE D4Due to x86’s little endian design, what we see from
Memory View
is start from lower bits to higer bits, which is reversed from human’s convient that write the most significant digit first. e.g.:1
20x0 0x1 0x2 0x3
0x00B6FED4 (&buf): F0 FF D0 00
Reference
- Variables are convert to memory address in assembly by compiler. Reference let compiler just substitute memory address as is (like a macro) using it’s copy operator (
=()
), so it is different toconst void*
with creates a memory area to store a memory address (Can beNULL
). - Copy operator
=()
copies value of a variable. Send the actual parameters to a function implicitly calls copy operator. - Dereference operator (aka. indirection operator)
*()
get the value from a memory address. - Address-of operator
&()
obtain the memory address of a variable. Since directly send variable name to=()
only get its stored values.
Classes vs Structs, and Enums
They are no different in low level, so a class
can inherit a struct
(Not recommand).
sturct
is more suitable for store multiple variables, its variables are default have attribute public
, therefore it’s convient to express a data structure. While class
is more suitable for express object, which has both member variables and methods.
Struct Bit-fields
1 | struct S { |
Enum is a way to define a set of distinct values that have underlying integer types (, see below).
1 |
|
using enum
to create alias forenum
Static
- Static functions and variables outside of class limit the scope in its translation unit (like
private
in a class). But define static functions or variables that share a same name in different translation unit will cause duplicate definition error in linking stage. - Static variable inside a class or struct means that variable is going to share memory with all of the instances of the class, in other words there’s only one instance of that static variable. Static methods cannot access non-static members its class, hence it don’t have instance of the class.
- Define
static
variables in header files if possible. - Local Static
1
2
3
4
5
6
7
void func() {
static int s_i{}; // 's_' means static
s_i++;
std::cout << s_i << '\n';
}
int main() { func(); func(); func(); func(); }
Classical Singleton
Singleton are classes that allow only one instance.
- One way:
1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton {
private:
static Singleton* s_instance;
Singleton() {}; // Private empty construct function prevents instantiate
public:
static Singleton& get() { return *s_instance; };
void hello() { std::cout << "Hello" << '\n'; };
};
// Static members in class shoud defined out-of-line, but here I given a nullptr to pass static analyze
Singleton* Singleton::s_instance{nullptr};
// Though no memory spaces created for the class, we can still access methods
int main() { Singleton::get().hello(); } - Another way:
1
2
3
4
5
6
7
8
9
10
11
12
class Singleton {
private:
Singleton() {};
public:
static Singleton& get() {
static Singleton s_instance; // Put instance into a local static variable (Pro: less code)
return s_instance;
};
void hello() { std::cout << "Hello" << '\n'; };
};
int main() { Singleton::get().hello(); }
Constructors & Destructors
Corstructor provides a way to initialize primitive types when creating instance. Otherwise you have to do it manually or they will be keep to whatever is left over in that memory.
To Prevent creating instance, you can set constructor as priavate, or delete it.
1 | class Log { |
Destructor provides a way to gently recycle memory when delete
an object.
It can be called directly: SomeClass.~SomeClass();
Inheritance & Virtual Functions
- Size of derived class (aka. sub class): (base class) + (defined variables)
- Derived class implicitly call base class’s constructor
Why we need virtual function:
1 |
|
Outputs:
1 | Entity |
The problem occurs that the second output should be “Cherno”. When the pointer type is the main class Entity
, the method get_name()
uses it’s main class’ version even it’s actually an instance of Player
, this definitely a problem.
If you want to override a method you have to mark the method in the base class as virtual
. Correct version:
1 |
|
virtual
could reduce “dynamic dispatch” (change object’s vtable in runtime).
virtual
has its own overhead, it needs extra Vtable space, in order to dispatch the correct method it includes a member pointer in the base class that points to the vtable. And every time we call virtual method, we go through that table to decision which method to map.
Through the extra overhead it’s still recommand to use as much as possible.
Interface (Pure Virtual Method)
1 |
|
Visibility in C++
- The default visibility of a Class would be
private
. If it’s astruct
then it would bepublic
by default. private
things only visiable in it’s own class, nor in sub class, except friend class.protected
things can be seen by sub class.
Literal Arrays & C++11 Standard Arrays
Be aware for operations out of index (e.g.
example[-1] = 0
), in C++ you can do this by force the compiler to ignore this check, and it’s definitely discouraged.
- Literal Array
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int main() {
int arr[5]; // On stack, destory on function end
int* arr2{new int[5]}; // on heap, would not auto destory
delete[] arr2; // Use square brackets to release an array on heap
int* ptr{arr}; // 'arr' is actually a pointer which stores the begin address of the array
for (int i = 0; i< 5; i++) arr[i] = i;
arr[2] = 5;
*(ptr + 2) = 5; // this is equal but in a pretty wild way
// Bacause 'ptr' has type 'int' (32 bits, 4 bytes),
// so + 2 let it advance two 'int's length (64 bits, 8 bytes)
// We can also do the same operation in this form (`char` is 8 bits, 1 bytes)
*(int*) ((char*) ptr + 8) = 5;
for (int i = 0; i< 5; i++) std::cout << arr[i] << '\n';
// You cannot dynamically chack a raw array's size.
int count{sizeof(arr) / sizeof(int)}; // Ways like this is unstable
// A good ways is to use an constant to remember the array size,
// or using c++11 standard arrays instead.
static const int&& arr_size{5};
int arr3[arr_size];
} - Standard Arrays
It’s more safe, but has little bit more overhead.1
2
3
4
5
int main() {
std::array<int, 5> arr;
for (int i = 0; i < arr.size(); i++) arr[i] = 2;
}
How Strings Work (and How to Use Them)
C-style Strings (String Literals) and std::string_view
C-style strings are stored in code segment (virtual address space, which is read-only), this means you can only replace new string to the variable to “change” it.
1 |
|
Each strings at end has
\0
(named “null termination character”) to prevent out of index at iteration. e.g.char str[7] = {'C', 'h', 'e', 'r', 'n', 'o', '\0'};
Terminal character will actuall break the behavior of string in many cases, usestd::string_view
can prevent this problem.
1
2
3
4
5
6
int main() {
char name[8]{"Che\0rno"};
std::cout << strlen(name) << std::endl;
}
A sample implementation of std::string_view
:
1 | // An implementation of std::string_view |
std::string
It’s a char array indeed.
1 |
|
1 |
|
Constants in C++
The mutability of const depends on how it stores:
- String Literal stored in the read-only section of the memory. So modifying will cause segmentation fault.
- Constant variables may convert to a literal and place the primitive value in assemble in compile stage, while if the code attempt to take the address of constant variable the compiler will let it place in memory.
Constant Pointer
1 |
|
const int*
equalsint const*
. Soconst int*& ptr{&a}
is illegal sincea
’s address is a rvalue,const int* const& ptr{&a}
orconst int*&& ptr{&a}
should work.
Const Method
Const methods cannot change member variables in the class, except for mutable
and static
variables.
1 |
|
Member Initializer Lists in C++ (Constructor Initializer List)
Use member initializer lists can prevent the use of =
which may initialize object twice.
1 |
|
Ternary Operators in C++ (Conditional Assignment)
Ternaay can simplify if else
1 |
|
Create/Instantiate Objects
There are two main section of memory: stack and heap.
- Stack objects have an automatic lifespan, their lifetime is actually controlled by the their scope.
the stack size is small (usually 1~10M), if you have a big object, you have to store it in the heap. - Heap: once you allocated an object in the heap, it’s gonna sit there unill you explicit delete it.
1 |
|
Manual resource release is memory leak prone, use smart pointer is a better idea.
The New/Delete Keyword
How the new
keyword find free space on memory? There is something called free list which maintain the addresses that have bytes free. It’s obvously written in intelligent but it’s stll quite slow.
new
is just an operator, it uses the underlying C functionmalloc()
, means that you can overloadnew
and change its bahavious.new
also calls the constructor.delete
also calls the destructor.
Three uses of new
(normal new, array new, placement new)
1 |
|
Implicit Conversion and the Explicit Keyword in C++
1 |
|
Operators and Operator Overloading in C++
In the case of operator overloading you’re allowed to define or change the behavior of operator
- Operators are just functions
Here goes some examples:
1 |
|
Object Lifetime (Stack/Scope Lifetimes)
- Don’t return object stored in stack
- Use
{}
to create a local scope so that stacked things will be released earlier. - Underlying of Unique Pointer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int* CreateArray() {
int array[50]; // Don't write code like this
return array; // The array gets cleared as soon as we go out of scope
}
class Entity {
public:
void print() { std::cout<< "Print from Entity!" << '\n'; }
~Entity() { std::cout << "Entity released~" << '\n'; }
};
// We can use `std::unique_pointer` which is a scoped pointer,
// but here we write our own to explain how it works
template<class T>
class ScopedPtr {
private:
T* m_ptr;
public:
ScopedPtr(T* ptr) : m_ptr{ptr} {}
~ScopedPtr() { delete m_ptr; }
// `->` is special, it's invoked in a loop to call another `->` if
// the return value is another object (not a pointer), and will finally
// dereference the founded pointer.
T* operator->() { return m_ptr; }
T operator*() { return *m_ptr; }
ScopedPtr(ScopedPtr<T>&) = delete; // Inhibit copy constructor
ScopedPtr<T> operator=(ScopedPtr<T>) = delete; // Inhibit copy assignment
};
int main() {
{ // scope with brace
Entity e; // the object created on stack will gets free when out of the scope
ScopedPtr<Entity> ptr = {new Entity};
// ScopedPtr<Entity> ptr2{ptr};
ptr->print();
(*ptr).print();
}
// since the scoped pointer object gets allocated on the stack which means it
// will gets deleted when out of the scope and call ~ScopedPtr()
}
Smart Pointers (std::unique_ptr, std::shared_ptr, std::weak_ptr)
Smart pointers is that when you call new
, you don’t have to call delete
. Actually in many cases with smart pointers we don’t even have to call new
.
- Unique pointer
- Shared pointer & Weak pointer
Shared pointer use something called reference counting. If create one shread pointer and define another shared pointer and copy the previous one, the reference count is now 2; when the first one dies (out of scope), the reference count goes down 1; when the last one dies. the reference count goes back to zero and free the memory.
Weak pointer will not increase the reference count.
1 |
|
Copying and Copy Constructors
Ues =
(shallow copy) to copy. An object created on heap without a copy constuctors or an object created on stack but with pointer variables that point objects on heap will lead to unexpected results since shallow copy don’t copy them fully but essentially just copy the address in pointer variable. So a copy constructor is required to delimit the behavior of the copy operation.
1 |
|
The Arrow Operator
- It’s possible to overload the Arror Operator and use it in specific class such as ScopedPtr:
- It can also be used to get the variable’s memory offset in an object (in some memory hack):
1 |
|
Dynamic Arrays (std::vector)
Vector in C++ is not mathematical vector, it’s kind of dynamic arrays like.
1 |
|
Optimizing the usage of std::vector
Two ways to reduce memory copy
1 |
|
Using Libraries in C++
Using GLFW as example.
- Visual Studio
- Create a folder called “Dependencies” under your project directory and then put the library into it.
1
2
3
4
5
6
7
8
9C:\Users\USERNAME\source\repos\Your_Project_Directory\Dependencies\
-> GLFW\
-> include\GLFW\
glfw3.h
...
-> lib-vc2015\
glfw3.dll
glfw3.lib
glfw3.dll.lib - Open project settings:
-> Configuration: All Configuration
-> C/C++ -> Additional Include Directories:$(SolutionDir)\Dependencies\GLFW\include
-> Linker -> General -> Additional Library Directories:$(SolutionDir)\Dependencies\GLFW\lib-vc2015
- Create a folder called “Dependencies” under your project directory and then put the library into it.
Static linking
Static linking happens at compile time, the lib intergrate into executable or a dynamic library
Visual Studio
- Open project setting
-> Linker -> Input -> Additional Dependencies:glfw3.lib;xxxxxx;balabala;...
- Static Link
Main.cpp 1
2
3
4
5
6
7
8// quote for header in this project, regular bracket for external library
// Or `extern "C" int glfwInit();`
// Since GLFW is actually a C library so we need `extern "C"`
int main() {
int result = glfwInit();
std::cout << result << '\n';
}
Dynamic linking
Dynamic linking happens at runtime
- Some librarys like GLFW supports both static and dynamic linking in a single header file.
glfw3.dll.lib
is basically a series of pointers intoglwfw3.dll
- Code is basically as same as static linking.
Visual Studio
-
Open project settings:
-> Linker -> Input -> Additional Dependencies:glfw3.dll.lib;xxxxxx;balabala;...
-
Put
glfw3.dll
to the same folder as your executable file (i.e:$(SolutionDir)\Debug
) -
In fact, to call a function in dynamic library, it needs a prefix called
__declspec(dllimport)
If you exploreglfw3.h
you will see there is a prefixGLFWAPI
in every function’s definition:1
2/* We are calling GLFW as a Win32 DLL */
So you need to define a Macro in VS:
Open your project setting:
-> C/C++ -> Preprocessor -> Preprocessor Definitions:GLFW_DLL;xxxxx;bababa...
But why it seems stll work properly without the
dllimport
prefix?
In modern windows,dllimport
is not needed for functions, butdllimport
is still needed for C++ classes and global variables.
Making and Working with Libraries in C++ (Multiple Projects in Visual Studio)
- Visual Studio Setup:
- Create one solution with 2 projects: “Game” and “Engine”,
- Project “Game”:
Ceneral->Project Defaults->Configuration Type: Application (.exe)
-> C/C++ -> General -> Additional include Directories:$(SolutionDir)\Engine\src;
- Project “Engine”:
Ceneral->Project Defaults->Configuration Type: Static library (.lib) - Right click on projects “Game” -> Add -> Reference -> Select project “Engine”
- Code for project “Engine”:
Your_Project_Directory\src\Engine.h 1
2
3
4
namespace engine {
void print_message();
}Your_Project_Directory\src\Engine.cpp 1
2
3
4
5
namespace engine {
void print_message() { std::cout << "Hello Game!" << '\n'; }
} - Code for project “Game”:
Your_Project_Directory\src\Application.cpp 1
2
int main() { engine::print_message(); }
How to Deal with Multiple Return Values in C++
Example scenario: We have a function called parse_shader()
, it needs to return two strings.
- Return a struct cotains two strings (Cherno’s choose):
1
2
3
4
5
6
7
8
9
10
struct ShaderProgramSource {
std::string vertex_source;
std::string fragment_source;
};
ShaderProgramSource parse_shader() {
// ... (Some statements that process result 'vs' and 'fs')
std::string vs, fs;
return {vs, fs};
} - Use reference paremeter (Probably one of the most optimal way)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void parse_shader(std::string& out_vertex_source, std::string& out_fragment_source) {
// ... (Some statements that process result 'vs' and 'fs')
std::string vs, fs;
std::tie(out_vertex_source, out_fragment_source) = std::tuple{vs, fs};
}
// Or use pointer parameter if you want to pass nullptr (ignore the output):
void parse_shader2(std::string* outVertexSource, std::string* outFragmentSource) {
std::string vs, fs;
if (outVertexSource) *outVertexSource = vs;
if (outFragmentSource) *outFragmentSource = fs;
}
int main() {
std::string vertex_source, fragment_source;
parse_shader(vertex_source, fragment_source);
parse_shader2(nullptr, &fragment_source);
} - Return a
std::array
orstd::vector
The different is primarly the arrays can be create on the stack whereas vectors gonna store its underlying storage on the heap.
So technically returning a standard array would be faster.1
2
3
4
5
6
7
std::array<std::string, 2> parse_shader() {
std::string vs, fs;
// ... (Some statements that process result 'vs' and 'fs')
return {vs, fs};
} - Using
std::tuple
andstd::pair
1
2
3
4
5
6
7
8
9
10
11
12
std::tuple<std::string, int> create_person() { return {"Cherno", 24}; }
std::pair<std::string, int> create_person2() { return {"Cherno", 24}; }
int main() {
// std::tuple can return more than two elements
auto person{create_person()}; // Automatically deduce the return type
auto& name{std::get<0>(person)}; // Get values in std::tuple
int age{std::get<1>(person)};
// std::pair is a little bit faster than tuple
auto [name2, age2]{create_person2()}; // c++17 structure binding
}
Templates
Template can improve code reuse rate and reduce duplicate code (e.g. function overload), the essence of template is similar to macros
- template type
1 | // In this case, template specifying how to create methods based on usage of them. |
- template argument
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17template<class T, int N> // Multiple template targets can be in one template definition
class Array {
private:
T m_arr[N];
public:
int get_size() const { return N; }
};
int main() {
Array<int, 5> arr; // It will generate the following code:
// class Array {
// private:
// int m_arry[5];
// public:
// int get_size() const { return 5; }
// };
std::cout << arr.get_size() << '\n';
} - The keyword
typename
Clearify something is a type.1
2
3
4
5
6
7
8
9
10
11
12
13// C++20 makes `typename` optional on where nothing but a type name can appear
template <class T>
T::U f(); // Return type
template <class T>
void f(typename T::U); // Ill-formed in global scope, without `typename`, `T::U` would be
// considered a static member
template <class T>
struct S {
T::U r; // member type
T::P f(T::P p) { // But Ok in class scope, argument type of a methods
return static_cast<T::R>(p); // Type in casts
}
}; - Non-type template parameter (C++20)
1
2
3
4
5
6
7template<class T> struct X { constexpr X(T) {} };
template <X x> struct Y {}; // Non-type template parameter
Y<0> y; // OK, Y<X<int>(0)>
template <typename T, std::size_t N>
struct S2 { T data[N]; /* Array of dependent bound */ };
S2 s{{1, 2, 3, 4, 5}}; - User-defined deduction guides
Template parameters of a class template could not be deduced from a constructor call until the introduction of deduction guides in C++171
2
3
4
5
6
7
8
9
10template <class T>
struct Container {
Container(T t) {}
template <class Iter> Container(Iter begin, Iter end) {}
};
template <class Iter> // Guides compiler how to deducing elements type
Container(Iter b, Iter e) -> Container<typename std::iterator_traits<Iter>::value_type>;
std::vector<double> v{1, 2};
Container d{v.begin(), v.end()}; // Deduces Container<double> - Abbreviated function template (C++20)
1
2
3
4void f1(auto); // same as template<class T> void f1(T)
void f2(Concept1 auto);
template<>
f1(int); // Can be specialized like normal
How to Make Base Class Access Derived Class’s Methods (CRTP)
Curiously Recurring Template Pattern (CRTP): A derived class derives the base class with itself as a template argument.
1 |
|
SFINAE
“Substitution Failure Is Not An Error”, compiler will continue to find suitable template.
If statement in template using SFINAE
1 | template <bool Cond, typename IfTrue, typename IfFalse> |
Narrowing conversion check using SFINAE
1 | template <typename From, typename To, typename = void> |
A skillfully implementation of custom std::formatter
. The parse()
method is a template, since it’s constexpr
specified, any possible cases that result a throw
will be filtered in a SFINAE way.
1 | struct QuotableString : std::string_view {}; |
Ways to Print Type Name
1 |
|
Concepts
1 | template <typename T> concept Addable = requires(T a, T b) { a + b; }; |
Stack vs Heap Memory in C++
Ignore…
Macros in C++
Macros do text replace at preprocessor stage
1 |
|
Macros function and combine with the environment, environment variables can be defined at: Project settings -> C/C++ -> Preprocessor -> Preprocessor Definitions
1 |
|
Variadic macros
1 | main() { |
The “auto” Keyword
Be careful with auto
:
1 |
|
auto
’ can reduce type length
1 |
|
Static Arrays in C++ (std::array)
TODO
Function Pointers in C++
- 3 ways to definite a Function Pointers
1
2
3
4
5
6
7
8
9void hello_world(int a) noexcept { std::println("Hello World! Value: {}", a); }
int main() {
auto const f1{hello_world}; // auto deducing
void (*const f2)(int){hello_world}; // C-style function pointer
using Balabala = std::function<void(int)>; // Using alias
// typedef void (*Balabala)(int); // Or C-style typedef
Balabala const& f3{hello_world};
f1(1), f2(2), f3(3);
} - A simple usage - the ForEach function:
1
2
3
4
5
6
7
8template <typename E> void print_value(E value) { std::println("Value: {}", value); }
template <typename Cont, typename Func> requires requires(Func func) { static_cast<std::function<void(typename Cont::value_type)>>(func); }
void for_each(Cont const& cont, Func func) { for (typename Cont::value_type v : cont) func(v); }
int main() {
std::vector vec{1, 5, 4, 2, 3};
for_each(vec, print_value<int>);
for_each(vec, []<typename E>(E value) { std::println("Value: {}", value); }); // Or use a lambda (with template parameter)
}
Lambdas in C++
A lambda is basically a little throwaway function that you can write and assign to a variable quickly.
- How to put outside variables into lambda function
[=]
: Pass everything in by value, the pass in variables is independent of the outside.
[&]
: Pass everything in by reference.
[a]
: Passa
by value
[&a]
: Passa
by reference. - using
mutable
keyword to allow modify pass in variables1
2
3
4
5
6int main() {
int a{8};
auto f{[=]() mutable { a = 5; std::println("Value: {}",a); }};
f();
std::println("Value: {}", a); // x is still 8, because [=] just copy value into this lambda.
} - We need to use
std::function
instand of C-style raw function pointer if lambda has pass in variables (stateful).1
2
3
4
5
6
7
8
9
10
11void for_each(std::vector<int> const& values, std::function<void(int)> const& func) noexcept {
for (int const i : values) func(i);
}
int main() {
std::vector values{1, 2, 3, 4, 5};
int state{};
// Cannot cast to C-style void(*callback)(int)
auto callback{[&](int value) { std::println("Current state: {}, Value: {}", state, value); }};
state = 1;
for_each(values, callback);
} - Usage of
std::find_if
(returns an iterator to the first element when callback function returns true)1
2
3
4
5int main() {
std::vector values{1, 5, 4, 2, 3};
auto iterator{std::find_if(values.begin(), values.end(), [](int value) { return value > 3; })};
std::println("First element that > 3: {}", *iterator);
} - Capturing Parameter Packs in Lambda
1
2
3
4
5
6
7
8
9
10
11
12constexpr int add(int a, int b) noexcept { return a + b; }
template <typename F, typename... Args>
constexpr auto delay_call(F&& f, Args&&... args) noexcept {
// C++20 improved capturing parameter packs in lambda
return [f = std::forward<F>(f), ... f_args = std::forward<Args>(args)]() constexpr noexcept { return f(f_args...); };
// "..." is like says "take whatever on the life and unpack it accordingly"
// If the parms pack f_args is "<int, int>{1, 2}"
// Compiler will expand `f(args...)` to `f(1, 2)`, expand `f(args)...` to `f(arg1), f(arg2)`
}
int main() {
std::println("{}", delay_call(add, 1, 2)());
}
Namespaces in C++
Use Namespace to
- Avoid naming conflict:
apple::print()
,orange::print()
- Avoid C library like naming:
GLFW_initialize
toGLFW::initialize
We can set to use only specific symbol in a namespace
1 | namespace apple { |
Nested namespaces can be shorten using alias:
1 | namespace apple::functions { |
Why don’t “using namespace std”
Absolutely don’t use using namespace
in header files.
If you must using using namespace
, please use it in a small scope as possible.
For example a serious issue of implicit conversion:
1 | namespace apple { |
Threads
If we want to do something else when we called functions that will block the current thread, we can use threads (or coroutines in C++20).
- A
std::thread
shoud have eitherjoin()
ordetach()
called otherwise it will callstd::terminate()
in its destructor.
Here is an example:
We created a thread that will do loop on outputting “Working…”,
and simultaneously the main() function is waiting for user input.
1 | int main() { |
Coroutines
1 | // Promise |
Timing
Make a Timer for statistical time-consuming
1 | template <class Res = std::chrono::milliseconds> |
Multidimensional Arrays
1 | int main() { |
The most issue is that the Multidimensional Arrays will results memory fragmentation. When iterating the array we have to jump to another location to read or write that data, and that results probably a cache miss which means that we’re wasting time fetching data from RAM.
One of the most feasible thing you can do is just store them in a single dimensional array:
1 | int main() { |
Sorting in C++
1 |
|
Type Punning in C++
You can Treat an Entity struct as an int array:
1 | struct Entity |
Unions in C++
Defined member in union
means they all in same memory location, union
is a common way for “Type Punning”.
1 | struct Vector2 { |
Type Punning can do as the same result, but using Union makes it more concise.
Virtual Destructors in C++
Virtual Destructors is really important if you are writing a father class,
otherwise no one’s going to be able to safely delete the extend class
(Because without virtual
mark you are just adding a new Destructor instead of overload it)
1 | class Base |
Casting in C++
C++'s cast can do anything that C-style casts can do, those casts make you code more solid and looks better.
- Static cast (compile time checking).
- Reinterpret cast
reinterpret_cast<>()
(for Type Punning). - Dynamic cast (will return
NULL
if casting is failed) - Const cast
const_cast<>()
(Remove the const-ness from references or pointers that ultimately refer to something not constant)
1 | class Base { |
Conditional and Action Breakpoints in C++
Condition Breakpoints: If I only want the breakpoint to trigger under a certain condition
Action Breakpoints: Generally print something to the console when a breakpoint is hit
They can prevent recompile and save time
For details. please watch the video
Safety in modern C++ and how to teach it
You should 100% use smart pointers if you are doing serious work
Precompiled Headers in C++
Look to 7:07
Dynamic Casting in C++
If we force type casting a Enemy class to Player and access data(funcions, variables) that is unique to player, the program will probablly crash.
Dynamic Casting is actually does some validation for us to ensure that cast is valid
1 | class Entity {}; |
BENCHMARKING in C++ (how to measure performance)
Always make sure that you profile is actually meaningful in a releases because you’re not gonna be shipping code in debug anyway
1 | class Timer |
STRUCTURED BINDINGS in C++
Only in C++17 and newer
a better way compare to How to Deal with Multiple Return Values in C++
1 | std::tuple<std::string, int> CreatePerson() |
How to Deal with OPTIONAL Data in C++
Multiple TYPES of Data in a SINGLE VARIABLE in C++?
How to store ANY data in C++
How to make C++ run FASTER (with std::async)
1 |
|
In VS, you can use DEBUG->Windows->Parrllel Stacks (Ctrl+Shift+D)
to do window parallel debugs
How to make your STRINGS FASTER in C++!
1 | ``` |
Small String Optimization in C++
Track MEMORY ALLOCATIONS the Easy Way in C++
1 | ``` |
const type&
is a special rule, realistically what happens is the compiler will probably create like a temporary variable.
there are not to kind of avoid creating an L-value but rather to just kind of support both support both L-value and R-values1
2
3const int& a = 10;
// int temp = 10;
// const int& a = temp;
Continuous Integration in C++
Static Analysis in C++
Static Analysis is a very important thing that even for an experienced programmer there is still going to be stuff what you miss.
It can find logic errors in your code and gives you some tips to fix it
clang-tidy
is a free tool to do static analysis
Argument Evaluation Order in C++
Move Semantics in C++
1 | class String |
std::move and the Move Assignment Operator in C++
std::move
actually do force casting but can make your program more search friendly
Move Assignment will allow us do move operation on existing objects
1 | class String |
ARRAY - Making DATA STRUCTURES in C++
1 | template <typename T> |
std::print
1 | int main() { |
Compiler Optimization
[[likely]]
/[[unlikely]]
used in if-statements and loops whith logical decision.volatile
to tell compiler don’t optimize.constexpr
declares that it is possible to evaluate the value of the function or variable at compile-time. Such variables nd functions can then be used where only compile-time.consteval
force evaluate expression in compile-time.noexcpt
for function that neverthrow
error. Destructors are implicitlynoexcept
.
Due to strong exception guarantee,std::vector
moves its elements (callsObject(Object&& (calls
Object(Object&&)))
) at rearrange only if their move constructors arenoexcept
. You cna usestatic_assert(std::is_nothrow_move_constructible_v<Object>);
to check.constinit
enforces variable is initialized at compile-time. Unlikeconstexpr
, it allows non-trivial destructors. Therefore it can avoid the problem that the order of initialiation of static variables from different translation units is undefined, by initialize them at compile-time.
Another use-case is with non-initializingthread_local
declarations. In such a case, it tells the compiler that the variable is already initialized, otherwise the compiler usually adds code the check and initialize it if required on each usage.
1 | namespace custom { |
1 | struct S { |
Three-way comparison
Comparison categories (The operator<=>()
's return types)
strong_ordering
: exactly one ifa < b
,a == b
,a > b
must be true and ifa == b
thenf(a) == f(b)
.weak_ordering
: exactly one ifa < b
,a == b
,a > b
must be true and ifa == b
thenf(a)
not neccessary equal tof(b)
.partial_ordering
: none ofa < b
,a == b
,a > b
might be true (may be incomparable) and ifa == b
thenf(a)
not neccessarily equal tof(b)
. e.g. Infloat
/double
theNaN
is not comparable
1 | template <typename T1, typename T2> requires requires(T1 a, T2 b) { a<b, a <= b, a> b, a >= b, a == b, a != b; } |
Modules
Module units whose declaration has export
are termed module interface units; all other module units are termed module implementation units
.
1 | export module hello.cpp; // dots are four readability purpose |
1 | module; // Global module fragment (include classical header files) |
1 | export module hello:helpers; // Module interface partition unit |
1 | import hello.cpp; |