dokee's site

Back

Modern C++ Basics - Basics ReviewBlur image

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);
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 type T.

Integer promote#

  • All arithmetic operations will promote integers that are smaller than int to int 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");
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::endian to check the endianness of platform.
  • Use std::byteswap to swap an integer byte by byte.

Literal prefix and suffix#

  • 10 – decimal
  • 0x10 – hexadecimal (get 16)
  • 010 – octal (get 8)
  • 0b0011'0100 – binary (get 2)
  • 1int; 1l, 1Llong; 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 1
  • bool is not allowed to ++/-- since C++17.

Floating points#

What is it?#

  • sign bit + exponent field + fraction field

    • float: 23 fraction + 8 exponent
    • double: 52 fraction + 11 exponent
  • 0 -0 inf -inf NaN

  • &&/|| NaN is true

  • inf+(-inf)= NaN

  • <stdfloat>: They are all different types rather than alias!

    • std::float16_t: 10+5
    • std::float32_t: 23+8
    • std::float64_t: 52+11
    • std::float128_t: 112+15
    • std::bfloat16_t: 7+8

Accuracy#

  • For normalized numbers of float, plus and minus will have effects iff. their magnitude is in 2241.6e72^{24} \sim 1.6 e^{7}
float a = 0.1f; // -> 0.100000001490116119384765625
double z = tan(pi/2.0); // -> 16331239353195370.0, not inf
cpp

Bit manipulation#

  • std::bit_cast<ToType>(fromVal) to unsigned integer, then use <bit>

Literal prefix and suffix#

  • For floating point, 1e-5 means 10510^{−5}.
  • 1.0double; 1.0ffloat; 1.0Llong double
  • .1 means 0.1 (zero before fraction can be omitted).

Compound Types#

Cv-qualifier#

  • const and volatile.
  • 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]);
cpp

VLA 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 or std::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 before E2.
  • For E1 = E2, E1 @= E2, E2 is always fully evaluated before E1.

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
  • int a; vs int 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!

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 type Class*) as parameter.
  • Sometimes you may hope methods unable to modify data members, then you need a const.
    • This is achieved by make this to be const Class*.
  • But, what if what we change is just status that user cannot know?
    • You may modify mutable variable in const method.
    • e.g. mutable Mutex myLock;
    • Use mutable carefully in restricted cases.
#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);
}
cpp

Inheritance#

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*/& by Base*/& will fill in the default param of Base 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;
}
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;
    }
};
cpp
Pure 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 that struct use public 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-like struct. e.g. reference.

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;
}
cpp

Three-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 returns std::strong_ordering, std::weak_ordering or std::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 an operator() const.
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 in operator(), 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.
  • 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;
}
cpp

Template#

  • 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.
Modern C++ Basics - Basics Review
https://astro-pure.js.org/blog/c/modern-c-basics/basics-review
Author dokee
Published at March 2, 2025
Comment seems to stuck. Try to refresh?✨