License : Creative Commons Attribution 4.0 International (CC BY-NC-SA 4.0)
Copyright : Hervé Frezza-Buet, CentraleSupelec
Last modified : April 19, 2024 10:22
Link to the source : type.md

Table of contents

Typing in C++

The auto type inference

Sometimes, the type of an expression can be “guessed” by the compiler, since it checks types. In this case, the type can be replaced by auto.

std::vector<std::pair<std::list<std::string>, double>> v;

std::vector<std::pair<std::list<std::string>, double>>::iterator it1 = v.begin(); // Another type leads to compiling error...
auto                                                             it2 = v.begin(); // ... so the compiler knows which type has to be used.

const auto&        it3 = *(v.begin()->first.begin());
const std::string& it4 = *(v.begin()->first.begin());

Type aliases

Do not use old-fashioned typedef old_type newtype; anymore, the using syntax is more powerful.

using number = int;
template<typename CONTENT> using collection = std::vector<CONTENT>;

collection<number> l = {1,2,3,4};

Type casts

Casting is a way to construct (explicit keyword)

#include <iostream>
#include <string>

struct Status {
  bool activated;
  operator bool          () const {return activated;}
  
  Status()                : activated(false)  {}
  
  operator     std::string  () const {if(activated) return "On"; return "Off";}
  Status(const std::string& s) : activated(s == "On") {}
  
  explicit operator char () const {if(activated) return 'a'; return 'u';}
  explicit Status(  char c) : activated(c == 'a' || c == 'A') {}
};

void print_string(const std::string& s)     {}
void print_bool(bool b)                     {}
void print_char(const char c)               {}
void print_status(const Status& s)          {}

int main(int argc, char* argv[]) {
  double d = 3.2; 
  int    i = d;                           // ok, but not nice
  bool   b = 3;                           // ok, but not nice

  i = b; std::cout << i << std::endl;     // Output : 1 !!!
  i = (int)d;                             // Ok, old syntax.
  i = int(d);                             // Ok, new syntax.
  b = bool(i);                            // Ok, new syntax.

  Status status;
  if(status) std::cout << "status activated" << std::endl;
  print_bool(status);

  std::string s = "foo";
  // print_status("toto");
  print_status(s);
  print_string(status);
  Status S = s;
  Status T {s};
  
  char c = 'a';
  // print_status('a');                  // Compiling error
  // print_status(c);                    // Compiling error
  print_status(static_cast<Status>('a'));
  print_status(Status('a'));
  print_char(status);
  // Status U = 'a';                     // Compiling error
  Status V {'a'};
  
  return 0;
}

static_cast<>, dynamic_cast<> and reinterpret_cast<>

#include <iostream>
#include <array>

struct A {
  int a;
  virtual ~A() {} // Mandatory to have dynamic_cast compiling.
};

struct B : public A {
  int  b;
  virtual ~B() {} // Mandatory to have dynamic_cast compiling.
};
struct C : public A {
  std::array<int, 10> c;
  virtual ~C() {} // Mandatory to have dynamic_cast compiling.
};
struct D {int d;};

int main(int argc, char* argv[]) {
  A* a = new A();
  B* b = new B();
  C* c = new C();

  A* a_ptr1 = b; 
  A* a_ptr2 = c;

  // Errors
  
  // B* b_ptr1 = a;                              // Compiling error, thanks.
  B* b_ptr1 = reinterpret_cast<B*>(a);           // b_ptr1->b is not allocated.
  // B* b_ptr2 = c;                              // Compiling error, thanks.
  B* b_ptr2 = reinterpret_cast<B*>(c);           // b_ptr1->b is allocated, it is c->c[0]...

  // Correct downcast
  
  // B* b_ptr3 = a_ptr1;                         // Compiling error, downcast detected.
  B* b_ptr3 = static_cast<B*>(a_ptr1);           // Right, a downcast may be done, since a_ptr1 points to a B.
  // B* b_ptr4 = a_ptr2;                         // Compiling error, downcast detected.
  B* b_ptr4 = static_cast<B*>(a_ptr2);           // Right, a downcast may be done, since a_ptr1 could points to a C...
  //                                                ... but here, this is an error. b_ptr4->b is allocated, it is c->c[0].
  
  B* b_ptr5 = dynamic_cast<B*>(a_ptr1);
  if(b_ptr5) {/* ok, a_ptr1 was indeed a b. */}  // Dynamic casts checks downcast at execution.
  C* c_ptr1 = dynamic_cast<C*>(a_ptr2);
  if(c_ptr1) {/* ok, a_ptr2 was indeed a c. */}  // Dynamic casts checks downcast at execution.

  // D* d_ptr  = dynamic_cast<C*>(a_ptr2);       // Compiling error, incompatible types.

  return 0;
}

const_cast<>

struct A {int a;};

void strange_set(const A& arg, int value) {
  // arg.a = value;   // Compiling error.
  const_cast<A&>(arg).a = value; 
}

int main(int argc, char* argv[]) {
  A a;
  strange_set(a, 10);
  return 0;
}

Cast of smart pointers

Smart pointers are small classes that handle a native pointer, but behave as a pointer. In order to apply to them the cast that we expect from native pointers, cast functions are implemented in the STL. These are functions, not C++ keywords, so there is a std:: prefix for them, as oppose to native cast operators.

#include <memory>
#include <iostream>
#include <iomanip>

struct Vehicle {
  int nb_seats = 0;
  virtual ~Vehicle(){}                                                // Virtual destructor is mandatory for dynamic casts.
}; 
struct Car  : public Vehicle{};                                       // Virtual destructor is omitted, but inherited.
struct Boat : public Vehicle{};                                       // Virtual destructor is omitted, but inherited.

int main(int argc, char* argv[]) {
  auto car_ptr     = std::make_shared<Car>();
  auto boat_ptr    = std::make_shared<Boat>();
  auto vehicle_ptr = std::make_shared<Vehicle>();
  
  vehicle_ptr = car_ptr;                                              // No error
  std::cout << "Car count = "
	    << car_ptr.use_count()
	    << std::endl;                                             // Displays 2
  
  // boat_ptr = car_ptr;                                              // Compiling error about operator=
  // boat_ptr = vehicle_ptr;                                          // Compiling error about operator=
  boat_ptr = std::static_pointer_cast<Boat>(vehicle_ptr);             // Compiles, but the cast is an illicite downcast
  std::cout << "Car count = "
	    << car_ptr.use_count()
	    << std::endl;                                             // Displays 3
  
  boat_ptr = std::dynamic_pointer_cast<Boat>(vehicle_ptr); 
  car_ptr  = std::dynamic_pointer_cast<Car> (vehicle_ptr);
  std::cout << std::boolalpha
	    << bool(boat_ptr)
	    << ", "
	    << bool(car_ptr)
	    << std::endl;                                             // Displays false, true

  auto car_const_ptr   = std::make_shared<const Car>();
  auto car_deconst_ptr = std::const_pointer_cast<Car>(car_const_ptr);
  // car_const_ptr->nb_seats++;                                       // Compiling error about read-only.
  car_deconst_ptr->nb_seats++;                                        // No error

  auto int_ptr = std::make_shared<int>(3);
  // car_ptr = int_ptr;                                               // Compiling error about operator=
  car_ptr = std::reinterpret_pointer_cast<Car>(int_ptr);              // Compiles, but the cast is an illicite cast.
  return 0;
}

Enumerated types

Enumerated types are strongly typed since C++11, avoid old-fashioned enum T {...} and use enum class ... declarations.

#include <iostream>
#include <cstdint>

enum class Religion : char {None = 'n', Buddhism = 'b', Christianity = 'c', Hinduism = 'h', Islam = 'i', Judaism = 'j', Other = 'o'};

struct Calendar {
  enum class Day   : std::uint8_t {None = 0, Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday};
  enum class Month : std::uint8_t {None = 0, January, February, March, April, May,June, July, August, October, November, December};

  struct Date {
    Day   day;
    Month month;
    int   number;
    int   year;
    Date() : day(Day::None), month(Month::None), number(0), year(0) {}
    Date(const Date&) = default;
    Date& operator=(const Date&) = default;
  };
};

void f(Religion        r) {std::cout << "f(Religion) called." << std::endl;}
void f(Calendar::Day   d) {std::cout << "f(Day) called."      << std::endl;}
void f(Calendar::Month m) {std::cout << "f(Month) called."    << std::endl;}
void f(char            c) {std::cout << "f(char) called."      << std::endl;}

int main(int argc, char* argv[]) {
  f(Religion::None);                                                  // Output :  f(Religion) called.
  f(Calendar::Day::None);                                             // Output : f(Day) called.
  f(Calendar::Month::None);                                           // Output : f(Month) called.
  // Calendar::Month m = Calendar::Day::Monday;                       // Compiling error
  // std::uint8_t    i = Calendar::Day::Monday;                       // Compiling error
  std::uint8_t i = static_cast<std::uint8_t>(Calendar::Day::Monday); 

  return 0;
}

Optional values (std::optional<T>)

Sometimes, you may handled values of type T that may exist or not. Using type T is unappropriate in this case (usually, people consider that a specific value of type T means nothing, by convention). The std::optional template gives a string typing to this situation.

#include <optional>
#include <iostream>

int main() {
  std::optional<int> x(10);                     // x = 10
  std::optional<int> y;                         // y = <nothing>
  auto               z = std::make_optional(3); // z = 3

  if(x)
    std::cout << "x has the value " << *x << std::endl;
  else
    std::cout << "x has no value";

  x = y;            // x has no value now.
  x = 5;            // x is 5 now.
  x = z;            // x is 3 now.
  z = std::nullopt; // z has no value now.
}

The following shows this in action.

#include <optional>
#include <iostream>

struct Point   {double x=0; double y=0;};

struct Segment {
  Point A;
  Point B;
  Segment(const Point& A, const Point& B) : A(A), B(B) {}
};

auto operator&(const Segment& s1, const Segment& s2) {
  std::optional<Point> intersection;
  // fake math here....
  if(s1.A.x < s2.B.y) intersection = {s1.B.x, s2.A.y}; 
  return intersection;
}

int main(int argc, char* argv[]) {
  Point A = {2.3, 4.8};
  Point B = {1.0, 1.2};
  Point C = {5.5, 4.1};
  Point D = {0.0, 0.5};

  if(auto oI = Segment(A,B) & Segment(C,D); oI) { // if(def-init; test)
    auto& I = *oI;
    std::cout << "Intersection at " << I.x << ',' << I.y << std::endl;
  }
  else
    std::cout << "No intersection" << std::endl;
    
  return 0;
}

  

Values of any type ?!?! (std::any)

Having any type can be typed. The type std::any holds a pointer to some value in the heap, and it handles the typing at execution type.

#include <any>
#include <list>
#include <array>
#include <string>
#include <utility>
#include <iostream>

double      f() {return 3.14;}
std::string g() {return "foo";}

int main() {
  std::list<int>          numbers = {1, 2, 3, 4, 5};
  std::array<std::any, 5> misc_values;
  
  misc_values[0] = f();
  misc_values[1] = g();
  misc_values[2] = std::make_pair(3,std::string("trois"));
  misc_values[3] = 0;
  misc_values[4] = numbers;

  auto value = std::make_any<double>(12.5);
  std::cout << misc_values[2].type().name() << std::endl; // Output : St4pairIiNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE
  try {
    // auto name = std::any_cast<std::string>(value); // throws exception
    value = misc_values[1];
    std::string name = std::any_cast<std::string>(value);
    
    value = misc_values[2];
    if(value.type() == typeid(std::pair<int,std::string>))
      name = std::any_cast<std::pair<int,std::string>>(value).second;
  }
  catch(const std::bad_any_cast&) {}
}

Unions (std::variant<...>)

Some values may be either from type A, either from type B. As opposed to std::any, if all the cases can be done at compiling time, the sizeof of an union is the max of the sizeof of the possibilities. Thus, it can be allocated statically, in the stack for example.

#include <variant>
#include <iostream>
#include <array>

struct ButtonClick {
  int x, y, button;
  ButtonClick(int x, int y, int button) : x(x), y(y), button(button) {}
  ButtonClick() = delete;  // Not default constructible.
};

struct Expose {
  int x, y, width, height;
  Expose(int x, int y, int width, int height) : x(x), y(y), width(width), height(height) {}
  Expose() = delete;      // Not default constructible.
};

// Variants need one of the types to be default-constructible. The
// fake std::monostate placeholder can be used if none of the type
// is default constructible.
using Event = std::variant<std::monostate, ButtonClick, Expose>;

int main() {
  Event e1 = ButtonClick(10, 50, 3);
  Event e2 = Expose(0, 0, 640, 480);
  // e1 and e2 have the same type. They can be put in an array for example.
  std::array<Event, 2> evts = {e1, e2};

  std::cout << std::boolalpha << std::endl
	    << std::holds_alternative<ButtonClick>(e1) << ' ' << std::holds_alternative<Expose>(e1) << std::endl // Output : true false
	    << std::holds_alternative<ButtonClick>(e2) << ' ' << std::holds_alternative<Expose>(e2) << std::endl // Output : false true
	    << std::endl;

  try {
    auto button_click = std::get<ButtonClick>(e1);
    // auto expose    = std::get<Expose>(e1); // throws the exception
    e1 = e2;
    auto expose    = std::get<Expose>(e1);
  }
  catch (std::bad_variant_access&) {}
  return 0;
}

Litterals

This generalizes the notation 123L for typing 123 as a long int.

The idea is to have a type that supports litteral. For examle, th system library chrono allows this writing.

#include <chrono>
std::chrono::milliseconds millis = 1ms;
millis = 1h + 2min - 1s;    
millis = 2 * 1h + 3min / 2; 

Here, ms, min, h are litterals, thay can be user defined (our litterals have to start with _, the others are reserved for the standard).

Let us make this piece of code work:

#include <iostream>
#include "homogeneity.hpp" // Home made !

int main(int argc, char* argv[]) {
  Value mass  = 2.5_kg + 3_g;
  Value dist  = 3_m;
  Value speed = dist*2_Hz;
  Value force = 4_N;
  Value work  = force*dist;
  Value power = work/10_s;

  std::cout << "mass  = " << mass  << std::endl  // Output : mass  = 2.503kg.
  	    << "dist  = " << dist  << std::endl  // Output : dist  = 3m.
  	    << "speed = " << speed << std::endl  // Output : speed = 6m./s.
  	    << "force = " << force << std::endl  // Output : force = 4m.kg./s^2.
  	    << "work  = " << work  << std::endl  // Output : work  = 12m^2.kg./s^2.
  	    << "power = " << power << std::endl; // Output : power = 1.2m^2.kg./s^3.
  return 0;
}

The file homogeneity.hpp that we have designed implements the use of litterals. You need to know the private keword, as well as Operator overloading to understand it.

Hervé Frezza-Buet,