COMP6771 Multichoices Collection

C++的多选题集合

Posted by Tron on May 7, 2023

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 the CHECKs and REQUIREs 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 and assert are both macros and do the exact same thing.
  • b) CHECK and assert are both macros, but a CHECK will evaluate an expression and report it if it’s false whereas assert will crash the program.
  • c) CHECK is a function that suggests a fact about our code should be true, but assert enforces it.
  • d) CHECK records the failure of an assertion but does nothing about it and is entirely unrelated to assert.

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 but assert will report it to the user.
  • b) REQUIRE and assert both evaluate expressions and terminate the currently executing test if false.
  • c) assert and REQUIRE both evaluate expressions, but only assert 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 to assert.

Answer: (d)

4.What are SECTION blocks in Catch2?

  • a) SECTION blocks are ways to divide testing logic in TEST_CASEs. Any state changes in a SECTION are not reflected in SECTIONs at the same level.
  • b) SECTION blocks are a way to break up long tests and have little use other than that.
  • c) SECTIONs are unique testing scopes that can only contain TEST_CASEs.
  • d) SECTIONs are part of Behaviour Driven Development and group together SCENARIOs.

Answer: (a)

5.What goes between the parentheses in TEST_CASEs and SECTIONs?

  • 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 and const 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 through my_age is illegal.
  • d) No: my_age is a copy of age and modifying my_age has no impact on age 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 a char and overload 3 is just a redeclaration of overload 1.
  • b) Overload 2: char is implicitly promotable to int and so overload 2 is the best match.
  • c) Overload 3: put was called with a temporary const char.
  • d) Overload 4: put was called with a temporary char and temporaries preferentially bind to references.

Answer: (a). A character literal has type char. This cannot bind to char& (overload 4 is not viable) and will not bind to an int without a cast (overload 2 is not viable). For the purposes of overload resolution, const char and char 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 a char.
  • b) Overload 2: put was called with a mutable char and and references have higher priority.
  • c) Overload 3: put was called with a const char 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 const src 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 to int * and so overload 2 is the best match.
  • c) Overload 3: neither int(&)[2] nor int * match int(&)[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 though buf is int[3], this automatically decays to int *, and so the only overload that accepts an int * in min 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 to short
  • 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 include api.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 to int 6771.
  • c) run-time error: main does not return a value.
  • d) Compile-time error: N is not const and so cannot be used in ints[N].

Answer: (b). Macros (such as #define) are copied-and-pasted during the preprocessing phase of compilation and so int N = constants::N; becomes int 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 real i 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 was nullptr or not before dereferencing.

Answer: (b). Like how malloc() returns nullptr 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, but main()’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, whereas std::vector<int>::size() returns an unsigned integer type. To do such a comparison, the compiler will implicity promote the int 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 to temperatures 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 and cend to rbegin and rend.

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() or rend() is “one-past-the-end”, and so is never dereferenceable, unlike begin().
  • 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 an iterator whereas rend() returns reverse_iterator. Everything else is the same.
  • d) rend() would only compare equal to begin() if temperatures was empty.

Answer: (a). begin() and rend() 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_iterators, even from begin().

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_iterators, especially from cbegin().

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_iterators, 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 a const_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 a const_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 an int
  • d) won’t compile

Answer: (d). It is impossible to modify the element pointed to by the forward_li’s const_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 into v followed by the determination of the minimum and maximum elements. It is more efficient to calculate the minmax in a single pass through v, 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 and y uninitialised.
  • c) No: This is a C-style struct; it has no default constructor.
  • d) Yes: default aggregate-initialisation would set x and y to 0.

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, so employee’s default constructor simply delegates to int’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, so p 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 for outer. For a similar reason, guard does allow for the implicitly generated default constructor.
  • c) Won’t compile: 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.
  • d) Default-constructible and copyable: 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.

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 for outer. However, guard is default constructible, so so is outer 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 to c.
  • c) Construction via Assignment Initialisation
  • d) c is “stealing” the data members of a 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 from a.
  • b) Construction via Copy Initialisation.
  • c) Copy assignment of a to c.
  • d) Aggregate assignment of a to c.

Answer: (c). This is copy assignment since a is being stored into a pre-existing variable c.

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 in operator[]() is not bottom-level const-qualified.
  • d) const-correct: auto correctly deduces the right const-correct type depending on if this 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 use vec3::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 of std::vector grow geometrically and keep at least twice as much space as they report to via capacity(). 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 to v.erase(), it invalidates iter. If, however, we had written iter = 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 to iter 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 on iter.
  • 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 of iter.
  • d) No: std::set only invalidates iterators when it is moved-from (i.e., in code like auto 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 or free(), which will invoke the relevant destructors for objects and any exceptions thrown from a destructor cause std::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 or free(), which will invoke the relevant destructors for objects and any exceptions thrown from a destructor (that are not caught and handled in that destructor) cause std::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 are noexcept (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 be virtual so that derived classes and initialise their A subobject.
  • b) A::a(): it is the only remaining function not in the vtable.
  • c) A::a(): Once one method is made virtual, all methods should be virtual as a matter of good code style.
  • d) Nothing: none of the above.

Answer: (d). Constructors are never virtual and A::a() is also not virtual (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 the B’s vtable.
  • b) A::~A(): In order for B::~B() to function correctly, A::~A() also needs to be in B’s vtable.
  • c) B::g(): any virtual 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() is static, so it will never appear in the vtable. Since A::~A() is virtual, B’s is also by default and it will call A’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 from A, so it inherits A’s virtual implementation.
  • b) B::g(): any virtual 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 is static, by putting this method into B’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 overriden A::f(int).
  • b) C::f(int): C has overriden B::f(int).
  • c) B::f(int): B explicitly overrode A::f(int), but C has not explicitly overridden B::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) though B has marked this method as final, the override specifier overrules this and successfully allows C to override B::h(int).
  • b) C::h(int): This code does not compile because C has not explicitly added a virtual destructor.
  • c) B::h(int): (despite the compilation error) B has marked this method as final, 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 to final.

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 type banana, d has static type door, bref and dbref have static type banana&, and dref has static type door&. The dynamic type is the type a variable actually is at runtime. b and d are neither references or pointers. Hence, their dynamic type is always the same as their static type. Hence, b and d have dynamic types banana and door respectively. bref, dref, and dbref are references. Hence, their dynamic type is the dynamic type of the object they refer to. Hence, bref has dynamic type banana&, dref has dynamic type door&, and dbref has dynamic type door&.

2.Is there anything wrong with the assignment b = d?

  • a) Yes: we have not defined operator= for banana.
  • b) Yes: since b is a banana, assigning d to it will cause d’s door 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:

  1. int max(int, int)
  2. double max(double, double)
  3. std::string max(std::string, std::string)
  4. char max(char, char)
  5. 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:

  1. array<float, 3>
  2. array<float, 3>::array()
  3. array<float, 3>::data()
  4. void manipulate_array(const array<int, 3> arr)
  5. void manipulate_array(const array<float, 3> arr)
  6. 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 single T.
  • b) (2): This is the only specialisation where the first template parameter matches perfectly, and Ts... has lower priority than a single T.
  • 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 a const int * and matches (3)’s template parameter pefectly and T& has higher priority over const T and T.
  • b) (2): auto non-type template parameters are more flexible than other types and are preferred. Also, const T has higher preference over T& and T.
  • c) (1): 2 & 3 are ambiguous since const T is equally viable with T& 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 than const 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 a const 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 than const 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 a const char * and not a std::string &.
  • b) "hello" would bind to lvalue() if we appended the literal suffix s to it.
  • c) "hello" is implicitly converted to a std::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 from const char *, so this expression will implicitly create a temporary std::string. Temporaries cannot bind to lvalue references.

2.Why won’t rvalue(s0) compile?

  • a) s0 is a std::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 a std::string and values cannot bind to rvalue references.
  • c) We have to use to std::move() to convert s0 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)
    1. tv: const char *
    2. tv: const std::string &
    3. tv: std::string
  • b)
    1. tv: const char *
    2. tv: std::string &
    3. tv: std::string &&
  • c)
    1. tv: const char(&)[6]
    2. tv: const std::string &
    3. tv: std::string
  • d)
    1. tv: const char(&)[6]
    2. tv: std::string &
    3. tv: std::string &&

Answer: (d).

  1. 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.
  2. In function call expressions, variables are implicitly converted to lvalue references, and so tv is a std::string &.
  3. std::move()’s behaviour is to convert its argument into an rvalue reference, hence tv is a std::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 an int and when used in expressions its value is copied into a temporary of type int.
  • b) int &: j is an int& and (j) is equivalent to j in every context.
  • c) int &: j is an int& and a parenthesised expression of an int& is still an int&.
  • d) int &&: j is an int& and a parenthesised expression of an int& is an int&&.

Answer: (c). According to the rules for decltype, parenthesised variable expressions are by definitino lvalues, thus var is also an int&.

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, and std::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 to decltype (int & &&), which collapses to int&.

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 is int&&.

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 type int. The parentheses have no effect on literals.
  • b) int &: There is a special case with decltype 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 is int&.
  • d) (int&&) &: int literals have type int&& 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 another int.

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 of f is const int(&)[3]. This means that sizeof(f) will be unequal to sizeof(void*) and we enter the first if branch. Here, we are simply returning a reference we were passed, thus const int(&)[3].
  • b) const int *: The type of f is const int(&)[3], and foo returns a reference to an array from the val lvalue. Because var is declared as auto, the return array reference decays into a pointer inside of the main() function and so var is a const int *.
  • c) const int(&&)[3]: Because an lvalue to arr was passed to foo, f’s type is deduced to be const int (&&)[3]. We then copy this reference into val and return it from foo. This rvalue reference is finally stored into var, hence const int(&&)[3].
  • d) const int *: The type of f is const int(&)[3], and the assignment val = f causes f to decay to a pointer. We then simply return a pointer, which is copied into var.

Answer: (d). There is alot going on here:

  1. decltype(f) is const int(&)[3].
  2. Because auto never deduces references (or top-level const!), the assignment val = f will cause the array reference in f to decay to a const int *.
  3. We then copy by value the pointer out to var.
  4. Thus, var is a const int *.

08-05-2023 01:45-01