COMP6771 Advanced C++ Programming
Week 3.1 Class Types
1
Why?
In this lecture
The rules around scope and class/object types are fundamental to understanding how your C++ code works.
What?
Scope Class types switch
2
Scope
The scope of a variable is the part of the program where it is accessible
Scope starts at variable definition
Scope (usually) ends at next “}”
You’re probably familiar with this even if you’ve never seen the term
Define variables as close to first usage as possible
This is the opposite of what you were taught in first year undergrad
Defining all variables at the top is especially bad in C++
1
2
3
4
5
6
7
8 9{
10
11
12
13
14
15
16
int i = 2;
std::cout << i << "\n";
int i = 3;
std::cout << i << "\n";
}
std::cout << i << "\n";
}
std::cout << i << "\n";
#include
int i = 1;
int main() {
}
std::cout << i << "\n";
if (i > 0) {
lecture-3/demo301-scope.cpp
3.1
Scope
Ways that we create scopes?
Classes Namespaces Functions Global
Random braces
3.2
Object Lifetimes
An object is a piece of memory of a specific type that holds some data
All variables are objects
Unlike many other languages, this does not add overhead
Object lifetime starts when it comes in scope
“Constructs” the object
Each type has 1 or more constructor that says how to construct it
Object lifetime ends when it goes out of scope
“Destructs” the object
Each type has a different “destructor” which tells the compiler how to destroy it
This is the behavior that primitive types follow, but you probably knew that intuitively. With classes, we tend to think a bit more explicitly about it.
3.3
Construction
Eg. https://en.cppreference.com/w/cpp/container/vector/vector Generally use () to call functions, and {} to construct objects
() can only be used for functions, and {} can be used for either There are some rare occasions these are different
Sometimes it is ambiguous between a constructor and an initialize list
1 auto main() -> int {
2 // Always use auto on the left for this course, but you may see this elsewhere.
3 std::vector
4
5 // There’s no difference between these:
6 // T variable = T{arg1, arg2, …}
7 // T variable{arg1, arg2, …}
8 auto v12 = std::vector
9 auto v13 = std::vector
10
11 {
12 auto v2 = std::vector
13 auto v3 = std::vector
14 } // v2 and v3 destructors are called here
15
16 auto v41 = std::vector
17 auto v42 = std::vector
18 } // v11, v12, v13, v41, v42 destructors called here
lecture-3/demo302-construction.cpp
3.4
Construction
Also works for your basic types
But the default constructor has to be manually called
This potential bug can be hard to detect due to how function stacks work (variable may happen to be 0)
Can be especially problematic with pointers
1 #include
3 double f() {
4 return 1.1; 5}
6
7 int main() {
8 // One of the reasons we do auto is to avoid ununitialized values.
9 // int n; // Not initialized (memory contains previous value)
10
11 int n21{}; // Default constructor (memory contains 0)
12 auto n22 = int{}; // Default constructor (memory contains 0)
13 auto n3{5};
14
15 // Not obvious you know that f() is not an int, but the compiler lets it through.
16 // int n43 = f();
17
18 // Not obvious you know that f() is not an int, and the compiler won’t let you (narrowing
19 // conversion)
20 // auto n41 = int{f()};
21
22 // Good code. Clear you understand what you’re doing.
23 auto n42 = static_cast
24
25 // std::cout << n << "\n";
26 std::cout << n21 << "\n";
27 std::cout << n22 << "\n";
28 std::cout << n3 << "\n";
29 std::cout << n42 << "\n";
30 }
lecture-3/demo303-construction2.cpp
3.5
Why are object lifetimes useful?
Can you think of a thing where you always have to remember to do something when you're done?
What happens if we omit f.close() here (assume similar behavior to c/java/python)?
How easy to spot is the mistake
How easy would it be for a compiler to spot this mistake for us?
How would it know where to put the f.close()?
1
2
3
4
5 6}
void ReadWords(const std::string& filename) {
std::ifstream f{filename};
std::vector
std::copy(std::istream_iterator
f.close();
3.6
Namespaces
Used to express that names belong together.
// lexicon.hpp
namespace lexicon {
std::vector
void write_lexicon(std::vector
} // namespace lexicon
4.1
Namespaces
Used to express that names belong together.
// lexicon.hpp
namespace lexicon {
std::vector
void write_lexicon(std::vector
} // namespace lexicon
Prevent similar names from clashing.
// word_ladder.hpp
namespace word_ladder {
std::unordered_set
} // namespace word_ladder
4.1
Namespaces
// word_ladder.hpp
namespace word_ladder {
std::unordered_set
} // namespace word_ladder
// read_lexicon.cpp
namespace word_ladder {
std::unordered_set
// open file…
// read file into std::unordered_set…
// return std::unordered_set
}
} // namespace word_ladder
4.2
Nested namespaces
namespace comp6771::word_ladder {
std::vector
word_ladder(std::string const& from, std::string const& to);
} // namespace comp6771::word_ladder
namespace comp6771 {
// …
namespace word_ladder {
std::vector
word_ladder(std::string const& from, std::string const& to);
} // namespace word_ladder
} // namespace comp6771
Prefer top-level and occasionally two-tier namespaces to multi-tier.
It’s okay to own multiple namespaces per project, if they logically separate things.
4.3
Unnamed namespaces
In C you had static functions that made functions local to a file.
C++ uses “unnamed” namespaces to achieve the same effect.
Functions that you don’t want in your public interface should be put into unnamed namespaces.
Unlike named namespaces, it’s okay to nest unnamed namespaces.
namespace word_ladder {
namespace {
bool valid_word(std::string const& word);
} // namespace
} // namespace word_ladder
4.4
Namespace aliases
Gives a namespace a new name. Often good for shortening nested namespaces.
namespace chrono = std::chrono;
namespace views = ranges::views;
4.5
Always fully qualify your function calls…
There are certain complex rules about how overload resolution works that will surprise you, so it’s a best practice to always fully-qualify your function calls.
int main() {
auto const x = 10.0;
auto const x2 = std::pow(x, 2);
auto const ladders = word_ladder::generate(“at”, “it”);
auto const x2_as_int = static_cost
}
Using a namespace alias counts as “fully-qualified” only if the alias was fully qualified.
4.6
…even if you’re in the same namespace
There are certain complex rules about how overload resolution works that will surprise you, so it’s a best practice to always fully-qualify your function calls.
1 2 3 4 5 6 7 8 9
10 11 12
namespace word_ladder {
namespace {
bool valid_word(std::string const& word);
} // namespace
std::vector
generate(std::string const& from, std::string const& to) {
// …
auto const result = word_ladder::valid_word(word);
// …
}
} // namespace word_ladder
Using a namespace alias counts as “fully-qualified” only if the alias was fully qualified.
4.7
…even if you’re in the same nested namespace
There are certain complex rules about how overload resolution works that will surprise you, so it’s a best practice to always fully-qualify your function calls.
1 2 3 4 5 6 7 8 9
10 11 12
namespace word_ladder::something::very_long {
namespace {
bool valid_word(std::string const& word);
} // namespace
std::vector
generate(std::string const& from, std::string const& to) {
// …
auto const result = word_ladder::something::very_long::valid_word(word);
// …
}
} // namespace word_ladder
Using a namespace alias counts as “fully-qualified” only if the alias was fully qualified.
4.8
What is OOP (Object-oriented programming)
A class uses data abstraction and encapsulation to define an abstract data type:
Abstraction: separation of interface from implementation Useful as class implementation can change over time
Encapsulation: enforcement of this via information hiding This abstraction leads to two key parts of the abstract data type:
Interface: the operations used by the user (an API)
Implementation: the data members the bodies of the functions in the interface and any other functions not intended for general use
5.1
C++ classes
Since you’ve completed COMP2511 (or equivalent), C++ classes should be pretty straightforward and at a high level follow very similar principles.
A class:
Defines a new type
Is created using the keywords class or struct
May define some members (functions, data) Contains zero or more public and private sections Is instantiated through a constructor
A member function:
must be declared inside the class
may be defined inside the class (it is then inline by default)
may be declared const, when it doesn¡¯t modify the data members
The data members should be private, representing the state of an object.
5.2
Member access control
This is how we support encapsulation and information hiding in C++
1 2 3 4 5 6 7 8 9
10
11
12
13
14
15
16
17
class foo {
public:
// Members accessible by everyone
foo(); // The default constructor.
protected:
// Members accessible by members, friends, and subclasses
// Will discuss this when we do advanced OOP in future weeks.
private:
// Accessible only by members and friends
void private_member_function();
int private_data_member_;
public:
// May define multiple sections of the same name
};
5.3
Constructor
Constructors behave very similar to other programming languages
1
2
3
4
5
6 7}
8
9
10
11
12
13
14
15
16
17
18
19
getval() {
return i_;
}
private:
int i_;
};
int main() {
auto mc = myclass{1};
std::cout << myclass.getval() << "\n";
}
#include
class myclass {
public:
myclass(int i) {
i_ = i;
lecture-3/demo305-basic.cpp
5.4
This pointer
A member function has an extra implicit parameter, named this
This is a pointer to the object on behalf of which the function is called
A member function does not explicitly define it, but may explicitly use it The compiler treats an unqualified reference to a class member as being made through the this pointer.
Generally we use a “_” suffix for class variables rather than a this-> to identify them
#include
class myclass {
public:
myclass(int i) {
this->i_ = i;
1
2
3
4
5
6 7}
8
9
10
11
12
13
14
15
16
17
18
19
int getval() {
return this->i_;
}
private:
int i_;
};
int main() {
auto mc = myclass{1};
std::cout << mc.getval() << "\n";
}
#include
class myclass {
public:
myclass(int i) {
i_ = i;
1
2
3
4
5
6 7}
8
9
10
11
12
13
14
15
16
17
18
19
int getval() {
return i_;
}
private:
int i_;
};
int main() {
auto mc = myclass{1};
std::cout << mc.getval() << "\n";
}
5.5
Class Scope
Anything declared inside the class needs to be accessed through the scope of the class Scopes are accessed using "::" in C++
1 // foo.h 2
3 class Foo {
4 public:
5 // Equiv to typedef int Age
6 using Age = int;
7
8 Foo();
9 Foo(std::istream& is);
10 ~Foo();
11
12 void member_function();
13 };
1 // foo.cpp
2 #include "foo.h"
3
4 Foo::Foo() {
5}
6
7 Foo::Foo(std::istream& is) { 8}
9
10
11
12
13
14
15
16
17
Foo::~Foo() {
}
void Foo::member_function() {
Foo::Age age;
// Also valid, since we are inside the Foo scope.
Age age;
}
5.6
A simple example
C++ classes behave how you expect
1 #include
2 #include
3
4 class person {
5 public:
6 person(std::string const& name, int const age);
7 auto get_name() -> std::string const&;
8 auto get_age() -> int const&;
9
10 private:
11 std::string name_;
12 int age_;
13 };
14
15 person::person(std::string const& name, int const age) {
16 name_ = name;
17 age_ = age;
18 }
19
20 auto person::get_name() -> std::string const& {
21 return name_;
22 }
23
24 auto person::get_age() -> int const& {
25 return age_;
26 }
27
28 auto main() -> int {
29 auto p = person{“Hayden”, 99};
30 std::cout << p.get_name() << "\n";
31 }
lecture-3/demo305-basic2.cpp
5.7
Classes and structs in C++
A class and a struct in C++ are almost exactly the same The only difference is that:
All members of a struct are public by default
All members of a class are private by default
People have all sorts of funny ideas about this. This is the only difference
We use structs only when we want a simple type with little or no methods and direct access to the data members (as a matter of style)
This is a semantic difference, not a technical one
A std::pair or std::tuple may be what you want, though
1 struct foo {
2 int member_; // default public 3 };
1 class foo {
2 int member_; // default private 3 };
5.8
Incomplete types
An incomplete type may only be used to define pointers and references, and in function declarations (but not definitions) Because of the restriction on incomplete types, a class cannot have data members of its own type.
1 2 3 4 5 6
struct node {
int data;
};
// Node is incomplete - this is invalid
// This would also make no sense. What is sizeof(Node)
node next;
But the following is legal, since a class is considered declared once its class name has been seen:
1 struct node {
2 int data;
3 node* next;
4 };
5.9
Constructor initialiser list
#include
#include
class myclass {
public:
myclass(int i) : i_{i} {}
int getval() {
1
2
3
4
5
6
7
8 9}
10
11
12
13
14
15
16
17
18
return i_;
private:
int i_;
};
int main() {
auto mc = myclass{5};
std::cout << mc.getval() << "\n";
}
lecture-3/demo306-initlist.cpp
The initialisation phase occurs before the body of the constructor is executed, regardless of whether the initialiser list is supplied
A constructor will:
1. Construct all data members in order of member declaration (using the same rules as those used to initialise variables)
2. Construct any undefined member variables that weren't defined in step (1)
3. Execute the body of constructor: the code may assign values to the data members to override the initial values
6.1
Constructor Logic
Constructors define how class data members are initalised
A constructor has the same name as the class and no return type Default initalisation is handled through the default constructor Unless we define our own constructors the compile will declare a default constructor
This is known as the synthesized default constructor
1 2 3 4 5 6 7
for each data member in declaration order
if it has an used defined initialiser
Initialise it using the used defined initialiser
else if it is of a built-in type (numeric, pointer, bool, char, etc.)
do nothing (leave it as whatever was in memory before)
else
Initialise it using its default constructor
6.2
Delegating constructors
A constructor may call another constructor inside the initialiser list
Since the other constructor must construct all the data members, do not specify anything else in the constructor initialiser list
The other constructor is called completely before this one. This is one of the few good uses for default values in C++
Default values may be used instead of overloading and delegating constructors
6.3
Delegating constructors
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
std::string const& get_s() {
return s_;
}
int get_val() {
return val_;
}
private:
std::string s_;
const int val_;
};
auto main() -> int {
dummy d1(5);
dummy d2{}; }
1 #include
3 class dummy {
4 public:
5 explicit dummy(int const& i) : s_{“Hello world”}, val_{i} {
6}
7 explicit dummy() : dummy(5) { 8}
lecture-3/demo307-deleg.cpp
6.4
Destructors
Called when the object goes out of scope Why might destructors be handy?
Freeing pointers
Closing files
Unlocking mutexes (from multithreading) Aborting database transactions
noexcept states no exception will be thrown (we will cover this later)
1 MyClass::~MyClass() noexcept { 2 // Definition here
3}
1 class MyClass {
2 ~MyClass() noexcept; 3 };
declaration definition
6.5
Explicit keyword
If a constructor for a class has 1 parameter, the compiler will create an implicit type conversion from the parameter to the class
This may be the behaviour you want (but usually not)
You have to opt-out of this implicit type conversion with the explicit keyword
1 class age {
2 public:
3 age(int age)
4 : age_{age} {}
5
6 private:
7 int age_;
8 };
9
10 auto main() -> int {
11 // Explicitly calling the constructor
12 age a1{20};
13
14 // Explicitly calling the constructor
15 age a2 = age{20};
16
17 // Attempts to use an integer
18 // where an age is expected.
19 // Implicit conversion done.
20 // This seems reasonable.
21 age a3 = 20;
22 }
1 #include
3 class intvec {
4 public:
5 // This one allows the implicit conversion
6 // intvec(std::vector
7 // : vec_(length, 0);
8
9 // This one disallows it.
10 explicit intvec(std::vector
11 : vec_(length, 0) {}
12
13 private:
14 std::vector
15 };
16
17 auto main() -> int {
18 int const size = 20;
19 // Explictly calling the constructor.
20 intvec container1{size}; // Construction
21 intvec container2 = intvec{size}; // Assignment
22
23 // Implicit conversion.
24 // Probably not what we want.
25 // intvec container3 = size;
26 }
lecture-3/demo310-explicit1.cpp lecture-3/demo310-explicit2.cpp
7
Const objects
Member functions are by default only callable by non-const objects
You can declare a const member function which is valid on const objects and non-const objects
A const member function may only modify mutable members
A mutable member should mean that the state of the member can change without the state of the object changing
Good uses of mutable members are rare
Mutable is not something you should set lightly
One example where it might be useful is a cache
8.1
Const member functions
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include
#include
class person {
public:
person(std::string const& name) : name_{name} {}
auto set_name(std::string const& name) -> void {
name_ = name;
auto get_name() -> std::string const& {
return name_;
}
private:
std::string name_;
};
auto main() -> int {
person p1{“Hayden”};
p1.set_name(“Chris”);
std::cout << p1.get_name() << "\n";
person const p1{"Hayden"};
p1.set_name("Chris"); // WILL NOT WORK... WHY NOT?
std::cout << p1.get_name() << "\n"; // WILL NOT WORK... WHY NOT?
}
1
2
3
4
5
6
7
8 9}
lecture-3/demo308-const.cpp
8.2
Static members
Static members belong to the class (i.e. every object), as opposed to a particular object.
These are essentially globals defined inside the scope of the class
Use static members when something is associated with a class, but not a particular instance
Static data has global lifetime (program start to program end)
9.1
8
9
10
11
12
13
14
15
16
17
18
return name.length() < 20;
std::string name_;
// For use with a database
class user {
public:
1
2
3
4
5
6 7}
private: }
user(std::string const& name) : name_{name} {}
auto valid_name(std::string const& name) -> bool {
auto main() -> int {
auto n = std::string{“Santa Clause”};
auto u = user{};
if (u.valid_name(n)) {
} }
user user1{n};
Static member functions
8
9
10
11
12
13
14
15
16
17
return name.length() < 20;
std::string name_;
// For use with a database
class user {
public:
user(std::string const& name) : name_{name} {}
static auto valid_name(std::string const& name) -> bool {
1
2
3
4
5
6 7}
private: }
auto main() -> int {
auto n = std::string{“Santa Clause”};
if (user::valid_name(n)) {
} }
user user1{n};
lecture-3/demo309-static.cpp
9.2
Static member fields
Static member fields are usually defined outside of the class scope. This will be explored in your tutorial
9.3
The synthesized default constructor
Is generated for a class only if it declares no constructors For each member, calls the in-class initialiser if present
Otherwise calls the default constructor (except for trivial types like int)
Cannot be generated when any data members are missing both in-class initialisers and default constructors
1 class B {
2 B(int b): b_{b} { 3
4}
5 int b_;
6 };
1 int main() {
2 int i_{0}; // in-class initialiser
3 int j_; // Untouched memory
4 A a_;
5 // This stops default constructor
6 // from being synthesized.
7 B b_;
8 };
1 class A { 2 int a_; 3 };
10 . 1
Deleting unused default member fns
Revise “The synthesized default constructor”
There are several special functions that we must consider when designing classes
Ask yourself the question:
Does it make sense to have this default member function?
Yes: Does the compile synthesised function make sense?
No: write your own definition
Yes: write “
No: write “
10 . 2
Deleting unused default member fns
Let’s look at an example regarding the copy constructor
1 #include
3 class intvec {
4 public:
5 // This one allows the implicit conversion
6 explicit intvec(std::vector
7 : vec_(length, 0) {}
8 // intvec(intvec const& v) = default;
9 // intvec(intvec const& v) = delete;
10
11 private:
12 std::vector
13 };
14
15 auto main() -> int {
16 intvec a{4};
17 // intvec b{a}; // Will this work?
18 }
lecture-3/demo311-delete.cpp
10 . 3
(Optional) Bookstore
Explore demo300 in the lecture repo. The style will not be appropriate as the style is from an older offering of the course.
11