

Modern C++ Basics - Basics Review
Learning C++: Where Segmentation Fault is Just a Life Metaphor
Fundamental types and compound types#
Integer#
Char#
charis not guaranteed to be signed or unsigned.- If you hope to use exact signed one, you need to explicitly use
signed char.
Signed integers overflow#
- It’s UB for signed integers to overflow.
- you can use
-ftrapvoption to trap for signed overflow in addition.
int a = std::numeric_limits<int>::max();
int b = 1;
int result = a + b;
std::println("Result of a + b: {}", result);cpp
Unsigned integer is always >= 0#
- There are many hidden bugs related to this, e.g.
std::string::npos. std::in_range(x)can be used to check whether a value is representable by integer typeT.
Integer promote#
- All arithmetic operations will promote integers that are smaller than
inttointfirst.
uint32_t a32 = 0xffffffff, b32 = 0x00000001, c32 = 0xffffffff;
uint8_t a8 = 0xff, b8 = 0x01, c8 = 0xff;
std::println("sizeof(int) : {}", sizeof(int));
std::println("uint32_t : {}", a32 + b32 > c32 ? "unexpected!" : "ok");
std::println("uint8_t : {}", a8 + b8 > c8 ? "unexpected!" : "ok");cpp
The size of integers#
-
What’s the size of integers? (int/long/pointer)
- ILP32(4/4/4): Widely used in 32-bit system
- LLP64(4/4/8): Widely used in Windows
- LP64(4/8/8): Widely used in Linux and MacOS
-
get special values for an arithmetic type, you may use
<limits>std::numeric_limits::max()get the maximum finite number. (Be careful when using it with floating points)
Bit manipulation#
<bit>: for unsigned integers.- Use
std::endianto check the endianness of platform. - Use
std::byteswapto swap an integer byte by byte.
Literal prefix and suffix#
10– decimal0x10– hexadecimal (get 16)010– octal (get 8)0b0011'0100– binary (get 2)1–int;1l, 1L–long;1ll, 1LL-long long1u, 1ull, 1llu-unsigned- Separator is supported, i.e.
0x11FF’3344;0b0011’0100
Bool#
- Special integer, only true (non-zero, not definitely 1) or false.
- Convert it to other integer types will get 1/0.
sizeof(bool)is not necessarily 1boolis not allowed to++/--since C++17.
Floating points#
What is it?#
-
sign bit + exponent field + fraction field
float: 23 fraction + 8 exponentdouble: 52 fraction + 11 exponent
-
0-0inf-infNaN -
&&/|| NaNistrue -
inf+(-inf)= NaN -
<stdfloat>: They are all different types rather than alias!std::float16_t: 10+5std::float32_t: 23+8std::float64_t: 52+11std::float128_t: 112+15std::bfloat16_t: 7+8
Accuracy#
- For normalized numbers of float, plus and minus will have effects iff. their magnitude is in
float a = 0.1f; // -> 0.100000001490116119384765625
double z = tan(pi/2.0); // -> 16331239353195370.0, not infcppBit manipulation#
std::bit_cast<ToType>(fromVal)to unsigned integer, then use<bit>
Literal prefix and suffix#
- For floating point,
1e-5means . 1.0–double;1.0f–float;1.0L–long double.1means0.1(zero before fraction can be omitted).
Compound Types#
Cv-qualifier#
constandvolatile.volatileis used to force the compiler to always get the content from memory.
Array-to-pointer conversion (decay)#
- The following three function declaration forms are completely equivalent, all decay to pointer
void arraytest(int *a);
void arraytest(int a[]);
void arraytest(int a[3]);cpp- The following retain the dimension information of the array (using references)
void arraytest(int (&a)[3]);cppVLA is not allowed#
- VLA (i.e.
int arr[n]) is not allowed in C++
Enumeration#
-
You need to ensure not to exceed the limit of enumeration value in the meaning of bits (i.e.
(1 << std::bitwidth(MaxEnum)) – 1), otherwise UB. -
For scoped enumeration, only comparisons are permitted; arithmetic operations will trigger compile error.
-
If you really want to do integer operations, you need to convert it explicitly.
-
use
std::underlying_type::typeorstd::underlying_type_tto get the integer type. -
use
std::to_underlying(day)to get the underlying integer directly;
Expression#
- Parameters in function are evaluated indeterminately, i.e. every sub-tree represented by the parameter is fully evaluated in a non-overlap way!
- Every overloaded operator has the same evaluation rule as the built-in operators, rather than only be treated as a function call.
- For
E1[E2],E1.E2,E1 << E2,E1 >> E2,E1is always fully evaluated beforeE2. - For
E1 = E2,E1 @= E2,E2is always fully evaluated beforeE1.
Class#
Ctor and dtor#
Initialization#
-
Reasons for using Uniform Initialization
{}:- (Almost) all initialization can be done by curly bracket
{} - Compared to
(), it will strictly check Narrowing Conversion - It can also prevent most vexing parse
- (Almost) all initialization can be done by curly bracket
-
int a;vsint a {};: Compared to default initialization, value initialization will zero-initialize those do not have a default ctor. -
Notice that
autowill behave in a weird way.- e.g.
auto a = {1}, auto is initializer list!
- e.g.
Ctor#
-
Member has been default-initialized before ctor enters the function body.
-
Use member initializer list or In-Class Member Initializer.
-
It’s dangerous if you use members behind to initialize previous members in ctor! ->
-Wall -
explicitis used to prevent the compiler from using the constructor for implicit conversions.
Copy ctor#
// Copy Ctor
Class(const Class& another) {
}
// operator=
Class& operator=(const Class& another) {
/*do something*/
return *this;
}cpp- Pay attention to self-assignment, especially when you use pointers.
Member functions#
- All non-static member functions in a class implicitly have a
thispointer (with the typeClass*) as parameter. - Sometimes you may hope methods unable to modify data members, then you need a
const.- This is achieved by make
thisto beconst Class*.
- This is achieved by make
- But, what if what we change is just status that user cannot know?
- You may modify
mutablevariable inconstmethod. - e.g.
mutable Mutex myLock; - Use
mutablecarefully in restricted cases.
- You may modify
#include <vector>
#include <mutex>
#include <print>
class StudentGrades {
public:
// Non-const version allows modification
int& operator[](size_t index) {
std::lock_guard<std::mutex> lock(mtx_);
cache_valid_ = false;
return grades_[index];
}
// Const version for read-only access
const int& operator[](size_t index) const {
std::lock_guard<std::mutex> lock(mtx_);
return grades_[index];
}
// Cached average calculation (const method)
double get_average() const {
std::lock_guard<std::mutex> lock(mtx_);
if (!cache_valid_) {
recalculate_average();
std::println("[Cache updated]");
}
return cached_average_;
}
void add_grade(int grade) {
std::lock_guard<std::mutex> lock(mtx_);
grades_.push_back(grade);
cache_valid_ = false;
}
private:
void recalculate_average() const {
cached_average_ = 0;
for (int g : grades_) cached_average_ += g;
if (!grades_.empty()) cached_average_ /= static_cast<double>(grades_.size());
cache_valid_ = true;
}
mutable std::mutex mtx_; // Thread-safe lock
mutable bool cache_valid_{false};
mutable double cached_average_{0};
std::vector<int> grades_;
};
// Function accepting a const object
void analyze_grades(const StudentGrades& sg) {
std::println("Average grade analysis: {}", sg.get_average());
}
int main() {
StudentGrades grades;
grades.add_grade(85);
grades.add_grade(92);
analyze_grades(grades); // First calculation
grades[1] = 95; // Invalidate cache
analyze_grades(grades); // Recalculate
const auto& const_view = grades;
std::println("First grade: {}", const_view[0]); // Use const version
analyze_grades(const_view);
}cppInheritance#
Polymorphism#
- In C++, polymorphism is valid only in pointer and reference.
Virtual methods#
- you should usually make dtor of base class
virtual. - You can change the access control specifier of virtual methods, but it’ll collapse the access barrier, so it should be avoided to use.
- Member function template cannot be virtual.
override and final#
-
It’s recommended to use
override. -
The meaning of “override” is not specialized for virtual methods like the keyword
override.- If you name a non-virtual function with the same name as parent’s, you’ll also override it.
- You need to use
Parent::Func()to call the parent version.
-
There is another similar specifier
final.- It means override, and the derived class cannot override again.
- It may do optimization in virtual table. (devirtualization)
Virtual methods with default params#
- When virtual methods have default params, calling methods of
Derived*/&byBase*/&will fill in the default param ofBasemethods!
void Parent::go(int i = 2) {
std::println("Base's go with i = {}.", i);
}
void Child::go(int i = 4) {
std::println("Derived's go with i = {}.", i);
}
int main() {
Child child;
child.go();
Parent& childRef = child;
childRef.go();
return 0;
}cpp
Template method pattern and CRTP#
- There is a pattern called template method pattern that utilize private virtual method. What derived classes need to do is just overriding those virtual methods.
class Student {
public:
float getGpa() const {
return getGpaCoeff() * 4.0f;
}
private:
virtual float getGpaCoeff() const {
return 1.0f;
}
};
class Tom : public Student {
float getGpaCoeff() const override {
return 0.8f;
}
};cpp- CRTP (curiously recurring template pattern)
template <typename Derived>
class Student {
public:
float getGpa() {
return static_cast<Derived*>(this)->getGpaCoeff() * 4.0f;
}
};
class Tom : public Student<Tom> {
public:
float getGpaCoeff() const {
return 0.8f;
}
};cppPure virtual function#
- It’s UB to call pure virtual function by vptr.
- Don’t call any virtual function and any function that calls virtual function in ctor & dtor!
class Base {
public:
Base() {
reallyDoIt(); // calls virtual function!
}
void reallyDoIt() {
doIt();
}
virtual void doIt() const = 0;
};
class Derived : public Base {
void doIt() const override {}
};
int main() {
Derived d; // error!
return 0;
}cpp
- Pure virtual functions can have definition, but it should be split from the prototype in the class.
- if you define a pure virtual dtor, it must have a definition (though likely do nothing).
- if you hope to provide a default behavior, but don’t want the derived class to directly accept it.
Struct#
structis almost same as class in C++, except thatstructusepublicas the default access control.- Aggregate struct can use designated initialization.
- Only data members exist and are all public.
- They shouldn’t have member functions. At most it has ctor & dtor & some operators.
- Array cannot use designated initialization though it’s an aggregate.
- There exists a special kind of “
struct” called bit field- If bit amount exceeds the type(e.g. use 9 bits for
unsigned char), it will be truncate to the max bits. - Much syntax in normal
structin C++ is invalid in bit field, and you may treat it as C-likestruct. e.g. reference.
- If bit amount exceeds the type(e.g. use 9 bits for
Function overloading#
- This is done by compilers using a technique called name mangling.
- Return type does NOT participate in name mangling. (Except that it’s a template parameter, since all template parameters are part of mangled name.)
Operator overloading#
Ambiguity#
- Conversion operator may cause ambiguity so that the compiler reports error. ->
explicit
class Real {
public:
Real(float f) : // use explicit to avoid ambiguity
val { f } {
}
Real operator+(const Real& a) const {
return Real{val + a.val};
}
operator float() {
return val;
}
private:
float val;
};
int main() {
Real a { 1.0f };
Real b = a + Real{0.1f}; // ok
// Real b = a + 0.1f; // error, ambiguous
return 0;
}cppThree-way comparison <=>#
struct Data {
int id;
float val;
auto operator<=>(const Data& another) const {
return val <=> another.val;
}
bool operator==(const Data& another) const {
return val == another.val;
}
};
int main() {
Data a { 0, 0.1f };
Data b { 1, 0.2f };
a < b; a <= b; a > b; a >= b; // boost by <=>
a == b; a != b; // boost by ==
return 0;
}cpp-
<=>actually returnsstd::strong_ordering,std::weak_orderingorstd::partial_ordering- The ordering can be compared with 0.
- You can also use bool
std::is_lt/eq/gt/lteq/gteq/neq(ordering)to judge what it is.
-
You may notice that
<=>is enough to know==, so why do we need to overload it manually?==may be cheaper.
Lambda expression#
- Since C++11, lambda expression is added as “temporary functor” or closure.
- It is just an anonymous
structgenerated by the compiler, with anoperator() const.
- It is just an anonymous
int main() {
int copy { 0 };
int ref { 0 };
auto f {
[c = copy, &ref]() {
return c + ref;
}
};
f();
return 0;
}
// same as
int main() {
int copy { 0 };
int ref { 0 };
class __lambda_5_9 {
public:
inline /*constexpr */ int operator()() const {
return c + ref;
}
private:
int c;
int& ref;
public:
__lambda_5_9(int& _c, int& _ref): c { _c }, ref { _ref } {}
};
__lambda_5_9 f = { __lambda_5_9 { copy, ref } };
f.operator()();
return 0;
}cpp-
If you use lambda expression in a non-static class method and you want to access all of its data members, you may need to explicitly capture
this(by reference, since only copy pointer) or*this(really copy all members).- It’s recommended to capture data members directly.
-
You may add specifiers after ().
- mutable: since C++17, remove
constinoperator(), i.e. for capture by value, you can modify them (but don’t really affect captured variable). - static: since C++23, same as
static operator()(it’s always better to use it if you have no capture!). constexpr/consteval/noexcept: we’ll introduce them in the future.
- mutable: since C++17, remove
-
It’s also legal to add attributes between
[]and().
Improvement in execution flow#
if(using T = xx; …);for(auto vec = getVec(); auto& m : vec);using enum VeryLongName;[[fallthrough]];
enum class VeryLongName {
LONG,
LONG_LONG,
LONG_LONG_LONG,
};
auto val = VeryLongName::LONG;
switch (val) {
using enum VeryLongName;
case LONG:
foo();
[[fallthrough]];
case LONG_LONG:
foo();
break;
case LONG_LONG_LONG:
foo();
break;
default:
break;
}cppTemplate#
- Since C++20, abbreviated function template is introduced.
void foo(const auto& a, const auto& b) {}
// same as
template <typename T1, typename T2>
void foo(const T1& a, const T2& b) {}cpp- Lambda expression can also use template.
auto less = [](const auto& a, const auto& b) static { return a < b; };
auto less = []<typename T>(const T& a, const T& b) static { return a < b; };cpp- It’s strongly recommended not mixing abbreviated template with template, which will cause many subtle problems.