Main

Week 8 (cont.)


Type conversions

In C++, implicit conversions are performed automatically when a value of one type is copied to a value of another type, e.g.,

short s = 2000;
int i; 
i = s;

The value of s is promoted from short (usually 2 bytes) to int (usually 4 bytes) without having to explicitly convert the type.

Converting from a smaller (e.g., float) to larger (e.g., double) type is called promotion and is performed automatically and will always preserve the value being assigned exactly. Other types of conversions (e.g., from larger to smaller types) may not preserve the value being assigned. In some cases the compiler will issue a warning, and you should fix it to prevent incorrect results being computed.

  • If a negative integer value is converted to an unsigned type, the resulting value corresponds to its 2's complement bitwise representation (i.e., -1 becomes the largest value representable by the type, -2 the second largest, ...).
  • The conversions from/to bool consider false equivalent to zero (for numeric types) and to null pointer (for pointer types); true is equivalent to all other values and is converted to the equivalent of 1.
  • If the conversion is from a floating-point type to an integer type, the value is truncated (the decimal part is removed). If the result lies outside the range of representable values by the type, the conversion causes undefined behavior.
  • Otherwise, if the conversion is between numeric types of the same kind (integer-to-integer or floating-to-floating), the conversion is valid, but the value is implementation-specific (and may not be portable).

Non-fundamental types, arrays, and functions implicitly convert to pointers, and pointers in general allow the following conversions:

  • Null pointers can be converted to pointers of any type
  • Pointers to any type can be converted to void pointers.
  • Pointer upcast: pointers to a derived class can be converted to a pointer of an accessible and unambiguous base class, without modifying its const or volatile qualification.

Implicit conversions with classes

  1. // implicit conversion of classes:
  2. #include <iostream>
  3. #include <typeinfo> // for typeid
  4. using namespace std;
  5.  
  6. class A {};
  7.  
  8. class  B {
  9. public:
  10.   // conversion from A (constructor):
  11.   B (const A& x) {
  12.      cout << "Constructor: Constructing a B from an A\n";
  13.   }
  14.  
  15.   // conversion from A (assignment):
  16.   B& operator= (const A& x) {
  17.      cout << "Assignment: Assigning an A to a B\n";
  18.      return *this; }
  19.  
  20.   // conversion to A (type-cast operator)
  21.   operator A() {
  22.      cout << "Type-cast: Converting to an A (type-cast operator)\n";
  23.      return A();
  24.   }
  25. };
  26.  
  27. int main ()
  28. {
  29.   A foo;
  30.   cout << "Type of foo: " << typeid(foo).name() << endl;
  31.  
  32.   B bar = foo;   // calls constructor
  33.   cout << "Type of bar: " << typeid(bar).name() << endl;
  34.  
  35.   bar = foo;      // calls assignment
  36.   cout << "Type of bar after assignment bar = foo: " << typeid(bar).name() << endl;
  37.  
  38.   foo = bar;      // calls type-cast operator
  39.   cout << "Type of foo after assignment foo = bar: " << typeid(foo).name() << endl;
  40.  
  41.   return 0;
  42. }

The explicit keyword

C++ allows one implicit conversion to happen for each argument in a function call.

What happens when we add the function

 
void fn (B arg) {} 

to class B?

It takes an argument of type B, but we can also call it with an object of type A, e.g., fn(foo);

If you want to prevent this, then change the constructor for B slightly to

explicit B (const A& x) {}

Adding the explicit keyword ensures that no implicit conversions will be done. It also prevents you from doing assignments such as B bar = foo; (which require implicit conversions). You can also use explicit with the typecast member function, e.g.,

explicit operator A() { return A(); }

Type casting

In C++, you can convert types explicitly using both C syntax (e.g., (int) someVar) and C++ function syntax (more on that below).

When explicitly converting class types, avoid using C syntax because even if the code compiles, the result may be incorrect and cause very hard to debug runtime errors.

C++ offers four casting operators:

  • const_cast <new_type> (expression)
  • static_cast <new_type> (expression)
  • dynamic_cast <new_type> (expression)
  • reinterpret_cast <new_type> (expression) (this one is dangerous!)

const_cast

You can use the const_cast to change the constness of a pointer. For example,

  1. void foo(string &s) { cout << s << endl;}
  2. int main() {
  3.   const string str = "Some text";
  4.   foo(str);
  5.   return 0;
  6. }

This is fine because foo does not actually modify s.

Important: Removing the constness of a pointer to an object to actually write to the object causes undefined behavior.

static_cast

static_cast can perform conversions between pointers to related classes, not only upcasts (from pointer-to-derived to pointer-to-base), but also downcasts (from pointer-to-base to pointer-to-derived). The name "static" implies that it's performed at compile time -- no checks are performed during runtime to guarantee that the object being converted is in fact a full object of the destination type. Therefore, it is up to the programmer to ensure that the conversion is safe.

  1. class Base {};
  2. class Derived: public Base {};
  3. int main() {
  4.   Base * a = new Base;
  5.   Derived * b = static_cast<Derived*>(a);
  6.   return 0;
  7. }

This would be valid code, although b would point to an incomplete object of the class and could lead to runtime errors if dereferenced.

With static_cast, you can

  • Convert from void* to any pointer type. In this case, it guarantees that if the void* value was obtained by converting from that same pointer type, the resulting pointer value is the same.
  • Convert integers, floating-point values and enum types to enum types.
  • Explicitly call a single-argument constructor or a conversion operator.
  • Convert to rvalue references.
  • Convert enum class values into integers or floating-point values.
  • Convert any type to void, evaluating and discarding the value.

dynamic_cast

dynamic_cast can only be used with pointers and references to classes (or with void*). Its purpose is to ensure that the result of the type conversion points to a valid complete object of the destination pointer type. You can use dynamic_cast both for upcasts and downcasts.

  1. #include <iostream>
  2. using namespace std;
  3.  
  4. class Cat {
  5. public:
  6.   virtual void meow() { std::cout << "Meowing like a regular cat! meow!\n"; }
  7. };
  8.  
  9. class WildCat : public Cat {
  10. public:
  11.   virtual void meow() { std::cout << "Meowing like a wild cat! MRREOWW!\n"; }
  12. };
  13.  
  14. int main() {
  15.   try {
  16.     Cat * Crazy = new WildCat; // Derived class
  17.     Cat * Fluffy = new Cat;  // Base class
  18.  
  19.     WildCat * catPtr;
  20.  
  21.     catPtr = dynamic_cast<WildCat*>(Crazy);
  22.     if (catPtr == 0) cout << "Null ptr on first type cast.\n";
  23.     else catPtr->meow();
  24.  
  25.     catPtr = dynamic_cast<WildCat*>(Fluffy);
  26.     if (catPtr == 0) cout << "Null ptr on second type cast.\n";
  27.     else catPtr->meow();
  28.  
  29.   } catch (exception &e) {
  30.     cout << "Exception occurred: " << e.what() << endl;
  31.   }
  32.   return 0;
  33. }

Even though both pointers are of the base type Cat *, Crazy actually points to an object of type WildCat. Hence, when we perform the dynamic_cast to WildCat on catPtr, it will point to a full object of type WildCat. The Fluffy pointer, on the other hand, points to the base class Cat, which is an incomplete object of class WildCat (missing the attitude data member).

When dynamic_cast cannot cast a pointer because it is not a complete object of the required class (as in the second conversion in the previous example), it returns a nullptr to indicate the failure. If dynamic_cast is used to convert to a reference type and the conversion is not possible, an exception of type bad_cast is thrown instead.

dynamic_cast can also perform the other implicit casts allowed on pointers: casting null pointers between pointers types (even between unrelated classes), and casting any pointer of any type to a void* pointer.

reinterpret_cast

reinterpret_cast converts any pointer type to any other pointer type, even of unrelated classes. The operation result is a simple binary copy of the value from one pointer to the other. All pointer conversions are allowed: neither the content pointed nor the pointer type itself is checked. This is very very dangerous, use only if there is absolutely no other alternative.

Green Marinee theme adapted by David Gilbert, powered by PmWiki