

Modern C++ Basics - Basics Review
Learning C++: Where Segmentation Fault is Just a Life Metaphor
Fundamental types and compound types#
Integer#
Char#
char
is 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
-ftrapv
option 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);
cppUnsigned 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
int
toint
first.
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");
cppThe 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::endian
to check the endianness of platform. - Use
std::byteswap
to 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 long
1u, 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 1bool
is 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
-0
inf
-inf
NaN
-
&&/|| NaN
istrue
-
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 inf
cppBit manipulation#
std::bit_cast<ToType>(fromVal)
to unsigned integer, then use<bit>
Literal prefix and suffix#
- For floating point,
1e-5
means . 1.0
–double
;1.0f
–float
;1.0L
–long double
.1
means0.1
(zero before fraction can be omitted).
Compound Types#
Cv-qualifier#
const
andvolatile
.volatile
is 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::type
orstd::underlying_type_t
to 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
,E1
is always fully evaluated beforeE2
. - For
E1 = E2
,E1 @= E2
,E2
is 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
auto
will 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
-
explicit
is 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
this
pointer (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
this
to beconst Class*
.
- This is achieved by make
- But, what if what we change is just status that user cannot know?
- You may modify
mutable
variable inconst
method. - e.g.
mutable Mutex myLock;
- Use
mutable
carefully 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.
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 ofBase
methods!
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;
}
cppTemplate 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#
struct
is almost same as class in C++, except thatstruct
usepublic
as 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
struct
in 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_ordering
orstd::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
struct
generated 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
const
inoperator()
, 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.