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
auto
type inferenceSometimes, 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());
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};
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;
}
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 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;
}
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;
}
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&) {}
}
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;
}
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.