Conversions in C++
Exercice 1: Implicit Conversions
Given the code below, determine the value of ui32 at the end of the program.
uint32_t ui32 = std::numeric_limits<uint32_t>::max();
float f = ui32;
ui32 = f;
Exercice 2: Implicit Conversions
Given the code below, determine the values of ui32 and i16 at the end of the program.
int32_t i32 = std::numeric_limits<int32_t>::min();
ui32 = i32;
int16_t i16 = i32;
Exercice 3: c-style vs C++-style Conversions
Given the code below, determine which statements compile and which deliver unexpected behaviors.
uint32_t ui32 = 0;
float f = ui32;
int32_t i32 = std::numeric_limits<int32_t>::min();
uint32_t *pui32 = (uint32_t*) &f;
uint32_t fAsUint32 = *pui32;
pui32 = static_cast<uint32_t*>(&f);
fAsUint32 = static_cast<uint32_t>(*pui32);
Exercice 4: c-style vs C++-style Conversions
Given the program below, determine which statements compile and which deliver undefined behaviors.
class Base {
private:
int32_t _d1 = 0;
public:
virtual void m1(int32_t i) { _d1 -= i; }
};
class Derived : public Base {
private:
int32_t _d2 = 1;
public:
void m1(int32_t i) override { _d2 += i; }
void m2(int32_t i) { _d2 -= i; }
};
class Unrelated {
private:
int16_t _d3 = 1;
public:
void m() { _d3++; }
};
int main() {
Base b;
Derived d;
Unrelated u;
// c-style casts
Base *pb = (Base *) &d;
pb->m1(1);
Derived *pd = (Derived *) pb;
pd->m1(1);
pb = &b;
pd = (Derived *) pb;
pd->m1(1);
pd->m2(1);
Unrelated *pu = (Unrelated *) &b;
pu->m();
// static_cast
pb = static_cast<Base *>(&d);
pd = static_cast<Derived *>(pb);
pb = &b;
pd = static_cast<Derived *>(pb);
pd->m2(1);
pu = static_cast<Unrelated*>(&b);
// dynamic_cast
pb = dynamic_cast<Base *>(&d);
pd = dynamic_cast<Derived *>(pb);
pd->m1(1);
pb = &b;
pd = dynamic_cast<Derived *>(pb);
pu = dynamic_cast<Unrelated*>(&b);
if (pu != nullptr) {
pu->m();
}
}
Types and variable declarations is C++
Exercice 5: Prefer auto to explicit type declarations
The main reason is rather simple: auto variables have their type
deduced by the compiler from their initializer. So they must be initialized.
int x; // potentially unitialized
auto y; // compiler error -> initialization is required
auto z = 0; // z's value is well defined and the compiler can deduce z's type
std::vector<DataRecord> v;
auto p = v.begin(); // vector<DataRecord>::iterator
auto with template classes and with std::function objects that are not covered here.
Exercice 6: Use constexpr whenever possible
constexpr indicates a value that is not only constant, but also known during compilation.
Values known during compilation are privileged especially on embedded systems, since they may be placed in read-only memory. C++ also requires integral constant expressions in contexts like specification of array sizes, integral template arguments, or enumerator values. In this case, it is useful to declare values using constexpr. This should in any case be preferred to the use of macros or literal values:
std::array<int, 10> data1; // ok but bad practice
#define SIZE 10
std::array<int, SIZE> data1; // ok but bad practice
int size3; // non-constexpr variable
constexpr auto arraySize3 = size3; // error! size3's value not known at compilation
std::array<int, size3> data3; // error! sz's value not known at compilation
constexpr auto arraySize4 = 10; // ok, 10 is a compile-time constant
std::array<int, arraySize4> data4; // ok, arraySize4 is constexpr
Exercice 7: Make the difference between copy contructor and assignment, and between () and {} when creating object
In C++, variables may be initialized with parentheses, an equals sign, or braces:
int x(0); // initializer is in parentheses
int y = 0; // initializer follows "="
int z{ 0 }; // initializer is in braces
=) is
used for initialisation, no assignment takes place;
instead, the copy constructor is invoked.
MyClass o1; // call default constructor
MyClass o2 = o1; // not an assignment; calls copy constructor
o1 = o2; // an assignment; calls assignment operator=
=, () or {} when initializign instances is not always possible.
For instance, using () when initializing non-static data members of a class is not possible:
class MyClass {
...
private:
int x{ 0 }; // fine, x's initial value is 0
int y = 0; // also fine
int z(0); // error!
}
= for non-copyable objects is not possible:
class MyClass {
...
explicit MyClass(int i);
MyClass(const MyClass&) = delete;
MyClass& operator=(const MyClass&) = delete;
}
MyClass o1{ 0 }; // fine
MyClass o2(0); // fine
MyClass o3 = 0; // error!
{} can be used anywhere for initializing objects.
It is important to point out that when using {}, implicit narrowing conversions
among primitive types are prohibited:
double x, y, z;
int sum1{ x + y + z }; // error! converting double to int is a narrowing conversion
int sum2( x + y + z ); // allowed!
int sum3 = x + y + z; // allowed!
However, there are cases when using {} or () for initializing objects have different behaviors.
This relates to constructors with std::initializer_lists and constructor overload resolution. This topic is not covered here.
Classes in C++
Exercice 8: User-Defined Conversions
Given the following String class:
class String {
public:
// constructors
String(const char* szArray) {
this->szArray = new char[strlen(szArray)];
strcpy(this->szArray, szArray);
}
String(int size) {
szArray = new char[size]{0};
}
// destructor
virtual ~String() {
if (this->szArray != nullptr) {
delete[] this->szArray;
this->szArray = nullptr;
}
}
private:
char* szArray = nullptr;
};
String class and modify
the program accordingly.
int main() {
String* s1 = new String("s1");
String s2("s2");
String* s3 = new String(10);
String s4 = 10;
String s5 = '1234';
return 0;
}
Exercice 9: Constructors, Assignments, and Destructors
The constructors, assignment operators and destructors control the lifecycle of objects: creation, copy, move, and destruction. They must therefore be defined carefully.
For each C++ class X, these are default operations:
- a default constructor: X()
- a copy constructor: X(const X&)
- a copy assignment: operator=(const X&)
- a move constructor: X(X&&)
- a move assignment: operator=(X&&)
- a destructor: ~X()
By default, the compiler defines each of these operations if it is used, but the default can be suppressed or overriden.
The String class from the previous exercise defines two constructors and the destructor. This means that the compiler will not provide a default constructor or destructor. The other functions are defined by default.
To understand whether the class is well defined, analyze the behavior of the following program:
int main() {
String s1("s1");
{
String s2 = s1;
}
return 0;
}
Exercice 10: Constructors, Assignments, and Destructors
Given the modified String class from the previous exercise, analyze whether the code below
produces the expected result.
int main() {
String s1("s1");
{
String s2("");
s2 = s1;
}
return 0;
}
Exercice 11: Constructors, Assignments, and Destructors
Imagine that you want to implement a different behavior when copying and assigning String instances, and that you want to move the ownership of the character array to the other instance rather than copying it.
Describe how you would modify the String class and the main() program for the previous two exercises.
Hint: You need to remove the definitions of the copy constructor and assignment operator because they implement a different behavior. Then, define a move constructor and a move assignment operator. This will implicitly define the copy constructor and assignment operator as deleted and the main() program will not compile anymore. You will then need to modify the main() program accordingly.
Callbacks in C/C++
Exercice 12: Callbacks in C
In C, callbacks are usually implemented as pointers to functions. A pointer to a function is typically defined as
typedef void (*fun_ptr)()
With this approach, a different signature should be defined for each type of callback. If we need to define a function that returns an int and takes an array of int as parameter, then the following pointer to function should be defined
typedef int (*fun_ptr)(int*)
void*. This allows to define more generic callback functions, at the price of removing types and function specific type casting. The compiler will not detect any error made by the programmer in type mismatching.
Find the error in the following program. Note that it compiles without any warning/error.
void func_int(void* arg1, void* arg2) {
int i1 = (int) *((int*) arg1);
int i2 = (int) *((int*) arg2);
printf("func_int is called with parameters %d %d\n", i1, i2);
}
void func_double(void* arg1, void* arg2) {
double d1 = (double) *((double*) arg1);
double d2 = (double) *((double*) arg2);
printf("func_double is called with parameters %f %f\n", d1, d2);
}
struct Rect {
int x;
int y;
};
void func_rect(void* arg1, void* arg2) {
Rect r1 = (Rect) *((Rect*) arg1);
Rect r2 = (Rect) *((Rect*) arg2);
printf("func_rect_void is called with parameters (%d, %d) - (%d %d)\n",
r1.x, r1.y, r2.x, r2.y);
}
typedef void (func_ptr)(void*, void*);
void func_with_callback(func_ptr fun, void* arg1, void* arg2) {
fun(arg1, arg2);
}
int main() {
int arg1 = 1;
int arg2 = 2;
func_with_callback(func_int, (void*) &arg1, (void*) &arg2);
double darg1 = 1.5;
double darg2 = 2.5;
func_with_callback(func_double, (void*) &arg1, (void*) &darg2);
Rect rarg1 = {1, 1};
Rect rarg2 = {2, 2};
func_with_callback(func_rect, (void*) &rarg1, (void*) &arg2);
return 0;
}
Exercice 13: Callbacks in C++
C++ offers mechanisms for more robust callback typing. The func_ptr type defined
in the example above and the callback mechanism can be transformed to:
template<typename T, typename U>
using Callback = std::function<void(T, U)>;
template <typename T, typename U>
void func_with_callback(Callback<T,U> cb, T t, U u) {
cb(t, u);
}
void callback_int(int arg1, int arg2) {
printf("func_int is called with parameters %d %d\n", arg1, arg2);
}
void callback_double(double arg1, double arg2) {
printf("func_double is called with parameters %f %f\n", arg1, arg2);
}
struct Rect {
int x;
int y;
};
void callback_rect(Rect r1, Rect r2) {
printf("func_rect is called with parameters (%d, %d) - (%d %d)\n",
r1.x, r1.y, r2.x, r2.y);
}
main() function.
Find a case that does not compile when mixing types.