Some notes for C++
Table of Contents
- Compile and link
- Header and include
- Default stuff in a class
- When is = operator=, and when is it ctor.
- Static: lifetime and access
- Virtual function and pure virtual function (interface)
- Mutable
- String literal
- Const
- Stack vs. heap instantiation
- Implicit conversion
- std::move
- Don't return an rvalue ref, just return a value
- std::forward
std::unique_ptr
,std::shared_ptr
, andstd::weak_ptr
- Ownership: pointer, reference, smart pointers
- Function pointer
- Union and type punning
- std::variant
- Singleton
- C++14 feels like a patch for C++11
- C++17
- C++20
- Design Class
These are some notes I wrote as I watched Cherno's C++ playlist and some other videos and resources.
Compile and link
- When compile a cpp file, all it cares is how the stuff (functions/class) you use in it is declared. As long as you tell it the declaration (e.g. function signature), it will compile. Even if it is never defined.
- The linking stage is actually linking all these compile stuff together. Therefore, if something is not defined, it will have a link error.
Header and include
#include
is just copying all the stuff in the header to the current file. This also explains why you can do forward declaration.- Headers has the declarations for you to compile the current file (translation unit).
#include <system_header>
#include "you_own_header"
- All the C system header ends with .h, e.g.,
<stdlib.h>
- All the C++ system header has no file extension, e.g.,
<iostrem>
Default stuff in a class
- There are a default constructor, default destructor, default copy constructor/assignment operator, and default move constructor/assignment operator.
- default constructor/destructor is as if you have an empty body of the constructor/destructor. The default constructor will not initialize the primitive type member variable in your class.
- default copy/move ctor do something with
std::memcopy
andstd::memmove
. Therefore, if you have pointers in your class, the default copy constructor will be a shallow copy.
When is = operator=, and when is it ctor.
A a; A b = a; // Constructor b = a; // operator=
The reasoning is pretty simple. The later one is equivalent to
b.operator=(a);
While in the former one, b has not been constructed yet, so it must be a constructor.
Static: lifetime and access
- The static keyword inside a local scope, e.g. in a class, function
means
- The lifetime of this variable is the same as the entire program.
- There is only one copy of this variable function.
- but only code inside the scope can access it (unless you pass it out).
- static functions is the same as static variables, but they feels a
little bit different
- forever lifetime: it can be called without an instance.
- Only one copy: the static member function does not have a
this
inside it. - local access: you need to call it through the class.
- The static keyword outside a scope just means it is only accessible / linked within this file/translation unit.
Virtual function and pure virtual function (interface)
- All method functions can be overwritten in the derived class. However, virtual functions will ask the compiler to create a virtual table in the runtime. And when you do polymorphism, it knows the correct function to call. Otherwise it will call the base class method if it is a base class pointer.
- And additional thing pure virtual functions has is that it forces the derived class to define it.
Mutable
Mark a class variable mutable will allow const methods to change it.
String literal
- They are on a read-only memory, so you cannot modify it.
- In order to modify it, you actually ask the compiler to copy it to a modifiable piece of memory.
char * a = "hello"; // convert a const char * const to char * a[2] = 'c'; // Wrong. it is read only. char b[] = "hello"; // copied from a read only memory to stack. b[2] = 'c'; // It is okay.
Const
const int *a; // the integer cannot be modified. int const * b; // same as above. int * const c; // the pointer cannot be modified to point to other stuff. BTW, int& is sort of like this. Once it is initialized, it cannot be used to reference a different thing.
Stack vs. heap instantiation
- Stack is about 2Mb
- Two reasons to do heap instantiation:
- pass it out of the scope
- too big for stack
- Why is it called stack and heap memory:
- When using stack memory, the variables are created and deleted based on scope. It works exactly like a stack data structure. When you create variables in scope, you allocate memory one after another just like you push things into a stack. When you go out of the scope, the memory allocated in the code is released one after another just like you pop things out of a stack.
- Stack is a small piece of memory reserved for your program. Heap is a large piece of memory shared by all other programs. Therefore, Heap may not be all available for you to use. And the way to manage which part is available and how much is available is just like a heap data structure.
Implicit conversion
It only does it once. For example,
class A { A (std::string a) {} }; void print(const A& a) { }; print("hello"); // This will not compile. "hello" is const char* const. It can be converted to std::string. However, you need to further converted to A, while is the second implicit conversion. print(std::string("hello")); // Good.
std::move
- It is as simple as converting lvalue to rvalue. The real move (include cleaning up the source properly) happens in the move ctor or move assignment. std::move is just to make sure you can match to the function that actually do the move.
- In other words, std::move mark a value as temporary (can be stolen from). The move ctor or assignment actually steals it.
Don't return an rvalue ref, just return a value
class B { A a; public: A give() { return std::move(a); // what happens here is that, a is moved to a temporary var and this temporary var is returned. } } int main() { B b; A a = b.give(); // with copy elision, this line does not trigger either copy or move constructor. }
class B { A a; public: A&& give() { return std::move(a); // what happens here is that, a is convert to a rvalue. } } int main() { B b; A a = b.give(); // move constructor is called here. }
Therefore, with copy elision, return an A&&
does not save you
anything. However, if you return a A&&
, potentially it can be misused.
If the receiver uses an A&&
to receive it. This will not trigger any
real move, either. If at this moment the original copy is destroyed,
then the receiver end has a dangling pointer.
A&& a = b.give(); // nothing happens here as well.
std::forward
It is converting something to T&&. This is only used in a template function used for relay purposes.
Ownership: pointer, reference, smart pointers
What does it mean when passing pointer, reference, smart pointers to a function (ctor).
A&
is almost the same asA * const
, except that reference guarantees its existence, while pointer can benullptr
.- In modern C++ and user side of the code, passing raw pointer and reference means borrow. No need to destroy.
- There are exceptions in old-style code base, such as ceres-solver, if I remember correctly.
- Pass a
unque_ptr
is transfering ownership, whileshared_ptr
is shared ownership.
Function pointer
- C style function pointer
ReturnType(*function_pointer)(ArgumentType)
this is the declaration of a function pointer.type variable
this is the declaration of a variabletypedef ReturnType(*FunctionPointerType)(ArgumentType)
this is how to typedef a function pointer type.typedef Type NewTypeName
is this a normal typedef.
- lambda function's type is undefined in the standard, so it is up to
the compiler's implementation. That is why you cannot pass it to a C
style function pointer. You can either use a template to hold it, or
convert it to
std::function
.
Union and type punning
- type punning is to interpret a piece of memory as a different type. For example
struct A { int x, y; }; A a; int* b = (int*)&a; b[0]; // a.x; b[1]; // a.y;
- Union is a nicer way to achieve this.
std::variant
- a safer way to do Union
- but instead of occupying memory that is the max of all the variants like union, it occupies memory that is the sum of all its variants.
Singleton
- a static member function defines and then returns a static variable looks better than declaring a static variable and then defining this variable.
C++14 feels like a patch for C++11
auto
as a function return type;- Template lambda function using auto keyword
Generalized capture expression in lambda
[value = v] () {}
make_unique
Now, technically, you no longer to use explicitnew
anddelete
.- More flexible
constexpr
C++17
Guaranteed Copy/ Move Elision
auto factory(){ return std::make_unique<int>(); } main() { auto widget = factory(); // move will not be invoked here. }
- more
const_expr
in std constexpr
in lambdastd::string_view
Class template argument deduction
std::array data{1,2,3,4}; // data will be std::array<int,4>
- fold expressions in variadic template
structured bindings
std::pair<int, double> v{1,3.}; auto [first, second] = v;
if-init
if (auto f = function(); f > 5){ // do something }
C++20
Coroutine
co_yield
similar to python'syield
It will be more complete in c++23. Don't do it yourself, use a library for now. The idea is that the function contains the status, and it returns different value based on the status. Beforeco_yield
, you can do it in three ways:- Create a class that holds the state
- Put the state as a global variable and use it in the function. BAD STYLE!
- Put the state in the capture of lambda. The real implementation is similar to this. It is like an object without a class definition.
co_wait
wait for another thread toco_yield
Beforeco_wait
, you can do it through callback:- When wait, register a callback to the other thread
- When the other thread yield, call the callback
- This is much harder to read.
Concept
For template, explicitly write out the constraint of the template type. Before C++20, template type can be anything, compile will output a "wired" error when you use the template function with a not supported type. The way to constraint the template type is through
enable_if
, but it is pretty hard to write.Another alternative is to use interface class and polymorphism, but this is more for unpredictable runtime types. It is over kill and has extra price to pay if your type is known at compile-time.
- Can be combined.
Range
- Build on top of concept
- Lazy evaluation
Module
- *.cppm
- export
- import
Design Class
Don't make data member const or references
- Otherwise, the copy assignment and move ctor/assignment will be gone. but copy ctor is okay.
- use a raw pointer instead of a reference. Function interface can still be a reference. It is just stored as a pointer.
Value initializer
Widget w{};
will also initializeint
orint*
.- best practice is to put the initial value in the class when you declare the variable. The reason to not put in ctor, is to reduce duplication if you have two ctors that both need to initialize.