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 objects 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.xv.rv.s | double |
v.yv.gv.t | double |
v.zv.bv.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 许可协议。转载请注明出处!