Week01
Lab106: Catch2 Syntax
1.What is a TEST_CASE
?
- a) A
TEST_CASE
is a uniquely-named testing scope that must contain every test we write about our program. - b) A
TEST_CASE
is a fancy macro that has no effect in “real” code. - c) A
TEST_CASE
is a uniquely-named testing scope that will keep track of all theCHECK
s andREQUIRE
s that pass or fail. - d) A
TEST_CASE
is a macro where only compile-time evaluable assertions about our code can be written.
Answer: (a)
2.What is a CHECK
? In what way, if any, is it different to assert()
?
- a)
CHECK
andassert
are both macros and do the exact same thing. - b)
CHECK
andassert
are both macros, but aCHECK
will evaluate an expression and report it if it’s false whereasassert
will crash the program. - c)
CHECK
is a function that suggests a fact about our code should be true, butassert
enforces it. - d)
CHECK
records the failure of an assertion but does nothing about it and is entirely unrelated toassert
.
Answer: (b)
3.What is a REQUIRE
? In what way, if any, is it different to assert()
?
- a)
REQUIRE
evaluates an expression and crashes the program if it is false butassert
will report it to the user. - b)
REQUIRE
andassert
both evaluate expressions and terminate the currently executing test if false. - c)
assert
andREQUIRE
both evaluate expressions, but onlyassert
has an effect if the expression is false. - d)
REQUIRE
evalutes an expression and if false will terminate the currently executing test and move onto the next one. It is entirely unrelated toassert
.
Answer: (d)
4.What are SECTION
blocks in Catch2?
- a)
SECTION
blocks are ways to divide testing logic inTEST_CASE
s. Any state changes in aSECTION
are not reflected inSECTION
s at the same level. - b)
SECTION
blocks are a way to break up long tests and have little use other than that. - c)
SECTION
s are unique testing scopes that can only containTEST_CASE
s. - d)
SECTION
s are part of Behaviour Driven Development and group togetherSCENARIO
s.
Answer: (a)
5.What goes between the parentheses in TEST_CASE
s and SECTION
s?
- a) The function or class name that is currently being tested.
- b) A succinct and insightful description in plain language of the code under test.
- c) A description that matches a similarly-worded comment directly above the
TEST_CASE
/SECTION
. - d) A note for future readers of your code about what you were thinking at the time of writing this test.
Answer: (b)
Lab108: Constant Referencing
1.Are there any errors in this code and if so what are they?
auto i = 3;
i = 4;
- a) Yes:
auto
is a reserved keyword - b) Yes: it is illegal to initialise a variable after it is defined.
- c) Maybe: it depends on what CPU this code is run on.
- d) No: assignment after initialisation is legal, even in C.
Answer: (d)
2.Are there any errors in this code and if so what are they?
const auto j = 5;
--j;
- a) Yes:
j
is a constant integer which cannot be modified. - b) Maybe: it depends if the programmer prefers east or west const.
- c) No: decrementing a constant integer creates a new one.
- d) Yes:
auto
andconst
should not be mixed.
Answer: (a)
3.Are there any errors in this code and if so what are they?
auto age = 18;
auto& my_age = age;
++my_age;
- a) Maybe: it depends if the code is compiled as C++98 or C++11 or higher.
- b) No: my_age is a reference to age, so preincrement is perfectly legal.
- c) Yes: references are inherently const and so modifying
age
throughmy_age
is illegal. - d) No:
my_age
is a copy ofage
and modifyingmy_age
has no impact onage
whatsoever.
Answer: (a)
4.Are there any errors in this code and if so what are they?
auto age = 21;
const auto &my_age = age;
--my_age;
- a) Yes:
auto
references can only be used with explicit type annotations. - b) Maybe: if this code is compiled with the “-pedantic” flag in GCC, it would be perfectly legal.
- c) No: my_age is a const reference, but
age
is not constant, so this is fine. - d) Yes:
my_age
is a reference to a constant integer, which cannot be modified.
Answer: (d)
Week02
Lab201: Definitive Declarations
1.Is the line marked (*) a declaration or a definition?
int get_age(); // (*)
int main() {
}
- a) Declaration
- b) Definition
- c) Neither
- d) Both
Answer: (a) This is a declaration only since the type and name of a function are known but the body has not yet been defined.
2.Is the line marked (*) a declaration or a definiton?
int get_age();
int age = get_age(); // (*)
- a) Declaration
- b) Definition
- c) Neither
- d) Both
Answer: (d). This is both since it is a global variable definition, and every definition is also a declaration.
3.Is the line marked (*) a declaration or a definition?
int main() {
auto age = 20; // (*)
}
- a) Declaration
- b) Definition
- c) Neither
- d) Both
Answer: (d). This is both since it is a stack-local variable definition, and every definition is also a declaration.
4.Is the line marked (*) a declaration or a definition?
int main() {
auto age = 20;
std::cout << age << std::endl; // (*)
}
- a) Declaration
- b) Definition
- c) Neither
- d) Both
Answer: (c). It is neither a declaration nor a definition since no names (variables or functions) are introduced.
5.Is the line marked (*) a declaration or a definition?
int get_age();
int get_age() { // (*)
return 6771;
}
- a) Declaration
- b) Definition
- c) Neither
- d) Both
Answer: (d). It is both since it is a function definition and evey definition is also a declaration.
Lab202: Resolution Overload
1.Consider the code below:
/* 1 */ auto put(char) -> void;
/* 2 */ auto put(int) -> void;
/* 3 */ auto put(const char) -> void;
/* 4 */ auto put(char &) -> void;
put('a');
Which overload of put
would be selected and why?
- a) Overload 1:
put
was called with achar
and overload 3 is just a redeclaration of overload 1. - b) Overload 2:
char
is implicitly promotable toint
and so overload 2 is the best match. - c) Overload 3:
put
was called with a temporaryconst char
. - d) Overload 4:
put
was called with a temporarychar
and temporaries preferentially bind to references.
Answer: (a). A character literal has type
char
. This cannot bind tochar&
(overload 4 is not viable) and will not bind to anint
without a cast (overload 2 is not viable). For the purposes of overload resolution,const char
andchar
are identical, so both overload 1 and 3 are viable. The reason in alternative (c) is not valid, therefore only alternative (a) can be chosen.
2.Consider the code below:
/* 1 */ auto put(char) -> void;
/* 2 */ auto put(char &) -> void;
/* 3 */ auto put(const char &) -> void;
char c = 'a';
put(c);
Which overload of put
would be selected and why?
- a) Overload 1:
put
was called with achar
. - b) Overload 2:
put
was called with a mutablechar
and and references have higher priority. - c) Overload 3:
put
was called with a constchar
and const references have higher priority. - d) No overload: this call is ambiguous.
Answer: (d). A variable of type
c
binds equally well to overload 1 and overload 2, so the call is ambiguous.
3.Consider the code below:
/* 1 */ auto memcpy(char *dst, const char *src, int n = 0) -> void *;
/* 2 */ auto memcpy(const char *dst, char * const src) -> char *;
char *dst = /* appropriate initialisation... */;
const char *src = /* appropriate initialisation... */;
void *ptr = memcpy(dst, src);
Which overload of memcpy
would be selected and why?
- a) Overload 1: both overloads are equally viable but the return type of overload 1 matches
ptr
’s type better - b) Overload 2: has exactly two arguments and a non-bottom-level const pointer is always convertible to a bottom-level const pointer.
- c) Overload 1: the first two arguments match perfectly and the default argument is used for the third.
- d) Overload 2: the top-level const
src
argument has higher priority than the corresponding bottom-level constsrc
in overload 1.
Answer: (c). As the alternative states, overload 1 matches the first two arguments perfectly and the compiler has a default value for the 3rd argument, so it is selected.
4.Consider the code below
/* 1 */ auto min(int(&arr)[2]) -> int;
/* 2 */ auto min(int *arr) -> int;
/* 3 */ auto min(int(&arr)[]) -> int;
auto fn(int buf[3]) -> void {
min(buf);
}
Which overload of min
would be selected and why?
- a) Overload 1: though
min
was called with an array of length 3, 3 is close to 2, so this is the best match. - b) Overload 2: the
buf
argument decays toint *
and so overload 2 is the best match. - c) Overload 3: neither
int(&)[2]
norint *
matchint(&)[3]
perfectly but a reference to an array of unknown length does, so this is the best match. - d) No Overload: this call is ambigous.
Answer: (b). The parameter of
fn
is a red-herring – even thoughbuf
isint[3]
, this automatically decays toint *
, and so the only overload that accepts anint *
inmin
is overload 2.
5.Consider the code below:
/* 1 */ auto sink(int i, ...);
/* 2 */ auto sink(int i, short s);
/* 3 */ auto sink(...);
auto L = std::numeric_limits<long>::max();
sink(1, L);
Which overload of sink
would be selected and why?
- a) Overload 1: correct number of parameters and a variadic function is preferred over a narrowing conversion from
long
toshort
- b) Overload 2: correct number of parameters and variadic functions have the lowest priority in overload resolution, so this is the only viable candidate.
- c) Overload 3: by definition, a single-parameter variadic function can be called with any number and type of arguments, so it is always the best match.
- d) No Overload: this call is ambigous.
Answer: (b). By definition, C-style variadic functions have the lowest priority in overload resolution, so if the compiler can choose something more specific, it will. Thus, only overload 2 will be chosen.
Lab204: Programmable Errors
1.What kind of error is first displayed in the below code?
// api.h
int rand();
// me.cpp
#include "api.h"
int rand() {
return 42;
}
// you.cpp
int rand() {
return 6771;
}
// client.cpp
#include "api.h"
int i = rand();
- a) Compile-time error:
you.cpp
did not includeapi.h
- b) Compile-time error: multiple definitions of
rand()
. - c) Link-time error: multiple definitions of
rand()
. - d) Logic Error: 6771 is not a random number!
Answer: (c). Functions defined in source files are by default globally visible (even without including a header file) due to backwards compatibility with C. So, there are two definitions of
rand()
, which is a link-time error.
2.What kind of error is first displayed in the below code?
namespace constants {
#define N 6771
}
int N = constants::N;
int main() {
int ints[N] = {1, 2, 3};
}
- a) Logic error:
constants
is a bad name for a namespace - b) Compile-time error: macros do not obey namespace rules and so
int N
is changed toint 6771
. - c) run-time error: main does not return a value.
- d) Compile-time error:
N
is not const and so cannot be used inints[N]
.
Answer: (b). Macros (such as
#define
) are copied-and-pasted during the preprocessing phase of compilation and soint N = constants::N;
becomesint N = constants::6771;
, which is an illegal identifier. Thus, this is the first error to halt compilation.
3.What kind of error is displayed in the below code?
#include <vector>
int main() {
std::vector<int> v;
unsigned i;
while (i-- > 0) {
v.push_back(i);
}
}
- a) Link-time error:
i
is just a variable declaration and the reali
hasn’t been defined yet. - b) Logic error:
i
is uninitialised and so its use is illegal. - c) Logic error:
v
is not used after the for-loop. - d) Run-time error: pushing back continuously to a vector can result in an “out of memory” error
Answer: (b). Programs that use uninitialised variables are ill-formed and exhibit undefined behaviour (this is also true in C). Though (d) is also a potential error, use of
i
is concretely a logic error.
4.What kind of error is displayed in the below code?
int main() {
int *ptr = new int{42};
*ptr = 6771;
return *ptr;
}
- a) Logic-error: you are only allowed to return numbers in the range [-128, 128] from
main()
. - b) Runtime-error:
new
can fail allocation and throws an exception if that happens - c) Compile-time error:
int{42}
is invalid syntax. - d) Logic-error: programmer did not check if
ptr
wasnullptr
or not before dereferencing.
Answer: (b). Like how
malloc()
returnsnullptr
if it fails,new
will throw an exception, so the programmer needs to write code that can handle that (though most don’t). (a) seems plausible, butmain()
’s return value being clamped is a convention rather than a hard rule.
Week03
Lab304: Reversal
Consider the following code:
#include <iostream>
#include <vector>
int main() {
std::vector<int> temperatures = {32, 34, 33, 28, 35, 28, 34};
for (int i = 0; i < temperatures.size(); ++i) { // (*)
std::cout << temperatures.at(i) << " ";
}
std::cout << "\n";
for (const auto &temp : temperatures) { // (^)
std::cout << temp << " ";
}
std::cout << "\n";
for (auto iter = temperatures.cbegin(); iter != temperatures.cend(); ++iter) { // (&)
std::cout << *iter << " ";
}
std::cout << "\n";
}
1.Why is the for-loop marked with an (*) potentially more unsafe than the others?
- a) It is a C-style for-loop, and the index could overflow.
- b) It is a C-style for-loop, and the comparison of signed vs. unsigned integers can produce surprising results.
- c) It is a C-style for-loop, and this makes it inherently inferior to C++ style for-loops.
- d) It is a C-style for-loop, and it is possible we go off the end of the
temperatures
vector.
Answer: (b). The loop variable is of type
int
, whereasstd::vector<int>::size()
returns anunsigned
integer type. To do such a comparison, the compiler will implicity promote theint
to an unsigned value, which can be catastrophic if the integer was negative (all integers are represented as twos-complement). In this specific code snippet that won’t happen due totemperatures
having only a few elements, but in general comparison of signed and unsigned integers is a major source of bugs.
2.We want to iterate through temperatures
in reverse. Which loop in this code is easiest to change and why?
- a) (*): Index calculations are easy to do and most people are used to seeing index-based reverse iteration
- b) (^): range for-loops and an appropriate use of std::reverse conveys our intent the best.
- c) (^): all standard library containers provide reverse iterators.
- d) (&): just change the
cbegin
andcend
torbegin
andrend
.
Answer: (d). The change that has the least amount of typing (and thus collateral changes) and is easiest to read is (d). (a) is painfully subjective and (b) and (c) are factually incorrect.
3.What differences, if any, are there between temperatures.begin()
and temperatures.rend()
?
- a) An end-iterator, whether from
end()
orrend()
is “one-past-the-end”, and so is never dereferenceable, unlikebegin()
. - b) No difference:
begin()
==rend()
since the beginning of a range is the end of its reversal. - c) The only difference is the type:
begin()
returns aniterator
whereasrend()
returnsreverse_iterator
. Everything else is the same. - d)
rend()
would only compare equal tobegin()
iftemperatures
was empty.
Answer: (a).
begin()
andrend()
are completely distinct types and have different semantics. Therefore, (b), (c), and (d) cannot be the answer. (a) talks about the semantics of “end-like” iterators, so it is correct.
Lab306: Categorising Iterators
1.Consider this code:
#include <vector>
int main() {
const std::vector<int> v;
auto iter = v.begin();
}
What iterator type and category is iter
?
- a) constant iterator / random-access
- b)
const_iterator
/ contiguous - c) constant
const_iterator
/ contiguous - d) won’t compile
Answer: (b). By definition, a constant vector’s iterators are contiguous
const_iterator
s, even frombegin()
.
2.Consider this code:
#include <vector>
int main() {
const std::vector<int> v;
auto iter = v.cbegin();
}
What iterator type and category is iter
?
- a)
const_iterator
/ contiguous - b) constant
const_iterator
/ contiguous - c) constant iterator / contiguous
- d) won’t compile
Answer: (a). By definition, a constant vector’s iterators are contiguous
const_iterator
s, especially fromcbegin()
.
3.Consider this code:
#include <vector>
int main() {
const std::vector<int> v;
auto iter = (*v.begin())++;
}
What iterator type and category is iter
?
- a)
const_iterator
/ contiguous - b) constant iterator / contiguous
- c) constant
const_iterator
/ contiguous - d) won’t compile
Answer: (d). By definition, a constant vector’s iterators are contiguous
const_iterator
s, so it is impossible to modify the element pointed to by the iterator.
4.Consider this code:
#include <list>
int main() {
std::list<int> li;
auto iter = li.cbegin();
}
What iterator type and category is iter
?
- a) constant iterator / bi-directional
- b) iterator / forward
- c)
const_iterator
/ bi-directional - d) won’t compile
Answer: (c). By definition, a list’s iterators are bi-directional, and
cbegin()
returns aconst_iterator
.
5.Consider this code:
#include <forward_list>
int main() {
std::forward_list<int> forward_li;
auto iter = forward_li.cbegin();
}
What iterator type and category is iter
?
- a)
const_iterator
/ forward - b) constant iterator / forward
- c) iterator / bidirectional
- d) won’t compile
Answer: (a). By definition, a forward_list’s iterators are forward iterators, and
cbegin()
returns aconst_iterator
.
6.Consider this code:
#include <forward_list>
int main() {
const std::forward_list<int> forward_li;
auto iter = (*forward_li.begin())++;
}
What iterator type and category is iter
?
- a)
const_iterator
/ forward - b) iterator / forward
- c)
iter
is anint
- d) won’t compile
Answer: (d). It is impossible to modify the element pointed to by the
forward_li
’sconst_iterator
.
7.Consider this code:
#include <set>
int main() {
std::set<int> st;
const auto iter = st.begin();
}
What iterator type and category is iter
?
- a) constant iterator / bidirectional
- b) iterator / forward
- c) iterator / bi-directional
- d) won’t compile
Answer: (a). By definition, a set’s iterator is bi-directional and here is being stored in a
const
variable.
8.Consider this code:
#include <string>
int main() {
std::string s;
auto iter = s.begin();
}
What iterator type and category is iter
?
- a) iterator / forward
- b) iterator / contiguous
- c) iterator / random-access
- d) won’t compile
Answer: (b). By definition, a string’s iterator is contiguous and here is being stored in a regular variable.
9.Consider this code:
#include <iterator>
#include <iostream>
int main() {
auto iter = std::istream_iterator<int>(std::cin);
}
What iterator type and category is iter
?
- a)
const_iterator
/ input - b) iterator / input
- c) iterator / forward
- d) won’t compile
Answer: (b). By definition,
std::istream_iterator
is an input iterator and here is being stored in a regular variable.
10.Consider this code:
#include <iterator>
#include <iostream>
int main() {
auto iter = std::ostream_iterator<int>(std::cout, " ");
}
What iterator type and category is iter
?
- a) iterator / output
- b)
const_iterator
/ output - c) constant iterator / input
- d) won’t compile
Answer: (a). By definition,
std::ostream_iterator
is an output iterator and here is being stored in a regular variable.
Lab307: Algorithms Galore
1.Consider the code below:
auto first(const std::vector<int> &v, const int needle) {
for (auto i = v.begin(); i != v.end(); ++i) {
if (*i == needle) {
return i;
}
}
return v.end();
}
What standard algorithm can this code be replaced by?
- a) std::get
- b) std::find_if
- c) std::search
- d) std::find
Answer: (d). This is by definition
std::find
.std::find_if
accepts an extra predicate argument, but that is not the case here.
2.Consider the code below:
auto second(std::vector<int> &v, std::vector<int>::iterator new_first) {
auto copy = std::vector<int>(v.begin(), new_first);
v.erase(v.begin(), new_first);
return v.insert(v.end(), copy.begin(), copy.end());
}
What standard algorithm can this be replaced by?
- a) std::erase
- b) std::shift_left
- c) std::rotate
- d) std::shift_right
Answer: (c). This is by definition
std::rotate
.
3.Consider the code below
auto third(std::span<float> floats) {
auto v = std::vector<float>{};
for (auto f : floats) {
v.push_back(f);
}
auto m = std::numeric_limits<float>::max();
for (auto f : v) {
if (f < m) m = f;
}
auto M = std::numeric_limits<float>::min();
for (auto f : v) {
if (M < f) M = f;
}
return std::make_pair(m, M);
}
What sequence of standard algorithms can this reasonably be replaced by?
- a) std::copy -> std::min_element -> std::max_element
- b) std::copy -> std::minmax_element
- c) std::vector iterator constructor -> std::min_element -> std::max_element
- d) std::memcpy -> std::max_element -> std::min_element
Answer: (b). This is a copy of
floats
intov
followed by the determination of the minimum and maximum elements. It is more efficient to calculate the minmax in a single pass throughv
, hence (b) is the most applicable.
Week04
Lab406: Defaults & Deletes
1.Consider the below code:
struct point2i {
int x;
int y;
};
Is this class-type default-constructible and why?
- a) No: We need to opt-in to a default aggregate initialiser.
- b) Yes: default aggreggate-initialisation would leave
x
andy
uninitialised. - c) No: This is a C-style struct; it has no default constructor.
- d) Yes: default aggregate-initialisation would set
x
andy
to0
.
Answer: (b). This is a C-style struct, so it falls under the “aggregate” rules of initialisation. A default-constructed aggregate leaves its data members in their default-constructed state, which for
int
is uninitialised.
2.Consider the below code:
class employee {
public:
employee(int employeeno);
private:
int employeeno;
};
Is this class-type default-constructible and why?
- a) Yes: the compiler can automatically synthesise the default constructor if we don’t provide one.
- b) No: a user-provided constructor prevents automatic synthesis of a default constructor.
- c) No: we have not provided an in-class member initialiser.
- d) Yes:
int
itself has a default constructor, soemployee
’s default constructor simply delegates toint
’s one.
Answer: (b). As the reason states, a user-defined constructor will disable the default constructor to be compiler-synthesised.
3.Consider the below code:
struct point2i {
point2i() = default;
point2i(int x = 42, int y = 6771);
int x;
int y;
};
Is this class-type default-constructible and why?
- a) No: the two provided constructors are ambiguous when called with 0 arguments, so this code won’t compile.
- b) Yes: we have explicitly defaulted the default constructor.
- c) Yes: Though both constructors can be called with 0 arguments, the compiler prefers the explicitly defaulted default-constructor.
- d) Yes: Though both constructors can be called with 0 arguments, in overload resolution the second constructor has higher priority, so it will be called.
Answer: (a). Both provided constructors are callable with 0 arguments and so the overload set is ambiguous. Thus, it won’t compile.
4.Consider the below code:
struct 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?
- a) Yes: the default constructor will be called for
p
’s initialisation - b) No:
point2i(point2i &&)
is invalid syntax. - c) No:
point2i
is not copyable at all, sop
cannot be initialised. - d) Yes:
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.
Answer: (a). As of C++17, any temporary value returned from a function and stored into a new variable (such as
p
) is guaranteed to have copy elision (this is the return-value optimisation). So, no copy happens here, only default-construction, which has been defaulted and is available to be called by the compiler.
5.Consider the below code:
struct 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?
- a) Neither default-constructible nor copyable: we have not explicitly told the compiler that we want
outer
to have the default constructor and copy/move constructors generated for us. - b) Default-constructible but not copyable:
guard
’s explicitly deleted copy/move constructor prevents the implicitly generated copy/move constructors forouter
. For a similar reason,guard
does allow for the implicitly generated default constructor. - c) Won’t compile:
guard
prevents the implicit copy/move constructors forouter
to be generated, as well the default constructor. Therefore, this class cannot be constructed, which is a compiler error. - d) Default-constructible and copyable:
guard
has no effect on the implicitly generated default, copy, and move constructors forouter
since it is astruct
. Ifouter
were aclass
, it would only be default-constructible, however.
Answer: (b): The compiler-generated copy/move special members are only generated if all the data members support it. Here,
guard
does not support copying or moving, so the compiler won’t generate them forouter
. However,guard
is default constructible, so so isouter
since no user-defined constructor is given.
Lab407: Construction Confusion
1.Consider the below code snippet:
std::vector<int> a(1, 2);
What is this line doing?
- a) Default construction.
- b) Construction via Direct Initialisation.
- c) Function declaration.
- d) From C++11 onwards, this is invalid syntax; won’t compile.
Answer: (b). According to language rules, this is “Direct Initialisation” since the initialising parentheses appear immediately to the right of the variable name.
2.Consider the below code snippet:
std::vector<int> a{1, 2};
What is this line doing?
- a) From C++11 onwards, this is invalid syntax; won’t compile.
- b) Function declaration.
- c) Construction via Aggregate Initialisation.
- d) Construction via Uniform Initialisation.
Answer: (d). By language rules this is “Direct Initialisation”, but since that option isn’t listed, the next best answer is “Uniform Initialisation”.
std::vector
is not an aggregate (since it is non-trivial class type), therefore (c) does not apply.
3.Consider the below code snippet:
std::vector<int> b = {1, 2};
What is this line doing?
- a) Construction via Copy Initialisation.
- b) Construction by Assignment Initialisation.
- c) Construction via Uniform Initialisation.
- d) Construction via Direct Initialisation.
Answer: (c). This is “Uniform Initialisation”, but not “Direct Initialisation” due to the brace placement.
4.Consider the below code snippet:
std::vector<int> a{1, 2};
std::vector<int> c = a;
What is this line doing?
- a) Construction via Copy Initialisation
- b) Copy assignment of
a
toc
. - c) Construction via Assignment Initialisation
- d)
c
is “stealing” the data members ofa
to construct itself.
Answer: (a). This is “Copy Initialisation” (a.k.a) copy construction. Construction occurs when a type is introduced along with a variable name.
5.Consider the below code:
std::vector<int> a{1, 2};
std::vector<int> c;
c = a;
What is this line doing?
- a) Reconstruction of
c
froma
. - b) Construction via Copy Initialisation.
- c) Copy assignment of
a
toc
. - d) Aggregate assignment of
a
toc
.
Answer: (c). This is copy assignment since
a
is being stored into a pre-existing variablec
.
Week05
Lab502: Overloaded Operators
For the following questions, consider the below class:
class vec3 {
public:
auto operator[](int n) { return elems_[n]; }
friend auto operator>>(std::istream &is, vec3 &v) -> std::istream&;
private:
double elems_[3] = {0, 0, 0};
}
1.In what cases would one need to overload an operator for a const and non-const version?
- a) Always; it is impossible to know how your code will be used in the future.
- b) Only when the operator is a member function and not a non-member function.
- c) When the operator has both a “getter” and “setter” version the two overloads are necessary.
- d) You should only add a const version if the return type is a reference type.
Answer: (c). A const-qualified and non-const qualified version is only needed when a getter and setter is needed. If only a getter is needed, then the method should be const-qualified. If only a setter is needed, then the method should not be const-qualified.
2.What could be the return type of vec3::operator[]
as it is currently written?
- a)
double *
- b)
double &
- c)
volatile double
- d)
double[]
Answer: (b). The most correct answer is a
double&
.
3.Is the vec3
class currently const-correct? If not, how would you change it to be so?
- a) Not const-correct:
operator[]()
needs a const version so const-objects can still be indexed into. The returned value must not be modifiable, however. - b) const-correct:
elems_
is not const, so this class can never have const-qualified methods. - c) Not const-correct: the
int n
parameter inoperator[]()
is not bottom-level const-qualified. - d) const-correct:
auto
correctly deduces the right const-correct type depending on ifthis
is a const-object or not.
Answer: (a).
operator[]()
should have both a getter and setter version, so as this class stands it is not const-correct.
4.Given the serialised format for a vec3
is double1 double2 double3
, what could be a potential implementation for operator>>
and why?
// a
// fill the 3 elems of v via a standard algorithm.
// no need to return "is" for chaining (due to the serialised format of vec3)
auto operator>>(std::istream &is, vec3 &v) -> std::istream& {
std::copy(std::istream_iterator<double>{is}, std::istream_iterator<double>{}, v.elems_, v.elems_ + 3);
}
// b
// fill the 3 elems of v.
// return "is" for chaining.
auto operator>>(std::istream &is, vec3 v) -> std::istream& {
is >> v.elems_[0];
is >> v.elems_[1];
is >> v.elems_[2];
return is;
}
// c
// fill the 3 elements of v from is.
// return "is" for chaining.
auto operator>>(std::istream &is, vec3 &v) -> std::istream& {
is >> v.elems_[1];
is >> v.elems_[2];
is >> v.elems_[3];
return is;
}
// d
// fill the 3 elems of v via a standard algorithm.
// return "is" for chaining.
auto operator>>(std::istream &is, vec3 &v) -> std::istream& {
std::copy(std::istream_iterator<double>{is}, std::istream_iterator<double>{}, v.elems_);
return is;
}
Answer: (d). Use of Standard Algorithms (good), returned is
for chaining (following the convention set in the Standard Library).
5.Is friendship necessary for vec3
’s operator>>
overload? Why or why not?
- a) Necessary: every implementation must use
elems_
, which is private – can only access this via friendship. - b) Necessary: non-member operator overloads should always be hidden friends.
- c) Not necessary:
operator>>
could potentially usevec3::operator[]()
(which is public) to fill the vec3’s elements, so defining it as a friend is superfluous. - d) Not necessary:
operator>>
can be implemented as a member function, and we only ever use it like so:my_vec3 >> std::cin
Answer: (c). If a non-member operator overload can be implemented in terms of a class’s public interface, then it should be – friendship is not necessary in that case. If that interface is not available, however, then friendship can be used.
Lab508: Iterator Invalidation
1.Consider the following code:
auto s = std::unordered_set<int>{1, 2, 3};
auto iter = s.find(1);
s.insert(4);
std::cout << *iter << std::endl;
Has iterator invalidation happened here, and if so, why?
- a) Yes: inserting into an array-based container always causes iterator invalidation.
- b) No:
std::unordered_set
always keeps its array length as a prime number and, since the next biggest prime after 3 is 7, there is enough space to insert a new element without a rehash. - c) Possible: if the internal load factor has been exceeded and the elements had to be copied and rehashed into a new array, then all iterators are invalidated.
- d) Yes:
std::unordered_set
always keeps its array length as a prime number and, since the capacity before inserting is equal to the number of elements, it is guaranteed there will be a new array allocation and rehash, invalidating all iterators.
Answer: (c). According to the C++ standard,
std::unordered_set
’s iterators are only invalidated through insertion if that insertion causes a rehash.
2.Consider the following code:
auto v = std::vector{};
v.reserve(2);
for (auto i = 0; i < 2; ++i) {
v.push_back(i);
}
auto iter = v.begin();
v.push_back(3);
std::cout << *iter << std::endl;
Has iterator invalidation happened here, and if so, why?
- a) Possible: if the capacity of
v
hasn’t been reached yet, then there will be no invalidation. Otherwise, there will be. - b) Yes: we reserved two spaces and filled them all so, at the time of the next push back, a new array will be allocated and all elements moved over, which will invalidate iterators.
- c) No: We ensured there was enough space in the vector when we called
reserve()
. - d) Possible: irrespective of calls to
reserve()
, almost all implementations ofstd::vector
grow geometrically and keep at least twice as much space as they report to viacapacity()
. If the true internal capacity has been reached, then the iterators will be invalidated, otherwise they won’t be.
Answer: (b). As the alternative states, two spaces are reserved and then a third element is pushed back. This will cause a resize, which invalidates all iterators.
3.Consider the following code:
auto v = std::vector{3, 2, 1};
auto iter = v.begin();
while (iter != v.end() - 1) {
iter = v.erase(std::find(v.begin(), v.end(), *iter));
}
std::cout << *iter << std::endl;
Has iterator invalidation happened here, and if so, why?
- a) No: whilst modifying a vector we’re looping over usually is disastrous, we are reassigning the loop variable
iter
every time to ensure it remains valid. - b) Yes: you should never modify vectors when you loop over them.
- c) Yes: because an iterator separate to
iter
is passed tov.erase()
, it invalidatesiter
. If, however, we had writteniter = v.erase(iter);
, it would not be invalidated. - d) Possible: implementors of
std::vector
are free to choose whether or not this specific use-case invalidates iterators, so it depends on which version of the standard library you compile with.
Answer: (a).
std::vector::erase
specifically returns a new iterator to allow loops like this without making an ill-formed program. By reassigning the loop variable every iteration, we ensure that access toiter
is always valid and safe.
4.Consider the following code:
auto s = std::set<int>{1, 2, 3};
auto iter = s.find(3);
s.erase(2);
std::cout << *iter << std::endl;
Has iterator invalidation happened here, and if so, why?
- a) No: erasing an unrelated element from a
std::set
has no effect oniter
. - b) Yes:
std::set
, as a binary search tree, always rebalances itself after every modification. - c) Possible:
std::set
, as a red-black tree, may rebalance itself if the erased element is in the ancestry ofiter
. - d) No:
std::set
only invalidates iterators when it is moved-from (i.e., in code likeauto s2 = std::move(s)
).
Answer: (a). As a linked container,
std::set
’s iterators are only invalidated when the element that iterator points to is removed. This is because the element is copied into heap-allocated memory. The internal structure of std::set may rebalance itself for efficency, but the elements themselves don’t move, and thus the iterators remain valid.
Week07
Lab702: Stack Unwinding
1.What is stack unwinding?
- a) The process of finding an exception handler, leaving any stack frames in the same state since the OS will automatically reclaim the memory.
- b) The process of examining the stack frame to find any potential errors in our code.
- c) The process of popping stack frames until we find an exception handler for a thrown exception.
- d) The process of printing out to the terminal all the variables that exist in each stack frame as an exception propagates.
Answer: (c). This is, by definition, what stack unwinding is.
2.What happens during stack unwinding?
- a) Relevant destructors are called on objects on the way out and any exceptions thrown in a destructor (that are not caught and handled in that destructor) cause
std::terminate
to be called. - b) Each stack frame’s memory is passed to
delete
orfree()
, which will invoke the relevant destructors for objects and any exceptions thrown from a destructor causestd::terminate
to be called. - c) Relevant destructors are called on objects on the way out and any exceptions thrown in a destructor cause
std::terminate
to be called. - d) Each stack frame’s memory is passed to
delete
orfree()
, which will invoke the relevant destructors for objects and any exceptions thrown from a destructor (that are not caught and handled in that destructor) causestd::terminate
to be called.
Answer: (a). The C++ compiler ensures that all destructors for in-scope variables are called when stack unwinding begins. If an exception is thrown in a destructor, that destructor must handle it and not let it escape, otherwise
std::terminate
is called. This is why all destructors arenoexcept
(either implicitly or explicitly).
3.What issue can this potentially cause? If an issue is caused, how would we fix it?
- a) No issues are caused: every type has a destructor (even fundamental types like pointers), as required by the ISO C++ Standard.
- b) It could potentially cause an issue, depending on if we use pointers to heap memory. If we don’t use pointers, there is no problem, but if we do use pointers, then we must ensure that that pointer is managed by an RAII class (such as
std::unique_pointer
). - c) If unmanaged resources were created before an exception is thrown, they may not be appropriately released. The solution is to ensure that every resource is owned by a Standard Library container, such as
std::vector
. - d) If unmanaged resources were created before an exception is thrown, they may not be appropriately released. The solution is to ensure that every resource is owned by an RAII-conformant class.
Answer: (d). Resources that are not managed by an RAII class may be leaked if an exception is thrown. (b) and (c) are not correct as resources include more than just memory (such as sockets, mutexes, etc., are also resources!) and there are more RAII classes than just the Standard containers.
Week08
Lab802: Visualising Tables
Consider the following class hierarchy:
struct A {
virtual void f(int) {}
virtual void g() {}
void a() {}
virtual ~A() {}
};
struct B : A {
void f(int) override {}
virtual void h(int) final {}
static void b() {}
};
struct C : B {
virtual void f(int, int) {}
virtual void x() {}
void h(int) override {}
};
Below is a representation of the incomplete vtables for A
, B
, and C
:
|A|B|C| |-|-|-| |A::f(int)|B::f(int)|$| |!|@|C::f(int, int)| |~A()|~B()|~C()| |A::g()|#|%| |-|B::h()|A::g()| |-|-|C::x()| |VTABLE END|VTABLE END|VTABLE END| Note 1:
-
denotes an empty slot: nothing is meant to be there.
Note 2: This is not necesarily how a vtable
would be created by the compiler.
1.In the slot marked !, what would be the most appropriate entry and why?
- a)
A::A()
: The constructor needs to bevirtual
so that derived classes and initialise theirA
subobject. - b)
A::a()
: it is the only remaining function not in the vtable. - c)
A::a()
: Once one method is madevirtual
, all methods should bevirtual
as a matter of good code style. - d) Nothing: none of the above.
Answer: (d). Constructors are never virtual and
A::a()
is also notvirtual
(only virtual methods go in the vtable), so none of the alternatives except (d) match.
2.In the slot marked @, what would be the most appropriate entry and why?
- a)
B::b()
: this is the only method not yet listed in theB
’s vtable. - b)
A::~A()
: In order forB::~B()
to function correctly,A::~A()
also needs to be inB
’s vtable. - c)
B::g()
: anyvirtual
method of the parent, if not explicitly overrided, has an implicit override with a default implementation that simply calls the parent’s version of the method. - d) Nothing: none of the above.
Answer: (d).
B::b()
isstatic
, so it will never appear in the vtable. SinceA::~A()
isvirtual
,B
’s is also by default and it will callA
’s destructor – it need not be in the vtable. (c) is simply not true. Therefore, (d).
3.In the slot marked #, what would be the most appropriate entry and why?
- a)
A::g()
:B
has not explicitly overridden this method fromA
, so it inheritsA
’svirtual
implementation. - b)
B::g()
: anyvirtual
method of the parent, if not explicitly overrided, has an implicit override with a default implementation that simply calls the parent’s version of the method. - c)
B::b()
: though it isstatic
, by putting this method intoB
’s vtable, it will be able to be overridden by derived classes. - d) Nothing: none of the above.
Answer: (a). The alternative states the reason perfectly.
4.In the slot marked $, what would be the most appropriate entry and why?
- a)
C::f(int)
:C
has overridenA::f(int)
. - b)
C::f(int)
:C
has overridenB::f(int)
. - c)
B::f(int)
:B
explicitly overrodeA::f(int)
, butC
has not explicitly overriddenB::f(int)
. - d) Nothing: none of the above.
Answer: (c). Again, the alternative states the reason perfectly. (a) and (b) are both untrue, and (d) is also incorrect.
5.In the slot marked %, what would be the most appropriate entry and why?
- a)
C::h(int)
: (despite the compilation error) thoughB
has marked this method asfinal
, theoverride
specifier overrules this and successfully allowsC
to overrideB::h(int)
. - b)
C::h(int)
: This code does not compile becauseC
has not explicitly added avirtual
destructor. - c)
B::h(int)
: (despite the compilation error)B
has marked this method asfinal
, meaning it cannot be further overridden by derived classes. - d) Nothing: none of the above.
Answer: (c). The
final
specifier on virtual methods means that derived classes cannot override them. This code will fail to compile thanks tofinal
.
Lab804: Static Dynamo
Consider the
#include <iostream>
struct banana {
virtual void f() {
std::cout << "banana ";
}
};
struct door : banana {
void f() override {
std::cout << "door ";
}
};
int main() {
banana b;
door d;
b = d;
banana &bref = dynamic_cast<banana&>(b);
door &dref = d;
banana &dbref = dynamic_cast<banana&>(d);
b.f();
d.f();
bref.f();
dref.f();
dbref.f();
}
1.For each of bref
, dref
, and dbref
: by the end of the program, what is this variable’s static and dynamic type?
- a)
bref
: static:door&
, dynamic:banana&
dref
: static:banana&
, dynamic:door&
dbref
: static:door&
, dynamic:door&
- b)
bref
: static:door&
, dynamic:banana&
dref
: static:door&
, dynamic:door&
dbref
: static:door&
, dynamic:banana&
- c)
bref
: static:banana&
, dynamic:door&
dref
: static:door&
, dynamic:door&
dbref
: static:banana&
, dynamic:door&
- d)
bref
: static:banana&
, dynamic:banana&
dref
: static:door&
, dynamic:door&
dbref
: static:banana&
, dynamic:door&
Answer: (d). The static type is the declared type of a variable. Hence,
b
has static typebanana
,d
has static typedoor
,bref
anddbref
have static typebanana&
, anddref
has static typedoor&
. The dynamic type is the type a variable actually is at runtime.b
andd
are neither references or pointers. Hence, their dynamic type is always the same as their static type. Hence,b
andd
have dynamic typesbanana
anddoor
respectively.bref
,dref
, anddbref
are references. Hence, their dynamic type is the dynamic type of the object they refer to. Hence, bref has dynamic typebanana&
,dref
has dynamic typedoor&
, anddbref
has dynamic typedoor&
.
2.Is there anything wrong with the assignment b = d
?
- a) Yes: we have not defined
operator=
forbanana
. - b) Yes: since
b
is abanana
, assigningd
to it will caused
’sdoor
half to be sliced off. This is the object slicing problem. - c) No: this code is perfectly legal code.
- d) Maybe: since
sizeof(banana) == sizeof(door)
, the result of this expression depends on the version of the compiler.
Answer: (b). When you try and assign a derived class into a variable of type base class, the base class only has sufficient space for the base class subobject. Hence, it copies over the base class subobject and completely ignores the derived class’ data.
3.In general, how is the object-slicing problem avoided?
- a) It cannot be avoided: C++’s value semantics preclude this possibility.
- b) Only use
std::unique_ptr
in code that uses polymorphic objects. - c) When dealing with polymorphic objects, always make sure when the static and dynamic types don’t align to use either a pointer or a reference.
- d) Make sure the size of polymorphic classes is always the same so that even if slicing occurs, there are no side-effects.
Answer: (c). The alternative states the reason perfectly.
Week09
Lab902: Instantiation
1.Consider the following code:
#include <string>
template <typename T>
T my_max(T a, T b) {
return b < a ? b : a;
}
auto main() -> int {
auto result = 7;
auto cat = std::string{"cat"};
auto dog = std::string{"dog"};
my_max(1, 2);
my_max(1.0, 2.0);
my_max(cat, dog);
my_max('a', 'z');
my_max(7, result);
my_max(cat.data(), dog.data());
}
How many template instantiations are there (not including any from std::string
)?
- a) 6
- b) 5
- c) 3
- d) 4
Answer: (b). The instantiations are:
int max(int, int)
double max(double, double)
std::string max(std::string, std::string)
char max(char, char)
const char *max(const char *, const char *)
2.Consider the following code:
template <typename T, int N>
class array {
public:
array() : elems_{} {}
const T *data() const;
private:
T elems_[N];
};
template<typename T, int I>
void manipulate_array(const array<T, I> arr) {
arr.data(); // such a complex manipulation;
}
int main() {
void (*fp1)(const array<int, 3>) = manipulate_array;
void (*fp2)(const array<float, 3>) = manipulate_array;
void (*fp3)(const array<char, 4>) = manipulate_array;
array<float, 3> arr;
(*fp2)(arr);
}
How many (function, class, member) template instantiations are there?
- a) 3
- b) 4
- c) 5
- d) 6
Answer: (d). The instantiations are:
array<float, 3>
array<float, 3>::array()
array<float, 3>::data()
void manipulate_array(const array<int, 3> arr)
void manipulate_array(const array<float, 3> arr)
void manipulate_array(const array<char, 4> arr)
Taking the pointer of a function template to a concrete type will cause a template instantiation, and this is done 3 times. Class templates are instantiated at a different time to when their member functions are instantiated. array<float, 3>
is used in main()
, and so it, as well as its default constructor get instantiated. Though array<float, 3>::data()
is used in void manipulate_array(const array<float, 3> arr)
, it does not have a template definition, so it cannot be instantiated. The same logic applies to the other two versions of manipulate_array
.
Lab904: Special Selection
1.Consider the following code:
template <typename ...Ts>
struct sink {}; /* 1 */
template <typename T>
struct sink<int, T> {}; /* 2 */
template <typename T, typename ...Ts>
struct sink<int, Ts...> {}; /* 3 */
using sunk = sink<int, void>;
Which specialisation would be selected and why?
- a) (3): This is the only specialisation where the first template parameter matches perfectly, and
Ts...
has higher priority than a singleT
. - b) (2): This is the only specialisation where the first template parameter matches perfectly, and
Ts...
has lower priority than a singleT
. - c) (1): The primary template is the most general of all of the specialisations and so it is the best match.
- d) Compilation error: 2 & 3 are equally viable.
Answer: (b). Alternative gives the reason perfectly.
2.Consider the following code:
template <typename T, const int *Arr>
struct sink {}; /* 1 */
template <typename T, auto Arr>
struct sink<const T, Arr> { }; /* 2 */
template <typename T, const int *Arr>
struct sink<T&, Arr> {}; /* 3 */
constexpr int arr[3] = {};
using sunk = sink<const short&, arr>;
Which specialisation would be selected and why?
- a) (3):
arr
decays to aconst int *
and matches (3)’s template parameter pefectly andT&
has higher priority overconst T
andT
. - b) (2):
auto
non-type template parameters are more flexible than other types and are preferred. Also,const T
has higher preference overT&
andT
. - c) (1): 2 & 3 are ambiguous since
const T
is equally viable withT&
in this case. The compiler falls back on (1). - d) (3): (3)’s
const int *
non-type template parameter aligns with the primary template’s non-type template parameter pefectly, so it is by default the most specialised candidate.
Answer: (a). Alternative gives the reason perfectly.
3.Consider the following code:
template <typename T>
void foo(T); /* 1 */
template <typename T>
void foo(T *); /* 2 */
template <>
void foo(int *); /* 3 */
void foo(const int *); /* 4 */
int main() {
int p = 0;
foo(&p);
}
Which specialisation would be selected and why?
- a) (1): Being the most general, this function template can be used with any argument, and so is selected as it is the best match in all cases of calls to
foo()
with a single argument. - b) (2): In overload resolution, (1), (2), and (4) are considered. (1) does not match a pointer as well as (2) and (4), so it drops out. The compiler is able to synthesise a function that matches
int *
better thanconst int *
, so (4) drops out. The compiler then instantiates (2), as (3) is an explicit specialisation, which is not allowed according to the C++ Standard. - c) (4):
int *
is always convertible to aconst int *
and is a real function (rather than a template). Therefore, it is the best match. - d) (3): In overload resolution, (1), (2), and (4) are considered. (1) does not match a pointer as well as (2) and (4), so it drops out. The compiler is able to synthesise a function that matches
int *
better thanconst int *
, so (4) drops out. Finally, the compiler searches for any relevant specialisations of (2) and finds (3), so it is selected.
Answer: (d). Alternative gives the reason perfectly.
Week10(Weeka)
Laba02: Bound Up
Consider the following code:
void value(std::string v) {}
void lvalue(std::string &lv) {}
void c_lvalue(const std::string &clv) {}
void rvalue(std::string &&rv) {}
void tvalue(auto &&tv) {}
int main() {
value("hello");
lvalue("hello"); // Won't compile
c_lvalue("hello");
rvalue("hello");
tvalue("hello");
std::string s0 = "world";
std::string s1 = "world";
std::string s2 = "world";
std::string s3 = "world";
std::string s4 = "world";
std::string s5 = "world";
value(s0);
lvalue(s0);
c_lvalue(s0);
rvalue(s0); // Won't compile
tvalue(s0);
value(std::move(s1));
lvalue(std::move(s2)); // Won't compile
c_lvalue(std::move(s3));
rvalue(std::move(s4));
tvalue(std::move(s5));
}
1.Why won’t lvalue("hello")
compile?
- a)
"hello"
is actually aconst char *
and not astd::string &
. - b)
"hello"
would bind tolvalue()
if we appended the literal suffixs
to it. - c)
"hello"
is implicitly converted to astd::string
, but temporaries cannot bind to mutable lvalue references. - d)
lv
(function parameter) is not used and under all compiler environments this is a compilation error.
Answer: (c).
std::string
has a converting constructor fromconst char *
, so this expression will implicitly create a temporarystd::string
. Temporaries cannot bind to lvalue references.
2.Why won’t rvalue(s0)
compile?
- a)
s0
is astd::string
and, when used in a function call expression, will be converted to an lvalue reference. Lvalue references cannot bind to rvalue references. - b)
s0
is astd::string
and values cannot bind to rvalue references. - c) We have to use to
std::move()
to converts0
to an rvalue reference otherwise this program has undefined behaviour. - d)
rvalue
is not const-qualified.
Answer: (a). The alternative gives the reason perfectly.
3.Why does c_lvalue(std::move(s3))
compile?
- a) Because we have used a special compiler flag to make this compile (
-pedantic
) - b) The rvalue from
std::move(s3)
is about to go out of scope, and it is illegal to modify rvalues. Luckily,c_lvalue
’s parameter is const, so this isn’t an issue. - c) The compiler knows
s3
is never used again after this function call, so it is type-safe to bind an rvalue reference to a const lvalue reference. - d) Everything, even rvalue references, are convertible to const lvalue references.
Answer: (d). Pre-C++11,
const T&
were used as a “catch-all” parameter to allow passing arguments of arbitrary value category to functions. To preserve this backward compatibility, even rvalue references to will bind to const-lvalues.
4.What is the deduced type of tv
in each of tvalue()
’s calls?
- a)
- tv:
const char *
- tv:
const std::string &
- tv:
std::string
- tv:
- b)
- tv:
const char *
- tv:
std::string &
- tv:
std::string &&
- tv:
- c)
- tv:
const char(&)[6]
- tv:
const std::string &
- tv:
std::string
- tv:
- d)
- tv:
const char(&)[6]
- tv:
std::string &
- tv:
std::string &&
- tv:
Answer: (d).
- From C, string literals are actually implicit character arrays with a terminating null character. It is only when used in function call expressions that an array will decay into a pointer. In the first call, the forwarding references captures the type of
tv
perfectly, so there is no decay into a pointer –tv
is a reference to a character array of length 6.- In function call expressions, variables are implicitly converted to lvalue references, and so
tv
is astd::string &
.std::move()
’s behaviour is to convert its argument into an rvalue reference, hencetv
is astd::string&&
.
Laba04: Inferential Declaration
1.Consider the following code:
int main() {
int i = 5;
int &j = i;
decltype((j)) var = /* initialiser */;
}
What is the type of var
and why?
- a)
int
:j
is a reference to anint
and when used in expressions its value is copied into a temporary of typeint
. - b)
int &
:j
is anint&
and(j)
is equivalent toj
in every context. - c)
int &
:j
is anint&
and a parenthesised expression of anint&
is still anint&
. - d)
int &&
:j
is anint&
and a parenthesised expression of anint&
is anint&&
.
Answer: (c). According to the rules for
decltype
, parenthesised variable expressions are by definitino lvalues, thusvar
is also anint&
.
2.Consider the following code:
int main() {
int i = 5;
decltype((std::move(i)) var = /* initialiser */;
}
What is the type of var
and why?
- a)
int &
:decltype
, when used with parentheses, always is an lvalue. - b)
int
: it is illegal to have stack-allocated variables as rvalue references – only function parameters can have this type. - c)
int &&
:decltype
preserves the value category of its argument, andstd::move()
converts its argument to an rvalue reference. - d)
int &
:decltype
, when used with parentheses, always is an lvalue. Thus,decltype(std::move(i))
is equivalent todecltype (int & &&)
, which collapses toint&
.
Answer: (c). Only for template parameters does reference collapsing with rvalue references happen. Here, we have a concrete rvalue type returned from
std::move
and the parentheses don’t affect rvalues. Thus,var
isint&&
.
3.Consider the following code:
int main() {
decltype((5)) var = /* initialiser */;
}
What is the type of var
and why?
- a)
int
:decltype
preserves the value category of its argument, and integer literals have typeint
. The parentheses have no effect on literals. - b)
int &
: There is a special case withdecltype
that states if a literal is used with parentheses then memory must be allocated for an lvalue reference. If this wasn’t the case,var
would be a dangling reference to an expired value. - c)
int &
:decltype
, when used with parentheses, always is an lvalue. Thus,var
isint&
. - d)
(int&&) &
:int
literals have typeint&&
and(&)
is deduced as an lvalue. Altogether(int &&) &
.
Answer: (a). Similar to the previous answer,
decltype
parentheses have no effect on non-lvalues. Thus,var
is just anotherint
.
4.Consider the following code:
constexpr auto foo(const auto &f) -> decltype(auto) {
if constexpr (sizeof(f) != sizeof(void *)) {
auto val = f;
return val;
} else {
auto val = *f;
return val;
}
}
int main() {
constexpr int arr[3] = {};
auto var = foo(arr);
}
What is the type of var
and why?
- a)
const int(&)[3]
: The type off
isconst int(&)[3]
. This means thatsizeof(f)
will be unequal tosizeof(void*)
and we enter the firstif
branch. Here, we are simply returning a reference we were passed, thusconst int(&)[3]
. - b)
const int *
: The type off
isconst int(&)[3]
, andfoo
returns a reference to an array from theval
lvalue. Becausevar
is declared asauto
, the return array reference decays into a pointer inside of themain()
function and sovar
is aconst int *
. - c)
const int(&&)[3]
: Because an lvalue toarr
was passed tofoo
,f
’s type is deduced to beconst int (&&)[3]
. We then copy this reference intoval
and return it fromfoo
. This rvalue reference is finally stored intovar
, henceconst int(&&)[3]
. - d)
const int *
: The type off
isconst int(&)[3]
, and the assignmentval = f
causesf
to decay to a pointer. We then simply return a pointer, which is copied intovar
.
Answer: (d). There is alot going on here:
decltype(f)
isconst int(&)[3]
.- Because
auto
never deduces references (or top-level const!), the assignmentval = f
will cause the array reference inf
to decay to aconst int *
.- We then copy by value the pointer out to
var
.- Thus,
var
is aconst int *
.
08-05-2023 01:45-01