#include <iostream>
int i = 1;
int main() {
std::cout << i << "\n";
if (i > 0) {
int i = 2;
std::cout << i << "\n";
{
int i = 3;
std::cout << i << "\n";
}
std::cout << i << "\n";
}
std::cout << i << "\n";
}
lecture-3/demo301-scope.cpp
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.
auto main() -> int {
// Always use auto on the left for this course, but you may see this elsewhere.
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, ...}
auto v12 = std::vector<int>{}; // No different to first
auto v13 = std::vector<int>(); // No different to the first
{
auto v2 = std::vector<int>{v11.begin(), v11.end()}; // A copy of v11.
auto v3 = std::vector<int>{v2}; // A copy of v2.
} // v2 and v3 destructors are called here
auto v41 = std::vector<int>{5, 2}; // Initialiser-list constructor {5, 2}
auto v42 = std::vector<int>(5, 2); // Count + value constructor (5 * 2 => {2, 2, 2, 2, 2})
} // v11, v12, v13, v41, v42 destructors called here
lecture-3/demo302-construction.cpp
#include <iostream>
double f() {
return 1.1;
}
int main() {
// One of the reasons we do auto is to avoid ununitialized values.
// int n; // Not initialized (memory contains previous value)
int n21{}; // Default constructor (memory contains 0)
auto n22 = int{}; // Default constructor (memory contains 0)
auto n3{5};
// Not obvious you know that f() is not an int, but the compiler lets it through.
// int n43 = f();
// Not obvious you know that f() is not an int, and the compiler won't let you (narrowing
// conversion)
// auto n41 = int{f()};
// Good code. Clear you understand what you're doing.
auto n42 = static_cast<int>(f());
// std::cout << n << "\n";
std::cout << n21 << "\n";
std::cout << n22 << "\n";
std::cout << n3 << "\n";
std::cout << n42 << "\n";
}
lecture-3/demo303-construction2.cpp
Can you think of a thing where you always have to remember to do something when you're done?
void ReadWords(const std::string& filename) {
std::ifstream f{filename};
std::vector<std::string> words;
std::copy(std::istream_iterator<std::string>{f}, {}, std::back_inserter{words});
f.close();
}
// lexicon.hpp
namespace lexicon {
std::vector<std::string> read_lexicon(std::string const& path);
void write_lexicon(std::vector<std::string> const&, std::string const& path);
} // namespace lexicon
Used to express that names belong together.
Prevent similar names from clashing.
// word_ladder.hpp
namespace word_ladder {
absl::flat_hash_set<std::string> read_lexicon(std::string const& path);
} // namespace word_ladder
// word_ladder.hpp
namespace word_ladder {
absl::flat_hash_set<std::string> read_lexicon(std::string const& path);
} // namespace word_ladder
// read_lexicon.cpp
namespace word_ladder {
absl::flat_hash_set<std::string> read_lexicon(std::string const& path) {
// open file...
// read file into flat_hash_set...
// return flat_hash_set
}
} // namespace word_ladder
namespace comp6771::word_ladder {
std::vector<std::vector<std::string>>
word_ladder(std::string const& from, std::string const& to);
} // namespace comp6771::word_ladder
namespace comp6771 {
// ...
namespace word_ladder {
std::vector<std::vector<std::string>>
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.
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.
namespace word_ladder {
namespace {
bool valid_word(std::string const& word);
} // namespace
} // namespace word_ladder
Unlike named namespaces, it's okay to nest unnamed namespaces.
namespace views = ranges::views;
namespace chrono = std::chrono;
Gives a namespace a new name. Often good for shortening nested namespaces.
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 = gsl_lite::narrow_cast<int>(x2);
}
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.
Using a namespace alias counts as "fully-qualified" only if the alias was fully qualified.
namespace word_ladder {
namespace {
bool valid_word(std::string const& word);
} // namespace
std::vector<std::vector<std::string>>
generate(std::string const& from, std::string const& to) {
// ...
auto const result = word_ladder::valid_word(word);
// ...
}
} // namespace word_ladder
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.
Using a namespace alias counts as "fully-qualified" only if the alias was fully qualified.
namespace word_ladder::something::very_long {
namespace {
bool valid_word(std::string const& word);
} // namespace
std::vector<std::vector<std::string>>
generate(std::string const& from, std::string const& to) {
// ...
auto const result = word_ladder::something::very_long::valid_word(word);
// ...
}
} // namespace word_ladder
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.
Using a namespace alias counts as "fully-qualified" only if the alias was fully qualified.
Example: Bookstore :
Since you've completed COMP2511 (or equivalent), C++ classes should be pretty straightforward and at a high level follow very similar principles.
This is how we support encapsulation and information hiding in C++
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
};
C++ classes behave how you expect
#include <iostream>
#include <string>
class person {
public:
person(std::string const& name, int age);
auto get_name() -> std::string const&;
auto get_age() -> int const&;
private:
std::string name_;
int age_;
};
person::person(std::string const& name, int const age) {
name_ = name;
age_ = age;
}
auto person::get_name() -> std::string const& {
return name_;
}
auto person::get_age() -> int const& {
return age_;
}
auto main() -> int {
person p("Hayden", 99);
std::cout << p.get_name() << "\n";
}
lecture-3/demo304-classbasic.cpp
class foo {
int member_; // default private
};
struct foo {
int member_; // default public
};
// foo.h
class Foo {
public:
// Equiv to typedef int Age
using Age = int;
Foo();
Foo(std::istream& is);
~Foo();
void member_function();
};
// foo.cpp
#include "foo.h"
Foo::Foo() {
}
Foo::Foo(std::istream& is) {
}
Foo::~Foo() {
}
void Foo::member_function() {
Foo::Age age;
// Also valid, since we are inside the Foo scope.
Age age;
}
struct node {
int data;
// Node is incomplete - this is invalid
// This would also make no sense. What is sizeof(Node)
node next;
};
struct node {
int data;
node* next;
};
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
class C {
int i_{0}; // in-class initialiser
int j_; // Untouched memory
A a_;
// This stops default constructor
// from being synthesized.
B b_;
};
class A {
int a_;
};
class B {
B(int b): b_{b} {}
int b_;
};
#include <string>
class nodefault {
public:
explicit nodefault(int i)
: i_{i} {}
private:
int i_;
};
int const b_default = 5;
class b {
// Constructs s_ with value "Hello world"
explicit b(int& i)
: s_{"Hello world"} , const_{b_default} , no_default_{i} , ref_{i} {}
// Doesn't work - constructed in order of member declaration.
/*explicit b(int& i)
: s_{"Hello world"} , const_{5} , ref_{i} , no_default_{ref_} {}*/
/*explicit 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 (modifying a const object).
const_string_ = "Goodbye world";
// Fails to compile (references *must* be initialized in the constructor).
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_;
};
lecture-3/demo306-initlist.cpp
#include <string>
class dummy {
public:
explicit dummy(int const& i)
: s_{"Hello world"}
, val_{i} {}
explicit dummy()
: dummy(5) {}
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{};
}
lecture-3/demo307-deleg.cpp
class MyClass {
~MyClass() noexcept;
};
MyClass::~MyClass() noexcept {
// Definition here
}
class age {
public:
age(int age)
: age_{age} {}
private:
int age_;
};
auto main() -> int {
// Explicitly calling the constructor
age a1{20};
// Explicitly calling the constructor
age a2 = age{20};
// Attempts to use an integer
// where an age is expected.
// Implicit conversion done.
// This seems reasonable.
age a3 = 20;
}
#include <vector>
class intvec {
public:
// This one allows the implicit conversion
// intvec(std::vector<int>::size_type length)
// : vec_(length, 0);
// This one disallows it.
explicit intvec(std::vector<int>::size_type length)
: vec_(length, 0) {}
private:
std::vector<int> vec_;
};
auto main() -> int {
int const size = 20;
// Explictly calling the constructor.
intvec container1{size}; // Construction
intvec container2 = intvec{size}; // Assignment
// Implicit conversion.
// Probably not what we want.
// intvec container3 = size;
}
lecture-3/demo310-explicit1.cpp
lecture-3/demo310-explicit2.cpp
#include <iostream>
#include <string>
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?
}
lecture-3/demo308-const.cpp
class foo {
public:
foo(int const miles) {
this->kilometres_ = miles / 1.159;
}
private:
int kilometres_; // default private
};
class foo {
public:
foo(int const miles) {
kilometres_ = miles / 1.159;
}
private:
int kilometres_; // default private
};
// For use with a database
class user {
public:
user(std::string const& name) : name_{name} {}
static auto valid_name(std::string const& name) -> bool {
return name.length() < 20;
}
private:
std::string name_;
}
auto main() -> int {
auto n = std::string{"Santa Clause"};
if (user::valid_name(n)) {
user user1{n};
}
}
lecture-3/demo309-static.cpp
#include <vector>
class intvec {
public:
// This one allows the implicit conversion
explicit intvec(std::vector<int>::size_type length)
: vec_(length, 0) {}
// intvec(intvec const& v) = default;
// intvec(intvec const& v) = delete;
private:
std::vector<int> vec_;
};
auto main() -> int {
intvec a{4};
// intvec b{a}; // Will this work?
}
lecture-3/demo311-delete.cpp