Featured image of post Advanced C++ Topics

Advanced C++ Topics

After 2.5 years of working professionally as a software engineer, I’ve come to realize how deep and nuanced modern C++ can be—far beyond the basics most of us learn in school or pick up on the job. This blog is my personal space to revisit, revise, and strengthen my understanding of advanced C++ topics. I hope you'll find something useful here.

Many of the insights in this blog are inspired by Scott Meyers’ Effective Modern C++, a must-read for any developer looking to deepen their understanding of C++11 and C++14.


Constructors

Parameterized Constructor

Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <string>

class Animal {
protected:
    std::string name;
    std::string species;

public:
    Animal(const std::string& n, const std::string& s) 
        : name(n), species(s) {
        std::cout << "Animal constructor: Name = " << name 
                  << ", Species = " << species << "\n";
    }
};

class Dog : public Animal {
private:
    std::string breed;
    int age;

public:
    // Dog constructor now passes both name and species to Animal
    Dog(const std::string& n, const std::string& s, const std::string& b, int a)
        : Animal(n, s), breed(b), age(a) {
        std::cout << "Dog constructor: Breed = " << breed 
                  << ", Age = " << age << "\n";
    }
};

int main() {
    Dog d("Buddy", "Canine", "Golden Retriever", 3);
}

/* output
Animal constructor: Name = Buddy, Species = Canine
Dog constructor: Breed = Golden Retriever, Age = 3
*/

Copy Constructor

Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Buffer {
private:
    int* data;

public:
    Buffer(int value) {
        data = new int(value);
    }

    // Copy constructor (deep copy)
    Buffer(const Buffer& other) {
        data = new int(*other.data);  // Allocate new memory
        std::cout << "Deep copy constructor called\n";
    }

    ~Buffer() {
        delete data;
    }
};

Purpose

  1. define a copy constructor when your class manages resources that require a deep copy

Move Constructor

Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <string>

class Resource {
private:
    std::string* data;

public:
    Resource(const std::string& val) : data(new std::string(val)) {}

    // Move constructor
    Resource(Resource&& other) noexcept : data(other.data) {
        other.data = nullptr;  // leave moved-from object valid
        std::cout << "Move constructor called\n";
    }

    ~Resource() {
        delete data;
    }

    void print() const {
        if (data) {
            std::cout << *data << std::endl;
        } else {
            std::cout << "(empty)" << std::endl;
        }
    }
};

int main() {
    Resource r1("hello");
    Resource r2 = std::move(r1); // calls move constructor
    r2.print();
    r1.print(); // should be empty or null
}

/*
Move constructor called
hello
(empty)
*/

Purpose

  • A move constructor is used to transfer (not copy) resources from a temporary object (rvalue) to a new object. It avoids expensive deep copies and enables performance optimizations.

Explicit Constructor

Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>

class Foo {
public:
    explicit Foo(int x) { std::cout << "Foo(int)\n"; }  // no explicit here
};

void bar(Foo f) {}

int main() {
    Foo f1(10);      // OK: direct initialization
    // Foo f2 = 10;  // Error: copy initialization requires implicit conversion, which is disabled by explicit
    // bar(10);      // Error: bar takes Foo, so int to Foo conversion needed (disabled)
    bar(Foo(10));    // OK: explicit constructor call also works
}

Purpose

  1. It avoids unexpected conversions. Prevents bugs caused by accidental type conversions.
  2. Helps write clearer code by forcing you to be explicit when creating objects.

Compiler Auto-generates …

1
2
3
4
5
6
Point();                               // default constructor
Point(const Point&);                   // copy constructor
Point(Point&&);                        // move constructor (C++11+)
Point& operator=(const Point&);        // copy assignment
Point& operator=(Point&&);             // move assignment
~Point();                              // destructor

const function

  • Definition: In C++, a const function is a member function that promises not to modify the object on which it is called. It is declared by placing the const keyword after the function’s parameter list. This is only relevant for member functions of classes or structs.

Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

class MyClass {
    int value;
public:
    MyClass(int v) : value(v) {}
    
    int getValue() const {
        // value = 5; //  Error: cannot modify member variable
        return value;
    }
};

void printValue(const MyClass& obj) {
    std::cout << obj.getValue();
}


int main(void) {
    MyClass myClass(10);
    printValue(myClass);
}

Features:

  1. Cannot modify any non-mutable member variables of the object.
  2. Can be called on const instances of the class.
  3. Can only call other const member functions.

volatile keyword

  • volatile tells the compiler:

    • “Don’t assume this variable stays the same — its value might change at any time, even if your code doesn’t touch it.”
  • Therefore:

    • Every time the variable is used, the compiler must reload it from memory
    • It cannot cache the value in a register or optimize out reads/writes

noexcept keyword

  • noexcept tells the compiler:

    • “This function is guaranteed not to throw any exceptions.”
  • Usage:

    • Performance: Functions marked noexcept can enable compiler optimizations.
    • Correctness: If a noexcept function does throw, the program will call std::terminate() and crash — this makes it clear something went wrong.
    • STL Compatibility: The Standard Library often prefers or requires noexcept move constructors and destructors for performance.

lvalue and rvalue

  • An lvalue refers to an object that has a name and a memory address
  • An rvalue is a temporary value that does not have a name and cannot be assigned to.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <iostream>

void check(int& x) {
    std::cout << "Lvalue\n";
}

void check(int&& x) {
    std::cout << "Rvalue\n";
}

int main() {
    int a = 10;

    check(a);               // Lvalue
    check(20);              // Rvalue
    check(std::move(a));    // Rvalue
}

Universal Reference

  • A universal reference is a reference that can bind to both lvalues and rvalues.
  • It’s written as T&& in a function template, where T is a deduced template parameter.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>

template<typename T>
void printType(T&& param) {
    if constexpr (std::is_lvalue_reference<decltype(param)>::value)
    // if constexpr: Like a regular if, but evaluated at compile time
    // decltype(param): This gives the exact type of the variable param, incl reference qualifiers
        std::cout << "Lvalue\n";
    else
        std::cout << "Rvalue\n";
}

int main() {
    int x = 10;
    printType(x);               // Lvalue
    printType(42);              // Rvalue
    printType(std::move(x));    // Rvalue
}
  • Case 1: lvalue
    • T = int& (because x is an lvalue)
    • param is of type int& && → collapses to int&
  • Case 2: rvalue
    • T = int
    • param is int&&

Deducing Types

1
2
3
4
template<typename T>
void f(ParamType param);

f(expr); // call f with some expression
  • The type deduced for T is dependent not just on the type of expr, but also on the form of ParamType. There are three cases.

Three cases

ParamType is a Reference or Pointer, but not a Universal Reference

Rules:

  1. If expr’s type is a reference, ignore the reference part.
  2. Then pattern-match expr’s type against ParamType to determine T.

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template<typename T>
void f(T& param); // param is a reference

int x = 27; // x is an int
const int cx = x; // cx is a const int
const int& rx = x; // rx is a reference to x as a const int

f(x); // T is int, param's type is int&
f(cx); // T is const int, param's type is const int&
f(rx); // T is const int, param's type is const int&

Features:

  1. the constness of the object becomes part of the type deduced for T
  2. the reference-ness is ignored during type deduction

ParamType is a Universal Reference

Rules:

  1. If expr is an lvalue, both T and ParamType are deduced to be lvalue references
  2. If expr is an rvalue, the “normal” (i.e., Case 1) rules apply.

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
template<typename T>
void f(T&& param); // param is now a universal reference

int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before

f(x); // x is lvalue, so T is int&, param's type is also int&
f(cx); // cx is lvalue, so T is const int&, param's type is also const int&
f(rx); // rx is lvalue, so T is const int&, param's type is also const int&
f(27); // 27 is rvalue, so T is int, param's type is therefore int&&

Features:

  1. when universal references are in use, type deduction distinguishes between lvalue arguments and rvalue arguments

ParamType is Neither a Pointer nor a Reference

Rules:

  1. As before, if expr’s type is a reference, ignore the reference part.
  2. Ignoring expr’s reference-ness, and const-ness, volatile-ness.

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template<typename T>
void f(T param); // param is now passed by value

int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before

f(x); // T's and param's types are both int
f(cx); // T's and param's types are again both int
f(rx); // T's and param's types are still both int

Features:

  1. const (and volatile) is ignored only for by-value parameters

An interesting case:

1
2
3
4
5
6
template<typename T>
void f(T param); // param is still passed by value

const char* const ptr = "Fun with pointers"; // ptr is const pointer to const object

f(ptr); // pass arg of type const char * const
  • const-ness of ptr will be ignored, and the type deduced for param will be const char*
  • The const-ness of what ptr points to is preserved during type deduction, but the const-ness of ptr itself is ignored when copying it to create the new pointer, param.

Array Arguments

Example 1:

1
2
3
4
5
6
7
template<typename T>
void f(T param); // template with by-value parameter

const char name[] = "J. P. Briggs";
const char * ptrToName = name;

f(name); // name is array, but T deduced as const char*

Feature 1:

  • Array-to-pointer decay rule: array declaration is treated as a pointer declaration

Example 2:

1
2
3
4
5
6
template<typename T>
void f(T& param); // template with by-reference parameter

const char name[] = "J. P. Briggs";

f(name);  // T is deduced as const char (&)[13]

Feature 2:

  • Functions can declare parameters that are references to arrays.
  • It enables creation of a template that deduces the number of elements that an array contains:

Function Arguments

Example

1
2
3
4
5
6
7
8
9
void someFunc(int, double); // someFunc is a function, type is void(int, double)

template<typename T>
void f1(T param); // in f1, param passed by value
template<typename T>
void f2(T& param); // in f2, param passed by ref

f1(someFunc); // param deduced as ptr-to-func, type is void (*)(int, double)
f2(someFunc); // param deduced as ref-to-func, type is void (&)(int, double)

Features:

  1. Function-to-pointer decay rule.

Distinguish between () and {} when creating objects

Uniform Initialization: Introduced in c++11, a single initialization syntax that can, at least in concept, be used anywhere and express everything. It’s based on braces.

Usage of Uniform Initialization

Specify the initial contents of a container

Example

1
std::vector<int> v{ 1, 3, 5 }; // v's initial content is 1, 3, 5

Prohibit implicit narrowing conversions among built-in types

Example

1
2
3
4
5
double x, y, z;

int sum1{ x + y + z }; // error! sum of doubles may not be expressible as int

int sum2(x + y + z); // okay (value of expression truncated to an int)

Default-construct an object, not to declare a function

Example

1
2
Widget w2(); // most vexing parse! declares a function named w2 that returns a Widget!
Widget w3{}; // calls Widget ctor with no args

Drawback of Uniform Initialization

Strong preference the call to the constructor of std::initializer_list (Constructor Overloading)

1
2
3
4
5
6
7
8
9
class Widget {
public:
 Widget(int i, bool b); 
 Widget(int i, double d);
 Widget(std::initializer_list<long double> il);
}

Widget w1(10, true); // uses parens and, as before, calls first ctor
Widget w2{10, true}; // uses braces, but now calls std::initializer_list ctor (10 and true convert to long double)

Use constexpr whenever possible

  • Definition: constexpr is a C++ keyword used to indicate that a value or function can be evaluated at compile time. It helps write faster and safer code by enabling computations to be done during compilation rather than at runtime

constexpr object

Example

1
2
3
4
5
6
int sz; // non-constexpr variable
constexpr auto arraySize1 = sz; // error! sz's value not known at compilation
std::array<int, sz> data1; // error! same problem

constexpr auto arraySize2 = 10; // fine, 10 is a compile-time constant
std::array<int, arraySize2> data2; // fine, arraySize2 is constexpr

Fearures:

  1. all constexpr objects are const, but not all const objects are constexpr, because const objects need not be initialized with values known during compilation.

constexpr function

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Point
{
public:
    constexpr Point(double xVal = 0, double yVal = 0) noexcept
        : x(xVal), y(yVal)
    {
    }
    constexpr double xValue() const noexcept { return x; }
    constexpr double yValue() const noexcept { return y; }
    constexpr void setX(double newX) noexcept { x = newX; }
    constexpr void setY(double newY) noexcept { y = newY; }

private:
    double x, y;
};

constexpr Point midpoint(const Point &p1, const Point &p2) noexcept
{
    return {(p1.xValue() + p2.xValue()) / 2,  // call constexpr
            (p1.yValue() + p2.yValue()) / 2}; // member funcs
}

int main(void)
{
    constexpr Point p1(9.4, 27.7);         // fine, "runs" constexpr ctor during compilation
    constexpr Point p2(28.8, 5.3);         // also fine
    constexpr auto mid = midpoint(p1, p2); // init constexpr object w/result of constexpr function
}

// g++ constexpr.cpp -o constexpr -std=c++14

Features:

  1. constexpr functions can be used in contexts that demand compile-time constants.
  2. When a constexpr function is called with one or more values that are not known during compilation, it acts like a normal function.

Using constexpr v.s. not using constexpr:

Reference

  • Many of the insights in this blog are inspired by Scott Meyers’ Effective Modern C++, a must-read for any developer looking to deepen their understanding of C++11 and C++14.
Built with Hugo
Theme Stack designed by Jimmy