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
- 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
- It avoids unexpected conversions. Prevents bugs caused by accidental type conversions.
- 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
|
Pointer to Const, Const Pointer and Reference
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
#include <iostream>
using namespace std;
int main(void) {
// ---------- Pointer to Constant ----------
int a = 5;
int b = 6;
const int* ptrToConst = &a;
ptrToConst = &b; // OKEY: You can change where the pointer points.
// *ptrToConst = 20; // Error: You cannot change the value it points to.
// ---------- Constant Pointer ----------
int* const constPtr = &a;
*constPtr = 6; // OKEY: You can change the value it points to.
// constPtr = &b; // ERROR: You cannot change where the pointer points.
// ---------- Reference ----------
int& r = a;
// Essentially, it is a constant pointer: int* const r = &a;
}
|
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:
- Cannot modify any non-mutable member variables of the object.
- Can be called on const instances of the class.
- Can only call other const member functions.
volatile keyword
noexcept keyword
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
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:
- If expr’s type is a reference, ignore the reference part.
- 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:
- the constness of the object becomes part of the type deduced for T
- the reference-ness is ignored during type deduction
ParamType is a Universal Reference
Rules:
- If expr is an lvalue, both T and ParamType are deduced to be lvalue references
- 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:
- when universal references are in use, type deduction distinguishes between lvalue arguments and rvalue arguments
ParamType is Neither a Pointer nor a Reference
Rules:
- As before, if expr’s type is a reference, ignore the reference part.
- 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:
- 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:
- Function-to-pointer decay rule.
auto type deduction
A special type deduction rule for auto
When the initializer for an auto-declared variable is enclosed in braces, the deduced type is always a std::initializer_list
, but template type deduction doesn’t.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
using namespace std;
template<typename T>
void f1(T initList) {}
template<typename T>
void f2(initializer_list<T> initList) {}
int main(void) {
auto x = {1, 2, 3}; // x's type is std::initializer_list<int>
f1(x); // Works: C++ deduces x to be of type std::initializer_list<int> — this is special behavior for auto.
// f1({1, 2, 3}); // Does not work: In template type deduction, {} by itself is not a type.
f2({1, 2, 3}); // Works: it matches the special braced-initializer deduction rule
}
|
2 cases when auto employs template type deduction
auto as a return type
1
2
3
|
auto createInitList() {
return { 1, 2, 3 }; // error: can't deduce type for { 1, 2, 3 }
}
|
auto in a lambda parameter
1
2
3
4
5
6
7
8
9
10
11
|
#include <vector>
int main(void) {
std::vector<int> v;
auto resetV = [&v](const auto& newValue) { v = newValue; };
// resetV({ 1, 2, 3 }); // Does not work: error! can't deduce type for { 1, 2, 3 }
auto x = {1, 2, 3};
resetV(x); // Works: auto deduces x to be of type std::initializer_list<int>
}
// -std=c++14
|
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.
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
|
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:
- 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:
constexpr
functions can be used in contexts that demand compile-time constants.
- 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
:
Prefer scoped enums to unscoped enums
Unscoped Enum can pollute namespace
1
2
3
4
5
6
7
8
9
|
int main(void) {
// ---------- Unscoped Enum can pollute namespace ----------
enum Color { white, black, red };
// auto white = 3; // error: redefinition of 'white' as different kind of symbol
// ---------- Scoped Enum can avoid namespace pollution ----------
enum class Animal { cat, dog, bird };
auto cat = 3; // OKEY
}
|
Scoped Enums are much more strongly typed
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include <iostream>
using namespace std;
int main(void) {
enum UnscopedEnum {A};
enum class ScopedEnum {B};
// Unscoped enum values implicitly convert to their underlying integral type — usually int
cout << A << endl;
// ScopedEnum::B is not implicitly convertible to int
cout << static_cast<int>(ScopedEnum::B) << endl;
}
|
Forward Declaration for Unscoped Enum
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
|
#include <iostream>
// Forward declaration of the enum
// Specify the underlying type for an unscoped enum
enum Color : int;
void paint(Color c); // Use Color before its full definition
// Full definition of the enum
// Underlying type specifications should also go on an enum’s definition
enum Color : int {
Red,
Green,
Blue
};
void paint(Color c) {
switch (c) {
case Red: std::cout << "Red\n"; break;
case Green: std::cout << "Green\n"; break;
case Blue: std::cout << "Blue\n"; break;
}
}
int main() {
paint(Green);
return 0;
}
|
Lambda Capture Modes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#include <iostream>
using namespace std;
int main(void) {
// ---------- Capture by value ----------
int x = 10;
auto lambda_value = [x]() {
cout << x << endl; // prints 10
};
lambda_value();
// ---------- Capture by reference ----------
auto lambda_reference = [&x]() {
x += 10;
};
lambda_reference();
cout << x << endl; // prints 20
}
|
Default by-reference capture can lead to dangling reference
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#include <iostream>
#include <functional>
std::function<void()> make_lambda() {
int x = 42;
// Capture everything by reference
return [&]() {
std::cout << x << std::endl; // Dangling reference!
};
}
int main() {
auto lambda_dangling_reference = make_lambda(); // x is destroyed when make_lambda() returns
lambda_dangling_reference();
// prints 0, once make_lambda() returns, x is gone
// its memory on the stack is invalid.
// So calling the lambda later accesses garbage.
return 0;
}
// Compile: g++ -O2 dangling_reference.cpp -o main -std=c++11
|
Default by-value capture is susceptible to dangling pointers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#include <iostream>
#include <functional>
std::function<void()> make_lambda() {
int* ptr = new int(42); // dynamically allocated
auto lambda = [ptr]() { // ptr is copied (by value), but it's still a raw pointer
std::cout << *ptr << std::endl;
};
delete ptr; // pointer now dangles!
return lambda; // lambda holds a dangling pointer
}
int main() {
auto lambda = make_lambda();
lambda(); // dereferencing dangling pointer
// prints 0
return 0;
}
|
Static and global variables cannot be captured
1
2
3
4
5
6
7
8
9
10
11
|
#include <iostream>
int global_x = 42;
auto lambda = []() {
std::cout << global_x << std::endl; // accesses directly
};
int main() {
lambda(); // prints 42
}
|
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.