class: center, middle, inverse
.footnote[Press f
for full-screen, ?
for presentation controls.]
class: center, middle, inverse
It’s easy to get lost in the details; don’t.
Look for intuitions and at the big picture.
We want to build a mental model, not a rule book.
class: center, middle, inverse
## Anything you say.red[1] can and will probably be taken offline
### .left[When starting out, detailing every nook and cranny of a concept would be detrimental.]
.footnote[.red[¹]: that goes too deep, deviates from the topic or only shows our prowess]
.left-column[
]
.right-column[ Why go through …
- crude data types
- raw pointers, contorted references
- manual memory management
.pull-left[ … just be cool and use Python ➜ ]
.pull-right[
print(2 ** 63) # 2⁶³
print(2 ** 80) # 2⁸⁰
> 9223372036854775808
> 1208925819614629174706176
]
How C++ fares against Python here?
int x1 = pow(2, 63); // Warning: overflow (GCC)
unsigned x2 = pow(2, 63); // Warning: overflow (GCC)
uint64_t x3 = 1 << 63; // Warning: shift count ≥ width
*uint64_t x4 = 1ul << 63; // finally!
// 2⁸⁰? Hmmm... all in good time!
.pull-left[ Results with GCC
2147483647
4294967295
0
9223372036854775808
]
.pull-right[
Results with Clang
1703138392
0
0
9223372036854775808
]
]
class: center, middle, inverse
So many retries! Aargh!! 🤦
Output varies between compilers for same program?! Oh mama! 😲
class: center, inverse name: ipow2-1
template: ipow2-1 name: ipow2-2
## Pop quiz: **What’s the size of `int`?**
template: ipow2-2
## Answer: **No fixed size!**
Ask your compiler: sizeof(int)
; never assume 🤔
.pull-left[
char
.little[(≥ 8-bits = 1 byte)].red[2]short
.little[(≥ 16-bits)]int
.little[(≥ 16-bits)]long
.little[(≥ 32-bits)]long long
.little[(≥ 64-bits)] ]
.pull-right[
float
.little[(32-bit)].red[3]double
.little[(64-bit)].red[3]long double
.little[(usually 80, 96 or 128 bits)]
.little[ Only the below rule about integers is well-defined; assuming more is risky!] ]
sizeof(char) ≤ sizeof(short) ≤ sizeof(int) ≤ sizeof(long) ≤ sizeof(long long)
.left[### Other Types]
void
bool
- Pointer types e.g.
int*
,unsigned char**
,void (*)(float)
.little[Arithmetic based on pointed-to type e.g.int *p; ++p;
movesp
bysizeof(int)
] - Array types e.g.
int[2], char[6][5]
.footnote[.red[¹]: variants of unsigned
and signed
(two’s complement)]
.footnote[.red[²]: A byte needn’t be 8 bits; it’s whatever char
’s bit width is 😳]
.footnote[.red[³]: Not guaranteed to be, but mostly, IEEE-754 float]
C++ Standard .little[(ISO/IEC 14882:2017)]
-
Many modern language users are at the mercy of one implementation
-
The ISO C++ standard guards C++ programmers with certain guarantees
.little[A contract between language users and compiler writers ] 🤝 -
Programs adhering to the standard always work and remain portable
.little[e.g. Compile a 20-year old programg++ -std=c++98 old.cpp
even today on any platform with a compiler; it works] -
Standard precisely defines many aspects of a program: well-defined ← this is 🏠
-
Standard loosely defines some aspects .little[(Implementation-defined, Unspecified and Undefined behaviour)] ☠ .little[
- for many exotic architectures having C++ compilers
.little[e.g. Unisys Servers with 9-bit bytes and 36-bit ints programmable in C and C++ (not Python or JS — sorry!)] - for freedom to compiler-authors — different compilers, varying implementations: a healthy competition ]
But it works on my machine!?
Say, you survived a wrong side drive on a highway at midnight once, would you argue its repeatable/correct?
??? e.g. array access out of bounds, accessing a zombie object, null pointer dereference and many more!
#include <cstdint> // ← fixed-width integer types
#include <limits> // ← query type limits from compiler
#include <optional>
#include <iostream>
std::optional<uintmax_t> ipow2(unsigned pow) {
// Future-proof by not using uint64_t and limiting to 64-bit architectures.
// Obtain size from compiler at compile-time; thanks to static typing
if (pow >= std::numeric_limits<uintmax_t>::digits)
return {};
// 2⁸ 2⁷ 2⁶ 2⁵ 2⁴ 2³ 2² 2¹
uintmax_t value = 1; // ----------------------------------------------+
value <<= pow; // … | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
return value; // ----------------------------------------------+
}
int main(int argc, char** argv) {
if (argc >= 2) {
auto const pow = std::strtoul(argv[1], nullptr /*str_end*/, 10 /*base*/);
auto const result = ipow2(pow);
if (result)
std::cout << *result << '\n';
else
std::cout << "Power beyond machine limits!\n";
}
}
- Use fixed-width integers:
uint8_t
,int_fast16_t
,int32_t
,uintptr_t
, … .little[-unsigned
: beware of wrap around behaviour; decrement with extreme care e.g.uint8_t x = 0; --x; // x is now 255
] - Use
short
,int
,long
, etc. when you’re sure minimum (guaranteed) width is enough
.little[- Firefox/Chromium hasint
s but use only a few compilers (all have 32-bitint
) and target onlyi686
,x86_64
andARM32
]
name: func-1
- Compile-time: types, qualifiers, functions, structs, classes, templates, … exist.
- Run-time: Oodles of binary your machine loves to gobble!
template: func-1 name: func-2
// Simple, complete program: no classes, libraries or includes. +-------------+
int add(int x, int y) { // +-------------+ | free |
return x + y; // | free | | |
} // S | | <-- add() +-------------+
// T | | | int int |
int main(int argc, char** argv) { // A +-------------+ add() --> +-------------+
int a = 1, b = -4; // C | int int | | int int |
return add(a, b); // K | int char** | <- main -> | int char** |
} // +-------------+ +-------------+
template: func-2
- Languages compiling to byte code, keep type information.red[1] at run-time too
.little[VM needs them for reflection, garbage collection, JIT optimizations, …] - .tag[Hardware] C++ strips them and spits object code
.little[Data and code vanish into zeros and ones. Raw binary, just as advertised 👍]
.tag[Flexibility] .tag[Performance] C++ data types — built-ins & custom — are raw; no base
Object
under the hood. They inherit nothing..red[2]
.footnote[.red[¹]: Commonly called boxed datatypes e.g. Integer
inheriting Java.lang.Object
wraps the actual integer. C++’s int
is machine integer.]
.footnote[.red[²]: You don’t pay for what you don’t use.]
.pull-left[
- Static type system .little[
- Expects all types, variables known at compile-time
- Key to compile-time error detection and performance]
- Type = (Size, Operations).little[- e.g.
int
loadssizeof(int)
bytes to an integer register; allows binary and arithmetic operations.] - Object = (Type, Memory) → Value .little[
- Allocation + Initialization
- Value interpretation based on type]
- Other languages hide locations .little[
- But every variable is a pointer! e.g. Python.red[1] internally uses
PyObject*
for every variable] ]
.pull-right[
- Most languages treat variable as stickers; labels stuck on objects .little[- Types associated with objects (at runtime too)
- Variables = handle to be (re)stuck on an object]
> i = 0 # type ∉ variable
> i = "hi" # no error on reseat
- Variable = _(Type, Location, Value)_
.little[ - Born, live, die with same type and location]
int a = 1;
float a = 0.5; // redefinition error
]
char c = 0xde; // value: 0xde (c), loc = 1000 (&c), type: char (gone at runtime)
int a = 3203338898; // allocate sizeof(int) bytes, initilize to 1
short *b = reinterpret_cast<short*>(&a); // *b = 0xfeed (on a big-endian machine)
char *p = &c; ,--------------------------------------------------
↓
… 1000 1001 /-- short --\ /--------- char* ---------\
----------------------------------------------------------------------------
.. | 0xde | 0xad | 0xfe | 0xed | 0x12 | 0x92 | 0x00 | 0x00 | 0x10 | 0x00 | ..
----------------------------------------------------------------------------
↑ \----------- int ----------/ |
`------------------------------------------------------'
.footnote[.red[¹]: CPython – the most common Python implementation]
class: center, middle, inverse
-
Consider an architecture, say x64; maximum representable
int
on x64 is0xffff'ffff'ffff'ffff
=18446744073709551615
-
Values, instructions, addresses cannot exceed as CPU cannot understand a larger integer
-
How then to represent larger integers? Big Integers a.k.a Big Numbers
-
Make an array of 64-bit integers each representing a part of the constituted big integer
-
e.g. for
1 << 80
, you’d need 3 integers; result, if shifted again by20
, prefix one more, …
// bits 127 96 95 64 63 32 31 0
+-----------------------------------------------+
| . . . | int | int | int | int | int | // a big int in memory
+-----------------------------------------------+
// elements 4 3 2 1 0
-
Fun Exercise: Implement
class BigInt
using astd::vector<uint8_t>
- Implement arithmetic and shift operators
- Needs overflow/underflow detection and bit manipulation
-
Use rudimentary hardware features as building blocks to build higher abstractions
name: zero-cost-abs-1
- .tag[Hardware] Manufacturers make dedicated hardware for certain algorithms .little[
- E.g. GPUs for 3D data and pixels, hardware decoders for MP3, etc.
- SIMD instruction sets to perform multiple operations in one CPU cycle. ]
- .tag[Performance] Using dedicated hardware is fast; really fast 🚀
- Software emulation provided as a fallback when dedicated hardware absent – slow 🚲 .little[
- e.g. Graphics using CPU when GPU is missing ]
template: zero-cost-abs-1 name: zero-cost-abs-2
-
Python’s
int
has arbitrary-precision because internally all integers are big! 😲- Programmer is never exposed system’s native integer (64-bits)
- Software emulation, not for specific algorithms but, for a basic functionality
- Every programmer penalized for flexibility so that expressions like
2 ** 80
work
template: zero-cost-abs-2
-
C++ only provides
int
, and no more, since your machine only has that!- .tag[Flexibility] Include a library or write one just for the module needing
BigInt
.little[- e.g. GNU Multiple Precision library is one of the fastest out there] - .tag[Hardware] .tag[Performance] Exposes dedicated hardware by providing direct access .little[- Using SSE2 is just a header away: live example of a 4-vector addition using 1 assembly instruction]
- .tag[Flexibility] Include a library or write one just for the module needing
You only pay for what you use. — C++ Design Principle
-
Turn-around vs throughput: different settings, different expectations
- Example: Ordering food at a restaurant vs ordering for home delivery 🍜
- Wasting time on avoidable, petty procedures means an overall delay in eating
- Stopping 5 mins for a smoke is OK when it’d be 30 mins restaurant ➜ home
- Example: Ordering food at a restaurant vs ordering for home delivery 🍜
-
Time-wise non-critical software examples
- Server-side code looking up voluminous database — .little[Overarching bottleneck: disk access, network]
- Content generators e.g. Doxygen — .little[Output quality matters but not time-taken]
-
Non-critical software: When bottleneck is elsewhere, optimizing every instruction or data is pessimization
Shaving off a few (unnoticeable) microseconds when overall lag would be in seconds is pessimisation.
- Critical software: No such luxury; picoseconds and bytes matter
- Very small lags every operation leads to a sluggish system
- You can’t put a finger on it but system feels slow
-
.tag[Performance] Most run on a VM.red[1] with garbage collection.
-
.tag[Flexibility] Sacrifices finer control for more features.
.little[e.g. reflection, garbage collection, rich built-in types like lists, dictionaries/maps, big integers, … ] -
.tag[Hardware] Abstracts machine away as much as possible.
.little[Assumes programmers doesn’t know hardware; relieves programmer from worrying about hardware intricacies]
-
.tag[Performance] Zero-overhead abstractions.red[2]; as close to hardware as possible but not closer.
.little[Easy to reason about the machine code generated for your program.] -
.tag[Flexibility] You choose what you want. You only pay for what you use.red[3].
.little[Programmer, not the language, is in charge. Believes programmer knows what s/he doing.] -
.tag[Hardware] Low-level access enables authoring kernel, virtual machines, …
.little[Direct access to CPU/GPU/OS facilities, yum! No VM, no GC no middleman — no commission.]
.footnote[.red[²]: std::vector
or std::map
should perform almost like a vector or red-black tree hand-coded in assembly]
.footnote[.red[³]: A key design guideline of the C++ standards committee; another is portability.]
.pull-left[
- Automatic allocation and deallocation .little[- Alloc/dealloc order: lexical/reverse lexical order.red[1] ]
- Fast: really fast! 🚀 .little[
- Allocation is a mere register increment/decrement
- Spatial and temporal coherence; cache-friendly ]
- Limited scope .little[
- Alive within current and called functions
- Can’t
return
local variable address but can pass up ] - Limited size (configurable) .little[
- Default MiB/thread: 2 (GCC), 1 (MSVC) ]
- Unexposed in most languages 😱 ]
.pull-right[
- Manual allocation and deallocation .little[- Manual memory management is a land mine 💥
- 40 years of experience proves we are bad at it; be smart, use
unique_ptr
,shared_ptr
,weak_ptr
,vector
,string
… - Never even write
new
,malloc
,CoTaskMemAlloc
, … - Know:
delete ≠ delete []
,delete ≠ free()
, … ] - Slow: de/alloc involves OS intervention .little[
- Virtual memory, book keeping, fragmentation
- Pointer chasing isn’t cache-friendly ]
- No scope; alive until manually freed .little[
- Hot bed of memory leaks! Stack unwinding example. ]
- Practically no limit .little[(32-bit: 3 GiB, 64-bit: 16 EiB)] ]
.left[> Stack-allocate if size known at compile-time, within scope and within size limit; heap-allocate otherwise.]
void LoadCount(int* count);
// THE GOOD // THE BAD // THE UGLY
int c = 0; int* c = new int; unique_ptr<int> c = make_unique<int>();
LoadCount(&c); LoadCount(c); LoadCount(c.get());
delete c; // needless alloc and dealloc - slow
.footnote[.red[¹]: Order matters: from top to bottom, left to right, declare least dependant to most dependant – both in function and struct
/class
.]
???
- Stack size is OS-dependant too: 8 MiB on macOS
- Know:
delete
between runtimes are unequal!
struct Passport { struct SmartPassport {
char* id; string id;
int expiry; int expiry;
} }
struct User { struct SmartUser {
int dob; int dob;
char* name; string name;
Passport* pass; unique_ptr<SmartPassport> pass;
}; };
int main() { SmartUser u1 = {0, {}, new SmartPassport()};
User *u2 = new User();
u2->name = new char[10]();
u2->pass = new Passport();
u2->pass->id = new char[10](); HEAP
---------------------------
delete [] u2->pass->id; ------/ .-----------------. \------
delete u2->pass; ---/ .->| | | | | | | | | | \---
delete [] u2->name; --/ | '-----------------' \--
delete u2; -/ .-----------. | .-------. \-
} / .->| int | | .->| char* |- .-----------------. \
+-------------+ / | |-----------| | | |-------| `-->| | | | | | | | | | \
| | | | | char* |-' | | int | '-----------------' |
S |-------------| .-\---' |-----------| | '-------' /
T | User* |--' \ | Passport* |---' .-----------. /
A |-------------| -\ '-----------' .-->| char* | /-
C | int | SP* |--, --\ | |-----------| /--
K |-------------| '-----------\-------------' | int | int | /---
| int | char* | ------\ '-----------' /------
+-------------+ ---------------------------
//--SHALLOW COPY: copy values as-is -------------------------------------------------
// owner clone
int owner = 12; // owner has data originally +----+ +----+
int clone = 0; // need to make a copy | 12 | | 12 |
clone = owner; // copy value as-is +----+ +----+
//--SHALLOW COPY of pointer (mostly wrong, unless intended sharing) -----------------
// owner data clone
int* owner = new int(12); // owner is a pointer +-------+ .-----. +-------+
int* clone = owner; // clone copies it!! | 0x100 |-->| 12 |<--| 0x100 |
// Inconsistent ownership!! +-------+ '-----' +-------+
// Responsibility unclear: who'll free data? 0x100
//--DEEP COPY: both gets to have its own copy----------------------------------------
// owner clone
int* owner = new int; *owner = 12; // +-----+ .----. +-----+ .----.
int* clone = new int; // allocate memory |0x100|->| 12 | |0x200|->| 12 |
*clone = *owner; // copy data, not address +-----+ '----' +-----+ '----'
// 0x100 0x200
//--MOVE: steal data from the owner--------------------------------------------------
int* owner = new int(12); // owner data thief
int* thief = nullptr; // +-------+ .------. +-------+
std::swap(owner, thief); // | 0x100 |---->| 12 | | 0x0 |---->X
// super cheap +-------+ '------' +-------+
// - just an integer swap 0x100
// - no memory allocation
// owner data thief
// +-------+ .------. +-------+
// X<----| 0x0 | | 12 |<----| 0x100 |
// +-------+ '------' +-------+
struct Machine { struct Image {
int max_memory; int w = 0, h = 0;
float dimensions; uint32_t* pixels = nullptr; // rgba
};
int main() { int size() const { return w * h; }
// shallow / trivial copy
Machine m1 = { 1, 1.2f }, m2; Image(int width, int height)
m2.max_memory = m1.max_memory; : w(width), h(height) { }
m2.dimensions = m2.dimensions; Image() : Image(0, 0) { }
~Image() { if (pixels)
delete [] pixels; }
};
// make an image, allocate pixel data
Image owner(4, 4);
owner.pixels = new uint32_t[owner.size()];
Image clone(owner.w, owner.h);
clone.pixels = owner.pixels; // BAD! clone.~Image will try delete on dangling ptr
// deep copy - byte by byte copy: each image gets its own copy of pixels
clone.pixels = new uint32_t[owner.size()];
std::copy_n(owner.pixels, owner.size(), clone.pixels);
// move: no allocation, no deep copy
Image thief;
thief.w = owner.w; thief.h = owner.h;
std::swap(thief.pixels, owner.pixels);
owner.w = owner.h = 0; // be a responsible thief ;)
} // leave owner in a decent state
// 1. Using built-in
typedef int Id;
using GroupId = float;
enum : uint8_t Weekends { Sat, Sun };
// 2. Composite
struct Point { float x; float y; };
class Canvas { // Canvas
Point origin = {}; // +-------+-------+-------+
int max_memory = 64; // | float | float | int |
// +-------+-------+-------+
public:
Canvas(float max);
};
// /<--- c[0] --->\ /<---- c[1] --->\
// ---+-----+-----+-----+-----+-----+-----+---
// 3. Aggregate // |float|float| int |float|float| int |
int vals[3] = {0, 1, 2}; // ---+-----+-----+-----+-----+-----+-----+---
Canvas c[2] = { Canvas(1024), Canvas(512) };
.pull-left[
- When authoring a class think data first .little[- Methods are vital to class usage; think user and design]
- In C++, inheritance isn’t for code reuse .little[- It is for interface compliance; prefer composition]
- Accessors (
const
) and mutators - Well-written classes are easy to use and hard to misuse ]
.pull-right[
- Managing resources? or POD?
- Smart wrappers or scoped guards
- Keep compiler-supplied functions?
- Operators overloading?
operator=
?
- Non-friend free functions > methods .little[- Utilities shouldn’t be methods] ]
Methods are functions with a hidden first argument for this
— the object pointer
struct Widget : public Point {
void Scroll(int clicks) { y_ += (clicks * OFFSET); } // change state of object
void SetScale(int s) { scale_ = s; } // directly or indirectly
int GetScale() const { return scale_; } // just observe; const guarantees
// no state change; no write to data members
// or call to non-const methods
private:
constexpr static int OFFSET = 3; // compile-time; stripped at run-time
int scale_;
};
Roughly equivalent to
struct __Widget { // object only has data (member variables)
float x_, y_; // base class members first and in order of declaration
int scale_; // code operating on it isn't part of it
};
// code elsewhere (.text segment)
void __Widget_Scroll(__Widget* w, int clicks) {
w->y_ += (clicks * 3); // OFFSET resolved at compiled-time
}
int __GetScale(const __Widget* w) { return w->scale_; }
-
Simple Function/Method calls
- Most function calls in C++ are glorified
JMP
instructions (assembly) - Exact offset of functions’ code in memory known at compile-time (Static typing)
- Compiler dispatches function calls with
JMP
to known offset
- Most function calls in C++ are glorified
-
Sometimes exact function is known only at run-time; we need Dynamic Dispatch.red[1]
-
Useful for abstractions fulfilled by another class / programmer — interfaces, plug-ins, … i.e. to facilitate separation of concerns .red[2]
-
Example 1:
IDisplay::clear()
needn’t know if it’s aCLcdDisplay
orCCrtMonitor
-
Example 2: a text editor with plug-ins .little[
- Text editor declares, not defines, a
bool IPlugin::post-process(std::string)
- Every plug-in author implements (defines) it differently
- When compiling editor the actual function’s code is non-existent; offset unknown – can’t do static dispatch
- At run-time, if a plug-in SO/DLL is present, load function (
dlsym
orGetProcAddress
) and dispatch dynamically - .tag[Flexibility] Both editor and plug-in code can build independently! Weak coupling FTW 💪 ]
.footnote[.red[¹]: also known broadly as (run-time) polymorphism] .footnote[.red[²]: The S in [SOLID Principles](https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)]
struct IDisplay {
virtual void Init() { } // virtual, not pure virtual, method; no `= 0`
// A concrete class may / may not override.
virtual ~IDisplay() { } // Interfaces MUST have virtual destructor.
// Pure virtual makes IDisplay an abstract base class (ABC) / interface.
// It can't be instantiated; need to have a concerte implementation.
// A concrete class MUST override these to be called concrete.
virutal Size GetResolution() const = 0;
virtual void PutPixel(float x, float y, Color c) = 0;
};
struct CCrtMonitor : public IDisplay {
Size GetResolution() const override { return my_resolution_; }
void PutPixel(float x, float y, Color c) override {
/* put color in frame buffer */
}
private:
void* frame_buffer_;
Size resolution;
};
struct CProjector : public IDisplay { // still an ABC as not all pure virtuals
Size GetResolution() const { } // are overridden
};
struct CLcdDisplay : public IDisplay {
/* similar definitions */
};
CCrtMonitor m1 in memory .---> Virtual table of CCrtMonitor in ".text"
+-------------------------+ | +-------------------------------------------+
| | | | |
| void* vptr |--’ | Offset to ~CCrtMonitor() |
| | | |
|-------------------------| |-------------------------------------------|
| | | |
| void* frame_buffer_ | | Offset to Init() |
| | | |
|-------------------------| |-------------------------------------------|
| | | |
| float x_ | | Offset to GetResolution() |
| | | |
|-------------------------| |-------------------------------------------|
| | | |
| float y_ | | Offset to PutPixel(float, float, Color) |
| | | |
+-------------------------+ +-------------------------------------------+
-
Compiler inserts a
vptr
to all objects of typeCCrtMonitor
-
They all point to
CCrtMonitor::vtable
-
Objects of type
CLcdDisplay
also get their ownvptr
pointing toCLcdDisplay::vtable
struct IDisplay { virtual ~IDisplay() { } };
struct CCrtMonitor : public IDisplay { ~CCrtMonitor() { /* cleanup stuff */ } };
class CLcdDisplay : public IDisplay { /* ... some code ... */ };
IDisplay* MakeDisplay(DisplayParams params) { // factory function
if (params.resolution <= 480)
return new CCrtMonitor;
else
return new CLcdDisplay;
}
int main() {
IDisplay od; // Error: can't instantiate an abstract class
IDisplay *pd; // OK: pointer to abstract IDisplay
CCrtMonitor crtMon;
crtMon.PutPixel(10, 5, Color(255, 0, 0)); // static dispatch
pd = MakeDisplay(params);
pd->PutPixel(20, 3, Color(0, 0, 255)); // dynamic dispatch
delete pd; // Destroy object behind IDisplay*. Calls
// 1. ~CCrtMonitor properly (thanks to virtual ~IDisplay)
// 2. ~IDisplay
// 3. releases memory
// Without virtual base destructor step 1 will be skipped!
// Cleanup code in ~CCrtMonitor won't be run!!
}
.tag[Flexibility] .tag[Performance] C++ doesn’t come with garbage collection because it has RAII!
#include <cstddef> // ← for std::byte; preferred over uint8_t
struct FileReader { // a crude smart wrapper
FileReader(std::string path) {
// accquire resources in the constructor
file_ = fopen(path.c_str(), 'r');
if (file_) {
data_ = new std::byte[100];
if (data_) fread(data_, sizeof(std::byte), 100, file_);
}
}
~FileReader() {
// destruct resources in the destructor
if (file_)
fclose(file_);
if (data_)
delete [] data_;
}
private:
std::byte* data_ = nullptr;
FILE* file_ = nullptr;
};
.footnote[.red[¹]: RAII – Resource Acquisition Is Initialization – is the cornerstone idiom of modern C++ but with the worst possible name 😠]
Didn’t we say NO new
or delete
? Let’s try again.
struct FileReader { // a smart wrapper with no low-level fiddling
File(std::string path) {
file_.reset(fopen(path.c_str(), 'r'));
if (file_) {
const auto file_size = getFileSize(file_.get()); // get() gives underlying T*
data_.resize(file_size); // auto resize vector's memory
fread(data_.data(), sizeof(std::byte), file_size, file_);
}
}
// ~FileReader() — Look ma, no destructor!
// Under the hood, when some `File f` gets destroyed, these are called (in order)
// 1. ~unique_ptr<FILE, FileCloser> calls fclose()
// 2. ~vector<std::byte>() auto deletes memory its ptr_ is pointing at
// 3. ~File() finally but since it's a no-op, it'd be optimized away
private:
std::unique_ptr<FILE, FileCloser> file_; // unique_ptr auto closes file
std::vector<std::byte> data_; // vector manages bytes, auto resizes array
};
// Functor is like a function but can have states; lambda functions are functors too.
struct FileCloser { // This function takes a FILE* and returns nothing.
void operator()(FILE* f) { // Callable thus: FileCloser fc; fc(file_ptr);
if (f) fclose(f); // Usable as unique_ptr<T>'s Deleter as its prototype
} // matches it by a taking T*; here T = FILE.
};
Recursive since FileReader
– a smart wrapper – is now embed-able in another higher abstraction 💡 When that gets destroyed, FileReader
will automatically release its resources.
- Auto-generated default constructor is a no-op; members would be garbage values
- Write a constructor to initialize states / data members
- Prefer in-class initializers; sometimes no constructors may be needed
struct Point {
float x, y; // garbage by default
};
struct Circle {
float radius = 1.0f; // in-class initializer
Point centre; // garbage if created using Circle()
Circle() = default;
Circle(float r) : radius(r), centre(0.0f, 0.0f) { // member initializer list
}
static Circle* MakeCircle(float radius, Point centre) { /* return a circle */ }
};
- Prefer member initializers over setting them in the body
- Make constructors
explicit
to prevent on-the-fly creation
.little[e.g.DrawFigure(float) ≠ DrawFigure(Circle(float))
for heavy-classes, on-the-fly creation is a red flag 🚷] - Want to control creation? .little[
- Make constructors
private
- Author a
static
class function that acts as a factory (Named Constructor Idiom) ]
-
References are just nicknames; underlying object is exactly the same .little[
-
Edit the reference and you edit the value
-
const
reference simply disallows editing object through that reference ] -
Temporaries are compiler-generated objects during expression evaluation
Size Circle::ComputeBounds() { return Size(width_, height_); }
c.ComputeBounds(); // returned Size is a temporary alive until expression evaluation
- Objects only have types; expressions have a non-reference type and a value category
Image i1, &i2, &&i3; // objects of type Image, lvalue-ref & rvalue-ref to Image
std::move(i2) // expression type: Image, value category: xvalue
l-values, r-values… oh-my-values!! 🤯
-
lvalue: have identity, cannot be stolen e.g.
int a; Circle &&c;
-
xvalue: have identity, can be stolen e.g. what’s returned from
std::move(c)
-
prvalue: no identity, can be stolen e.g.
str.substr(1, 2)
name: copy-move-ctor1
struct Image {
int width_; int height_; std::vector<std::bytes> pixels_;
// copy constructor for deep copying
Image(const Image& that) : w_(that.w_), h_(that.h_) {
const auto count = w_ * h_;
if (that.pixels_ && (count > 0)) {
this->pixels_.resize(count);
std::copy_n(that.pixels_, count, this->pixels_);
}
}
// move constructor
Image(Image&& that) : w_(that.w_)
, h_(that.h_)
, pixels_(std::move(that.pixels_)) {
}
};
template: copy-move-ctor1
But wait! RAII recursively, right? Wouldn’t the defaults suffice? They totally do!! 👌
class Image {
// ... and we're done! We wisely chose RAII wrapper over raw pointers :)
// How implicitly generated defaults look like? Move, same as above. Copy:
//
// Image(const Image& that) : w_(that.w_), h_(that.h_), pixels_{that.pixels_} {
// } // std::vector's copy constructor does the right thing 👍
};
class Image {
// operator methods are just like any other methods; can be called with expression
// Image i1, i2; i1 = i2 is the same i1.operator=(i2)
// copy-assignment operator, similar to copy ctor but without the memory alloc
Image& operator= (const Image &that) {
this->w_ = that->w_;
this->h_ = that->h_;
this->pixels_ = that->pixels_; // std::vector::operator= does deep copy, yay!
return *this;
}
// move-assignment operator
Image& operator=(Image&& that) {
this->w_ = std::move(that->w_);
this->h_ = std::move(that->h_);
this->pixels_ = std::move(that->pixels_); // std::vector::operator= does move
return *this; // its stuff safely, yay!
}
// You only used smart types as members, didn't ya, you sly fox?! 😏
// Skip writing both and bask in the glory: _The Rule of Zero¹_
};
- Implicitly generated move constructor will fallback to copy if a type isn’t move-friendly e.g. is a POD or has no move constructor
- No implicit generation if even one member is non-copyable or immovable
.footnote[.red[¹]: The Rule of Three / Five / Zero]
Out
parameter a.k.a return value:X f()
– by value .little[
- Returned prvalue would get moved even without optimizations
- Zero cost due to Return Value Optimization / Copy Elision even for move-unfriendly types ]
Out
but expensive to move (e.g.std::vector<BigPOD>
):f(X&)
orf(X*)
– by reference
In/Out
parameter:f(X&)
– by reference
-
In
parameter:f(const X&)
– byconst
reference -
In
but cheap or impossible to copy (e.g.int
,std::unique_ptr
):f(X)
– by value -
In
but need ownership:f(X&&)
– rvalue reference -
In
but need copy: give two overloadsf(const X&)
–const
reference – for those who want to keep theirsf(X&&)
– rvalue reference – for those who want to give up theirs
.footnote[.red[¹]: based on Herb Sutter’s CppCon 2014 presentation; see pages 22 to 34.]
class: left, inverse name: knuth-1
template: knuth-1 name: knuth-2
.little[### We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.]
template: knuth-2 name: knuth-3
.little[##Yet we should not pass up our opportunities in that critical 3%.
template: knuth-3
We mean to eek out the very last drop of juice a CPU’s got!
???
We’ll discuss sane defaults when writing C++ software — just good habits, not pessimizations.
It’s complex and critical; comparable to a
.pull-left[
-
Kernel
-
Game engine
-
Compiler / virtual machine
-
Real-time financial trading system ]
.pull-right[
Wasting a small time delta every operation = systemic lag hurting productivity. ]
Ever wondered what language virtual machines are written in?
- C#
- Java
- Python
- JavaScript
- Mozilla’s SpiderMonkey
- Google’s V8
The Need for Speed justifies writing browsers in C++ 🏁
class: center, middle, inverse
-
Standard C and C++ Documentation
.little[Online documentation that matches C++ standard the closest.] -
ISO C++ FAQ
.little[Your first go-to when in doubt.] -
The Definitive C++ Book Guide and List
.little[Curated/updated list of book recommendations by skill level.]
-
Coliru Stacked-Crooked
.little[Minimal with Share option.] -
Compiler Explorer / Godbolt
.little[Shows disassembly mapping each line of C++ – supports numerous compilers!]