Why?
What?
#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
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();
}
Can you think of a thing where you always have to remember to do something when you're done?
// 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 {
std::unordered_set<std::string> read_lexicon(std::string const& path);
} // namespace word_ladder
// word_ladder.hpp
namespace word_ladder {
std::unordered_set<std::string> read_lexicon(std::string const& path);
} // namespace word_ladder
// read_lexicon.cpp
namespace word_ladder {
std::unordered_set<std::string> read_lexicon(std::string const& path) {
// open file...
// read file into std::unordered_set...
// return std::unordered_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 = static_cost<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.
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
};
Constructors behave very similar to other programming languages
#include <iostream>
class myclass {
public:
myclass(int i) {
i_ = i;
}
getval() {
return i_;
}
private:
int i_;
};
int main() {
auto mc = myclass{1};
std::cout << myclass.getval() << "\n";
}
lecture-3/demo305-basic.cpp
#include <iostream>
class myclass {
public:
myclass(int i) {
i_ = i;
}
int getval() {
return i_;
}
private:
int i_;
};
int main() {
auto mc = myclass{1};
std::cout << mc.getval() << "\n";
}
#include <iostream>
class myclass {
public:
myclass(int i) {
this->i_ = i;
}
int getval() {
return this->i_;
}
private:
int i_;
};
int main() {
auto mc = myclass{1};
std::cout << mc.getval() << "\n";
}
// 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;
}
C++ classes behave how you expect
#include <iostream>
#include <string>
class person {
public:
person(std::string const& name, int const 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 {
auto p = person{"Hayden", 99};
std::cout << p.get_name() << "\n";
}
lecture-3/demo305-basic2.cpp
class foo {
int member_; // default private
};
struct foo {
int member_; // default public
};
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;
};
#include <string>
#include <iostream>
class myclass {
public:
myclass(int i) : i_{i} {}
int getval() {
return i_;
}
private:
int i_;
};
int main() {
auto mc = myclass{5};
std::cout << mc.getval() << "\n";
}
lecture-3/demo306-initlist.cpp
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
#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
}
declaration
definition
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
// 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
// For use with a database
class user {
public:
user(std::string const& name) : name_{name} {}
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"};
auto u = user{};
if (u.valid_name(n)) {
user user1{n};
}
}
Static member fields are usually defined outside of the class scope. This will be explored in your tutorial
int main() {
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 <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
Let's look at an example regarding the copy constructor
Explore demo300 in the lecture repo. The style will not be appropriate as the style is from an older offering of the course.