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
|
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.
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
:
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.