Skip to content

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
Another motivation is to avoid writing a long, difficult-to-remember type that the compiler already recognises, but which a programmer could get wrong.
std::vector<DataRecord> v;
auto p = v.begin();         // vector<DataRecord>::iterator
There are other reasons for using 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
Using either notation for primitive types makes no difference. However, it is important to understand that when the equals sign (=) 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=
Using =, () 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!
}
On the other hand, using = 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!
As you can see, only {} 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;
};
determine if the following program compiles. If not, fix the error and determine the program’s behavior. If the program compiles, determine whether it does really what is expected. If it is not the case, fix the 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)()
One can then define a function with the same signature and pass it as callback to another function that takes a pointer to function as argument.

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*)
For avoiding redefining pointer to functions for each specific case, arguments are often defined as 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);
}
We may then defined callback functions that are strongly typed:
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);
}
Based on these type and callback definitions, rewrite the main() function. Find a case that does not compile when mixing types.