COMP6771 - Advanced C++ Programming
Weekly Exercise - Week 4
With my own solution (might be incorrect)
Consider the following program:
cpp#include <iostream>
namespace {
auto i = 42;
}
namespace comp6771 {
auto i = 6771;
auto foo(int) -> void {
std::cout << "comp6771::foo(int)" << std::endl;
}
}
namespace comp6772 {
auto foo(int) -> void {
std::cout << "comp6772::foo(int)" << std::endl;
}
auto foo(char) -> void {
std::cout << "comp6772::foo(char)" << std::endl;
}
}
namespace comp6773 {
struct uchar {
unsigned char c;
};
auto bar(uchar) -> void {
std::cout << "comp6773::bar(comp6773::uchar)" << std::endl;
}
}
int main() {
std::cout << i << std::endl;
{
int i = 0;
std::cout << i << std::endl;
using ::i;
std::cout << i << std::endl;
}
comp6771::foo(i);
comp6772::foo(i);
using namespace comp6771;
using namespace comp6772;
foo(i);
foo('c');
auto uc = comp6773::uchar{'c'};
bar(uc);
}
Some of the lines of this program are ill-formed according to C++'s rules about namespaces and scopes.
Figure out which lines of this program are ill-formed - write them dowon!
Hint: you should pretend you are like a compiler and can only look at this source file as it is. Don't try and remove any lines as that could make subsequent lines become well-formed.
C++using ::i;
C++foo(i);
using ::i;
i
is declared in an unnamed namespace, not in the global namespace (::
). Therefore, it cannot be brought into scope using using ::i;
.foo(i);
using namespace comp6771;
and using namespace comp6772;
, this line becomes ambiguous because both comp6771::foo(int)
and comp6772::foo(int)
are in scope. The compiler cannot resolve which foo
to call.Write the alternative which most accurately answers the questions below.
Consider the following structure:
cppstruct object {
object() {
std::cout << "ctor ";
}
object(const object &) {
std::cout << "copy-ctor ";
}
~object() {
std::cout << "dtor ";
}
};
cpp{
std::cout << "Pointer: ";
std::list<object *> l;
object x;
l.push_back(&x);
}
Pointer: ctor ctor dtor dtor
. l
is a list of object
-derived types and x
is an object
, so the constructor and destructor for object
will run for both.Pointer: ctor dtor
. Variables are destructed in the reverse-order of definition on the stack, so to prevent a double-free bug, l
's single element (the address of x
) only has the constructor and destructor run for it.Pointer: ctor dtor
. The only object
whose lifetime ends in this code block is x
, and the list l
is irrelevant.Pointer: ctor ctor dtor dtor
. l
's default constructor creates an object *
instance and a second instance is created when the address of x
is pushed back. Two constructions implies two destructions.cpp{
std::cout << "\nValue: ";
std::list<object> l;
object x;
l.push_back(x);
}
Value: ctor copy-ctor dtor dtor
. The default constructor of object
is called when x
comes into scope and the copy constructor is called when x
is pushed back into l
. At the end of scope, x
is destructed first and, since l
holds object
s by value, its single element is destructed second.Value: ctor copy-ctor dtor dtor
. x
is default-constructed and destructed as per usual, but the temporary that is created by passing x
into push_back()
is copy-constructed and destructed in that expression.Value: ctor copy-ctor copy-ctor dtor dtor dtor
. x
is default-constructed and destructed as per usual, but the temporary that is created in the call to l.push_back()
and the resulting element of l
are copy-constructed and destructed.Value: ctor copy-ctor dtor copy-ctor dtor dtor
. x
is default-constructed and destructed as per usual, but the temporary that is created in the call to push_back()
has its lifetime end at the end of that expression before it is copied into l
. At the end of scope, l
's single element is also destructed.Write the alternative which most accurately answers the questions below.
cppstruct point2i {
int x;
int y;
};
Is this class-type default-constructible and why?
x
and y
uninitialised.x
and y
to 0
.cppclass employee {
public:
employee(int employeeno);
private:
int employeeno;
};
Is this class-type default-constructible and why?
int
itself has a default constructor, so employee
's default constructor simply delegates to int
's one.cppstruct point2i {
point2i() = default;
point2i(int x = 42, int y = 6771);
int x;
int y;
};
Is this class-type default-constructible and why?
cppstruct point2i {
point2i() = default;
point2i(const point2i &) = delete;
point2i(point2i &&) = delete;
};
point2i get_point() { return point2i{}; }
point2i p = get_point();
Will this code compile and why?
p
's initialisationpoint2i(point2i &&)
is invalid syntax.point2i
is not copyable at all, so p
cannot be initialised.point2i
has no data members, so even though the copy and move constructors are deleted, the compiler knows that those constructors would have had no effect anyway.cppstruct guard {
guard() = default;
guard(const guard &) = delete;
guard(guard &&) = delete;
};
struct outer {
guard g;
};
Is the outer
class-type default-constructible or copyable and why?
outer
to have the default constructor and copy/move constructors generated for us.guard
's explicitly deleted copy/move constructor prevents the implicitly generated copy/move constructors for outer
. For a similar reason, guard
does allow for the implicitly generated default constructor.guard
prevents the implicit copy/move constructors for outer
to be generated, as well the default constructor. Therefore, this class cannot be constructed, which is a compiler error.guard
has no effect on the implicitly generated default, copy, and move constructors for outer
since it is a struct
. If outer
were a class
, it would only be default-constructible, however.Write the alternative which most accurately answers the questions below.
cppstd::vector<int> a(1, 2);
What is this line doing?
cppstd::vector<int> a{1, 2};
What is this line doing?
cppstd::vector<int> b = {1, 2};
What is this line doing?
cppstd::vector<int> a{1, 2};
std::vector<int> c = a;
What is this line doing?
a
to c
.c
is "stealing" the data members of a
to construct itself.cppstd::vector<int> a{1, 2};
std::vector<int> c;
c = a;
What is this line doing?
c
from a
.a
to c
.a
to c
.In src/4.5/namespaced.cpp
, we have provided the below main()
function:
cppint main() {
namespace spaceland = comp6771;
// should be an alias for std::vector.
auto v = spaceland::vector{6771};
// name: earth, position from sun: 3
// a planet is a kind of
auto earth = spaceland::planet{"earth", 3};
// should produce an object with the same type as the "earth" variable above.
auto old_earth = spaceland::planets::terrestrial{"earth", 3};
std::cout << v[0] << std::endl;
std::cout << earth.name << std::endl;
std::cout << old_earth.pos << std::endl;
}
In src/4.4/namespaced.h
, implement the rest of the missing namespace functionality such that this code compiles and produces this output (note the newline at the end):
txt6771 earth 3
There is a plain-old-data struct in src/4.5/namespaced.h
that may be used as a planet
type.
Note: you are not allowed to modify src/4.5/namespaced.cpp
.
Hint: it does not matter how you implement the namespaces in the header file -- if your code compiles and produces the above output, then it is correct.
C++namespace comp6771 {
using vector = std::vector<int>;
using planet = celestial_body;
namespace planets {
using terrestrial = celestial_body;
}
}
Implement the following class specification in src/4.6/ferrari.h
and/or src/4.6/ferrari.cpp
.
Method | Description |
---|---|
ferrari(const std::string &owner, int modelno) | This constructor should initialise the object state to keep track of the owner name and model number. Speed is initially 0. |
ferrari() | This constructor should default-initialise the object's state so that its owner name is "unknown" and its model number is 6771 . Speed is initially 0. |
std::pair<std::string, int> get_details() . | Returns this Ferrari's owner and model number. |
void drive(int spd) . | Start driving at speed spd . If no speed is given, it should default to 88 . |
std::string vroom() . | Returns a string depending on how fast this Ferrari is currently moving. If the speed is strictly less than 20, it should return the empty string. If 20 <= speed < 80 , it should return "vroom!!". Otherwise, it should return "VROOOOOOOOM!!!". |
Note: You need to ensure your code is const-correct. Which methods should be const-qualified has intentionally been left out.
When implementing this class, you should ensure you are using modern C++ best practices, such as member initialiser lists, delegating constructors, etc. You should check with your tutor to make sure that your style aligns with modern practices.
In src/4.6/ferrari.test.cpp
, you will also need to write at least five tests to make sure your code is correct.
C++class ferrari {
public:
ferrari();
ferrari(const std::string &owner, int modelno);
std::pair<std::string, int> get_details();
void drive(int spd=88);
std::string vroom();
private:
std::string owner_;
int modelno_;
int speed_;
};
ferrari::ferrari(const std::string& owner, int modelno) {
owner_ = owner;
modelno_ = modelno;
speed_ = 0;
}
ferrari::ferrari() {
owner_ = "unknown";
modelno_ = 6771;
speed_ = 0;
}
std::pair<std::string, int> ferrari::get_details() {
return {owner_, modelno_};
}
void ferrari::drive(int spd) {
speed_ = spd;
}
std::string ferrari::vroom() {
if (speed_ < 20) {
return "";
}
if (speed_ < 80) {
return "vroom!!";
}
return "VROOOOOOOOM!!!";
}
C++ has value semantics by default (rather than reference semantics like other languages, such as Java). Classes allow developers to write their own value-types that act, look, and feel like the regular built-in types like int
or double
.
In this exercise we begin to create our own value-type representing a rational number. Rational numbers are any number that can be represented as a fraction with integer numerator and denominator. Note that x/0
for any x
is not a rational number.
In src/4.7/rational.cpp
and associated files, implement the rational_number
class and write at least three tests for it.
The class should have:
null
, which represents "no" rational number. This should be implemented as an empty std::optional<rational_number>
.auto make_rational(int num, int denom) -> std::optional<rational_number>
that returns either a rational number or the above static data member if denom == 0
.make_rational
).add(), sub(), mul(), div()
as friend functions so that, for r1
and r2
which have type rational_number
, one may write: add(r1, r2)
, etc.. The return type for add(), sub(), mul()
should be rational_number
, but for div()
it should be std::optional<rational_number>
.eq
and ne
so that, for r1
and r2
which have type rational_number
, one may write if (eq(r1, r2)) { ... }
. The return type for both of these functions should be bool
.
auto value() -> double
which returns the quotient of the numerator and the denominator as a double
.The size of every instance of your class should be no bigger than 16 bytes
.
C++class rational_number {
public:
rational_number() = default;
static auto make_rational(int num, int denom) -> std::optional<rational_number> {
if (denom == 0) {
return std::nullopt;
}
if (num == 0) {
return rational_number(0, 1);
}
auto gcd = std::gcd(num, denom);
return rational_number(num / gcd, denom / gcd);
}
auto value(std::optional<rational_number> r) -> std::optional<double> {
if(r == std::nullopt) {
return std::nullopt;
}
if (r.value().get_denom() == 0) {
return std::nullopt;
}
return static_cast<double>(r.value().get_num()) / static_cast<double>(r.value().get_denom());
}
auto get_num() -> int {
return num_;
}
auto get_denom() -> int {
return denom_;
}
friend auto add (std::optional<rational_number> r1, std::optional<rational_number> r2) -> std::optional<rational_number>{
if (r1 == std::nullopt or r2 == std::nullopt) {
return std::nullopt;
}
int denom_lcm = std::lcm(r1.value().get_denom(), r2.value().get_denom());
int r1_num = denom_lcm / r1.value().get_denom() * r1.value().get_num();
int r2_num = denom_lcm / r2.value().get_denom() * r2.value().get_num();
int new_num = r1_num + r2_num;
int new_num_denom_gcd = std::gcd(new_num, denom_lcm);
int new_denom = denom_lcm / new_num_denom_gcd;
new_num /= new_num_denom_gcd;
return make_rational(new_num, new_denom);
}
friend auto sub (std::optional<rational_number> r1, std::optional<rational_number> r2) -> std::optional<rational_number>{
if (r1 == std::nullopt or r2 == std::nullopt) {
return std::nullopt;
}
auto r3 = make_rational(r2.value().get_num() * -1, r2.value().get_denom());
return add(r1, r3);
}
friend auto mul(std::optional<rational_number> r1, std::optional<rational_number> r2) -> std::optional<rational_number> {
if (r1 == std::nullopt or r2 == std::nullopt) {
return std::nullopt;
}
int new_num = r1.value().get_num() * r2.value().get_num();
int new_denom = r1.value().get_denom() * r2.value().get_denom();
int gcd = std::gcd(new_num, new_denom);
new_num /= gcd;
new_denom /= gcd;
return make_rational(new_num, new_denom);
}
friend auto div(std::optional<rational_number> r1, std::optional<rational_number> r2) -> std::optional<rational_number> {
if (r2.value().get_num() == 0) {
return make_rational(0, 0);
}
if (r1 == std::nullopt or r2 == std::nullopt) {
return std::nullopt;
}
if (r1.value().get_num() == 0) {
return make_rational(0, 1);
}
auto r3 = make_rational(r2.value().get_denom(), r2.value().get_num());
return mul(r1, r3);
}
friend auto eq(std::optional<rational_number> r1, std::optional<rational_number> r2) -> bool {
if (r1 == std::nullopt and r2 == std::nullopt) {
return true;
}
if ((r1 == std::nullopt and r2 != std::nullopt) or (r2 == std::nullopt and r1 != std::nullopt)) {
return false;
}
if (r1.value().get_num() == r2.value().get_num() and r1.value().get_denom() == r2.value().get_denom()) {
return true;
}
return false;
}
friend auto ne(std::optional<rational_number> r1, std::optional<rational_number> r2) -> bool {
return !eq(r1, r2);
}
private:
int num_;
int denom_;
rational_number(int num, int denom) : num_(num), denom_(denom) {}
};
The OpenGL Shader Language (GLSL) is a C/C++-like language used to write shader programs that can run on GPUs.
One convenient feature of the GLSL built-in type vec3
is that you can access its components by various names. For example:
v.x
: access the 1st component in v
as a spatial dimension.v.r
: access the 1st component in v
as a colour dimension.v.s
: access the 1st component in v
as a texture dimension.In all, there are three sets of syntactic sugar:
x
, y
, z
: for 1st, 2nd, and 3rd componenets of a vec3
.r
, g
, b
: for 1st, 2nd, and 3rd componenets of a vec3
.s
, t
, p
: for 1st, 2nd, and 3rd componenets of a vec3
.In src/4.8/vec3.h
and/or src/4.8/vec3.cpp
, complete the below specification.
When you are done, write at least three tests in src/4.8/vec3.test.cpp
.
Data Member | Type |
---|---|
v.x v.r v.s | double |
v.y v.g v.t | double |
v.z v.b v.p | double |
x
, r
, and s
should refer to the same data member.
Likewise, y
, g
, and t
should refer to the same data member.
Similarly, z
, b
, and p
should refer to the same data member.
Therefore, sizeof(vec3) == 3 * sizeof(double)
should be true.
Hint: you may find this page on unions useful. Particularly, anonymous unions inside of class-types.
cpp/* 1 */ vec3();
/* 2 */ vec3(double c);
/* 3 */ vec3(double a, double b, double c);
c
.Note: you must ensure the below code snippet cannot happen:
cpp// should fail to compile.
vec3 foo() { return 1.0; }
vec3 v = foo();
a
, the second is b
, and the third is c
.Your class should be copyable and destructible.
None.
Aside from the constructors, vec3
is intended to be a plain data struct, so it is OK to access its data members directly.
C++struct vec3 {
vec3() : vec3(0, 0, 0) {}
explicit vec3(double c) : vec3(c, c, c) {}
vec3(double a, double b, double c) : x{a}, y{b}, z{c} {}
union {
double x;
double r;
double s;
};
union {
double y;
double g;
double t;
};
union {
double z;
double b;
double p;
};
};
本文作者:Jeff Wu
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!