This week is about Object Oriented Programming in C++.
()
: only for functions
{}
: either functions or objects, generally objects to minimise ambiguity
int main() {
std::vector<int> v11; // Calls 0-argument constructor. Creates empty vector.
// There's no difference between these:
// T variable = T{arg1, arg2, ...}
// T variable{arg1, arg2, ...}
std::vector<int> v12{}; // No different to first
std::vector<int> v13 = std::vector<int>(); // No different to the first
std::vector<int> v14 = std::vector<int>{}; // No different to the first
std::vector<int> v3{v2.begin(), v2.end()}; // constructed with an iterator
std::vector<int> v4{v3}; // Constructed off another vector
std::vector<int> v51{5, 2}; // Initialiser-list constructor {5, 2}
std::vector<int> v52(5, 2); // Count + value constructor (5 * 2 => {2, 2, 2, 2, 2})
}
For basic types, constructors works but default constructors need to be manually called.
int main() {
int n; // not constructed (memory contains previous value)
int n2{}; // Default constructor (memory contains 0)
int n3{5};
// This version is nice because it gives us an error.
int n4{5.5};
// You need to explictly tell it you want this.
int n6{static_cast<int>(5.5)};
// Not so nice. No error
int n5 = 5.5;
}
Unless we define our own constructors the compile will declare a default constructor
for each data member in declaration order
if it has an in-class initialiser
Initialise it using the in-class 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
Cannot be generated when any data members are missing both in-class initialisers and default constructors
class A {
int a_;
};
class B {
B(int b): b_{b} {}
int b_;
};
class C {
int i{0}; // in-class initialiser
int j; // Untouched memory
A a;
// This stops default constructor
// from being synthesized.
B b;
};
Data members are constructed in order of memebr declaration.
class NoDefault {
NoDefault(int i);
}
class B {
// Constructs s_ with value "Hello world"
B(int& i): s_{"Hello world"}, const_{5}, no_default{i}, ref_{i} {}
// Doesn't work - constructed in order of member declaration.
B(int& i): s_{"Hello world"}, const_{5}, ref_{i}, no_default{ref_} {}
B(int& i) {
// Constructs s_ with an empty string, then reassigns it to "Hello world"
// Extra work done (but may be optimised out).
s_ = "Hello world";
// Fails to compile
const_ = "Goodbye world";
ref_ = i;
// This is fine, but it can't construct it initially.
no_default_ = NoDefault{1};
}
std::string s_;
// All of these will break compilation if you attempt to put them in the body.
const int const_;
NoDefault no_default_;
int& ref_;
};
default
<function declaration> = default;
tells the compiler to synthesize the function
class T {
T(const T&);
};
class T {
// A copy-assignment operator
T& operator=(const T& original);
// The copy-and-swap idiom
// This is also a copy-assignment operator
T& operator=(T copy) {
std::swap(*this, copy);
return *this;
}
};
```cpp MyClass base; MyClass copy_constructed = base;
MyClass copy_assigned; copy_assigned = base;
#### Rvalue Eeferences
- Rvalue references look like T&& (lvalue is T&)
- An lvalue denotes an object whose resource cannot be reused
- An rvalue denotes an object whose resources can be reused
```cpp
void inner(int&& value) {
++value;
std::cout << value << '\n';
}
void outer(int&& value) {
inner(value); // This fails? Why?
std::cout << value << '\n';
}
int main() {
f1(1); // This works fine.
int i;
f2(i); // This fails because i is an lvalue.
}
The caller (main) has promised that it won’t be used anymore, and an rvalue reference parameter is an lvalue inside the function.
std::move
converts something to an rvalue, which says “I don’t care about this anymore”, and all it does is to allow the compiler to use rvalue reference overloads.
void inner(int&& value) {
++value;
std::cout << value << '\n';
}
void outer(int&& value) {
inner(std::move(value));
// Value is now in a valid but unspecified state.
// Although this isn't a compiler error, this is bad code.
// Don't access variables that were moved from, except to reconstruct them.
std::cout << value << '\n';
}
int main() {
f1(1); // This works fine.
int i;
f2(std::move(i));
}
class T {
T(T&&) noexcept;
};
class T {
T& operator=(T&&) noexcept;
};
noexcept
: compiler wont generate recovery code. Exception thrown in noexcept
function will terminate the program. noexcept
tells caller there is no need to worry about exception handling.
Destructors are called when objects goes out of scope. It is marked as noexcept
because it is not part of controlled process. In practice, implicit destructors are noexcept unless the class is “poisoned” by a base or member whose destructor is noexcept(false).
An incomplete type may only be used to define pointers and references, and in function declarations (but not definitions). A class cannot have data members of its own.
struct Node {
int data;
// Node is incomplete - this is invalid
// This would also make no sense. sizeof(Node) is unknown
Node next;
};
struct Node {
int data;
// this is fine
Node* next;
};
class Foo {
public:
// Members accessible by everyone
Foo();
protected:
// Members accessible by members, friends, and subclasses
private:
// Accessible only by members and friends
void PrivateMemberFunction();
int private_data_member_;
public:
// May define multiple sections of the same name
};
class
vs struct
Only difference:
Difference is semantic, not technical. std::pair
and std::tuple
are struct templates.
friend
functions::
Anything declared inside the class needs to be accessed through the scope of the class, using ::
// foo.h
class Foo {
public:
// Equiv to typedef int Age
using Age = int;
Foo();
Foo(std::istream& is);
~Foo();
void MemberFunction();
};
// foo.cpp
#include "foo.h"
Foo::Foo() {
}
Foo::Foo(std::istream& is) {
}
Foo::~Foo() {
}
void Foo::MemberFunction() {
Foo::Age age;
}
this
pointerthis
as a parameter implicitlystatic
membersboth function and data may be declared static
```cpp
// For use with a database
class User {
static std::string table_name;
static std::optional
void commit(); std::string username; }
User user = *User::query(“Alice”); user.username = “Bob” User::commit(); // fails to compile (commit is not static) user.commit();
std::cout « User::table_name; std::cout « User::username; // Fails to compile
## `explicit` type conversions
If a constructor for a class has 1 parameter, the compiler will create an implicit type conversion from the parameter to the class, can be turned off by the keyword `explicit`.
```cpp
class Age {
Age(int age);
};
// Explicitly calling the constructor
Age age{20};
// Attempts to use an integer
// where an age is expected.
// Implicit conversion done.
// This seems reasonable.
Age age = 20;
class IntVec {
// This one allows the implicit conversion
IntVec(int length): vec_(length, 0);
This one disallows it.
explicit IntVec(int length): vec_(length, 0);
std::vector<int> vec_;
};
// Explictly calling the constructor.
IntVec container{20};
// Implicit conversion.
// Probably not what we want.
IntVec container = 20;
namespace
// path/to/file.h
namespace path {
namespace to {
class MyClass {
}
void MyFn();
} // namespace to
} // namespace path
// main.cpp
#include "path/to/file.h"
int main() {
path::to::MyClass myClass;
path::to::MyFn();
}
using
There are two ways to use
#include "path/to/file.h"
int main() {
// Imports a single thing
// into the current scope.
using path::to::MyClass;
MyClass myClass;
}
// Far cleaner than typedef, and the
// arguments are the right way around!
int main() {
using intvec = std::vector<int>;
intvec vec;
C::iterator_t it;
}
class IteratorC {
}
class C {
using iterator_t = IteratorC;
}
using namespace std;
The most mind blowing thing this week is about using namespace std;
. Previously I have seen a lot of examples make use of it and it worked fine. And it makes codes shorter. But in lecture we are told not to write it, which confused me. After discussion with lecturer, I completely changed my mind: shorter != cleaner
. I thought that shorter code is cleaner to read. But it was wrong. using namespace
will pollute namespaces and it makes code actually harder to read (You dont know map
is std::map
or map
of from somewhere else). It sacrifices readability to gain shorter code, which is completely not worthy. Readability matters, and shorter decreases readability. Shorter code is never the goal to reach, code is for human to read.