-
- Downloads
#101 Update metadata for all operator notebooks.
parent
4f7e6f25
Branches
Tags
Showing
- 3-Operators/0-main.ipynb 5 additions, 6 deletions3-Operators/0-main.ipynb
- 3-Operators/1-Intro.ipynb 5 additions, 6 deletions3-Operators/1-Intro.ipynb
- 3-Operators/1b-hands-on.ipynb 5 additions, 6 deletions3-Operators/1b-hands-on.ipynb
- 3-Operators/2-Comparison.ipynb 5 additions, 6 deletions3-Operators/2-Comparison.ipynb
- 3-Operators/3-Stream.ipynb 5 additions, 6 deletions3-Operators/3-Stream.ipynb
- 3-Operators/3b-hands-on.ipynb 5 additions, 6 deletions3-Operators/3b-hands-on.ipynb
- 3-Operators/4-CanonicalForm.ipynb 5 additions, 6 deletions3-Operators/4-CanonicalForm.ipynb
- 3-Operators/5-Functors.ipynb 5 additions, 6 deletions3-Operators/5-Functors.ipynb
- 3-Operators/5b-hands-on.ipynb 5 additions, 6 deletions3-Operators/5b-hands-on.ipynb
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
# [Getting started in C++](./) - [Operators](./0-main.ipynb) - [Introduction](./1-Intro.ipynb) | # [Getting started in C++](./) - [Operators](./0-main.ipynb) - [Introduction](./1-Intro.ipynb) | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
## Motivation | ## Motivation | ||
We've seen at length in the object programming part that classes are basically new types defined by the developer. However sometimes we would like to use exactly the same syntax as for the base type. Let's see for instance a basic class to handle tri-dimensional vectors: | We've seen at length in the object programming part that classes are basically new types defined by the developer. However sometimes we would like to use exactly the same syntax as for the base type. Let's see for instance a basic class to handle tri-dimensional vectors: | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
class Vector | class Vector | ||
{ | { | ||
// Friendship because `Add()` needs to access private members and no accessors were defined. | // Friendship because `Add()` needs to access private members and no accessors were defined. | ||
friend Vector Add(const Vector& v1, const Vector& v2); | friend Vector Add(const Vector& v1, const Vector& v2); | ||
public : | public : | ||
Vector(double x, double y, double z); | Vector(double x, double y, double z); | ||
Vector() = default; | Vector() = default; | ||
void Print() const; | void Print() const; | ||
private : | private : | ||
double x_ = 0.; | double x_ = 0.; | ||
double y_ = 0.; | double y_ = 0.; | ||
double z_ = 0.; | double z_ = 0.; | ||
}; | }; | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
Vector::Vector(double x, double y, double z) | Vector::Vector(double x, double y, double z) | ||
: x_(x), | : x_(x), | ||
y_(y), | y_(y), | ||
z_(z) | z_(z) | ||
{ } | { } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
Vector Add(const Vector& v1, const Vector& v2) | Vector Add(const Vector& v1, const Vector& v2) | ||
{ | { | ||
Vector ret; | Vector ret; | ||
ret.x_ = v1.x_ + v2.x_; | ret.x_ = v1.x_ + v2.x_; | ||
ret.y_ = v1.y_ + v2.y_; | ret.y_ = v1.y_ + v2.y_; | ||
ret.z_ = v1.z_ + v2.z_; | ret.z_ = v1.z_ + v2.z_; | ||
return ret; | return ret; | ||
} | } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
#include <iostream> | #include <iostream> | ||
void Vector::Print() const | void Vector::Print() const | ||
{ | { | ||
std::cout << "(" << x_ << ", " << y_ << ", " << z_ << ")" << std::endl; | std::cout << "(" << x_ << ", " << y_ << ", " << z_ << ")" << std::endl; | ||
} | } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
{ | { | ||
Vector v1(3., 5., 7.); | Vector v1(3., 5., 7.); | ||
Vector v2(7., 5., 3.); | Vector v2(7., 5., 3.); | ||
Vector v3 = Add(v1, v2); | Vector v3 = Add(v1, v2); | ||
v3.Print(); | v3.Print(); | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
Now the same with a _plain old data type_ is much more natural to write with no (apparent) method: | Now the same with a _plain old data type_ is much more natural to write with no (apparent) method: | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
{ | { | ||
double x1 = 3.; | double x1 = 3.; | ||
double x2 = 7.; | double x2 = 7.; | ||
double x3 = x1 + x2; | double x3 = x1 + x2; | ||
std::cout << x3 << std::endl; | std::cout << x3 << std::endl; | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
C++ provides the way to mimic this behaviour with **operator overloading**. This is a very powerful conceit, but also one that should be approached with some care... | C++ provides the way to mimic this behaviour with **operator overloading**. This is a very powerful conceit, but also one that should be approached with some care... | ||
We will see the general way to define such an operator in this notebook and see in dedicated notebooks which are the ones specifically useful. | We will see the general way to define such an operator in this notebook and see in dedicated notebooks which are the ones specifically useful. | ||
## Overloading an operator | ## Overloading an operator | ||
To overload an operator, the syntax is just the keyword **operator** followed by the operator to overload. In the following we will just replace the `Add` method by `operator+`. | To overload an operator, the syntax is just the keyword **operator** followed by the operator to overload. In the following we will just replace the `Add` method by `operator+`. | ||
The following code illustrate how to do so... but unfortunately doesn't run with Xeus Cling (you may play with it [@Coliru](https://coliru.stacked-crooked.com/a/765b9dc1e2b73c71)). | The following code illustrate how to do so... but unfortunately doesn't run with Xeus Cling (you may play with it [@Coliru](https://coliru.stacked-crooked.com/a/765b9dc1e2b73c71)). | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
// DOESN'T RUN WITH XEUS-CLING | // DOESN'T RUN WITH XEUS-CLING | ||
#include <iostream> | #include <iostream> | ||
class VectorPlus | class VectorPlus | ||
{ | { | ||
public : | public : | ||
VectorPlus(double x, double y, double z); | VectorPlus(double x, double y, double z); | ||
VectorPlus() = default; | VectorPlus() = default; | ||
void Print() const; | void Print() const; | ||
// Friendship as free function operator+ wouldn't otherwise be allowed to access data attributes. | // Friendship as free function operator+ wouldn't otherwise be allowed to access data attributes. | ||
friend VectorPlus operator+(const VectorPlus& v1, const VectorPlus& v2); | friend VectorPlus operator+(const VectorPlus& v1, const VectorPlus& v2); | ||
private : | private : | ||
double x_ = 0.; | double x_ = 0.; | ||
double y_ = 0.; | double y_ = 0.; | ||
double z_ = 0.; | double z_ = 0.; | ||
}; | }; | ||
VectorPlus::VectorPlus(double x, double y, double z) | VectorPlus::VectorPlus(double x, double y, double z) | ||
: x_(x), | : x_(x), | ||
y_(y), | y_(y), | ||
z_(z) | z_(z) | ||
{ } | { } | ||
void VectorPlus::Print() const | void VectorPlus::Print() const | ||
{ | { | ||
std::cout << "(" << x_ << ", " << y_ << ", " << z_ << ")" << std::endl; | std::cout << "(" << x_ << ", " << y_ << ", " << z_ << ")" << std::endl; | ||
} | } | ||
VectorPlus operator+(const VectorPlus& v1, const VectorPlus& v2) | VectorPlus operator+(const VectorPlus& v1, const VectorPlus& v2) | ||
{ | { | ||
// Provides a symmetric implementation of operator +: both vectors are at the same level! | // Provides a symmetric implementation of operator +: both vectors are at the same level! | ||
VectorPlus ret; | VectorPlus ret; | ||
ret.x_ = v1.x_ + v2.x_; | ret.x_ = v1.x_ + v2.x_; | ||
ret.y_ = v1.y_ + v2.y_; | ret.y_ = v1.y_ + v2.y_; | ||
ret.z_ = v1.z_ + v2.z_; | ret.z_ = v1.z_ + v2.z_; | ||
return ret; | return ret; | ||
} | } | ||
int main(int argc, char** argv) | int main(int argc, char** argv) | ||
{ | { | ||
VectorPlus v1(3., 5., 7.); | VectorPlus v1(3., 5., 7.); | ||
VectorPlus v2(7., 5., 3.); | VectorPlus v2(7., 5., 3.); | ||
VectorPlus v3 = v1 + v2; // Nicer syntax! | VectorPlus v3 = v1 + v2; // Nicer syntax! | ||
v3.Print(); | v3.Print(); | ||
VectorPlus v4 = operator+(v1, v2); // but "usual" method syntax is possible as well | VectorPlus v4 = operator+(v1, v2); // but "usual" method syntax is possible as well | ||
v4.Print(); | v4.Print(); | ||
return EXIT_SUCCESS; | return EXIT_SUCCESS; | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
It should be noted that for most operators it is also possible to define them as a class method instead: | It should be noted that for most operators it is also possible to define them as a class method instead: | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
class VectorPlusAsMethod | class VectorPlusAsMethod | ||
{ | { | ||
public : | public : | ||
VectorPlusAsMethod(double x, double y, double z); | VectorPlusAsMethod(double x, double y, double z); | ||
VectorPlusAsMethod() = default; | VectorPlusAsMethod() = default; | ||
void Print() const; | void Print() const; | ||
// I would rather put the definition outside but Xeus-cling doesn't seem to accept this. | // I would rather put the definition outside but Xeus-cling doesn't seem to accept this. | ||
VectorPlusAsMethod operator+(const VectorPlusAsMethod& v) const | VectorPlusAsMethod operator+(const VectorPlusAsMethod& v) const | ||
{ | { | ||
VectorPlusAsMethod ret; | VectorPlusAsMethod ret; | ||
ret.x_ = x_ + v.x_; | ret.x_ = x_ + v.x_; | ||
ret.y_ = y_ + v.y_; | ret.y_ = y_ + v.y_; | ||
ret.z_ = z_ + v.z_; | ret.z_ = z_ + v.z_; | ||
return ret; | return ret; | ||
} | } | ||
private : | private : | ||
double x_ = 0.; | double x_ = 0.; | ||
double y_ = 0.; | double y_ = 0.; | ||
double z_ = 0.; | double z_ = 0.; | ||
}; | }; | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
VectorPlusAsMethod::VectorPlusAsMethod(double x, double y, double z) | VectorPlusAsMethod::VectorPlusAsMethod(double x, double y, double z) | ||
: x_(x), | : x_(x), | ||
y_(y), | y_(y), | ||
z_(z) | z_(z) | ||
{ } | { } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
#include <iostream> | #include <iostream> | ||
void VectorPlusAsMethod::Print() const | void VectorPlusAsMethod::Print() const | ||
{ | { | ||
std::cout << "(" << x_ << ", " << y_ << ", " << z_ << ")" << std::endl; | std::cout << "(" << x_ << ", " << y_ << ", " << z_ << ")" << std::endl; | ||
} | } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
{ | { | ||
VectorPlusAsMethod v1(3., 5., 7.); | VectorPlusAsMethod v1(3., 5., 7.); | ||
VectorPlusAsMethod v2(7., 5., 3.); | VectorPlusAsMethod v2(7., 5., 3.); | ||
VectorPlusAsMethod v3 = v1 + v2; | VectorPlusAsMethod v3 = v1 + v2; | ||
v3.Print(); | v3.Print(); | ||
VectorPlusAsMethod v4 = v1.operator+(v2); // but "usual" method syntax is possible as well | VectorPlusAsMethod v4 = v1.operator+(v2); // but "usual" method syntax is possible as well | ||
v4.Print(); | v4.Print(); | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
We see here in the definition of the `operator+` that both `VectorPlusAsMethod` objects added aren't symmetric: one is the data attribute while the other is the data attribute of another object given as an argument. If is often advised to rather use the free function version to avoid this asymmetry, but it is mostly a matter of taste as both are working. | We see here in the definition of the `operator+` that both `VectorPlusAsMethod` objects added aren't symmetric: one is the data attribute while the other is the data attribute of another object given as an argument. If is often advised to rather use the free function version to avoid this asymmetry, but it is mostly a matter of taste as both are working. | ||
As a side note, please remark the `VectorPlusAsMethod::operator+` implementation is able to reach the private data attributes of the argument `v`; this means the private status is set **at class level** and not at object level. | As a side note, please remark the `VectorPlusAsMethod::operator+` implementation is able to reach the private data attributes of the argument `v`; this means the private status is set **at class level** and not at object level. | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
## Operator between different types | ## Operator between different types | ||
It is also possible to define an operator which acts upon two objects of different nature: | It is also possible to define an operator which acts upon two objects of different nature: | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
class VectorPlusDouble | class VectorPlusDouble | ||
{ | { | ||
public : | public : | ||
VectorPlusDouble(double x, double y, double z); | VectorPlusDouble(double x, double y, double z); | ||
VectorPlusDouble() = default; | VectorPlusDouble() = default; | ||
void Print() const; | void Print() const; | ||
// Defined in the class declaration due to Xeus-cling limitation. | // Defined in the class declaration due to Xeus-cling limitation. | ||
VectorPlusDouble operator+(double value) const | VectorPlusDouble operator+(double value) const | ||
{ | { | ||
VectorPlusDouble ret; | VectorPlusDouble ret; | ||
ret.x_ = x_ + value; | ret.x_ = x_ + value; | ||
ret.y_ = y_ + value; | ret.y_ = y_ + value; | ||
ret.z_ = z_ + value; | ret.z_ = z_ + value; | ||
return ret; | return ret; | ||
} | } | ||
private : | private : | ||
double x_ = 0.; | double x_ = 0.; | ||
double y_ = 0.; | double y_ = 0.; | ||
double z_ = 0.; | double z_ = 0.; | ||
}; | }; | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
#include <iostream> | #include <iostream> | ||
void VectorPlusDouble::Print() const | void VectorPlusDouble::Print() const | ||
{ | { | ||
std::cout << "(" << x_ << ", " << y_ << ", " << z_ << ")" << std::endl; | std::cout << "(" << x_ << ", " << y_ << ", " << z_ << ")" << std::endl; | ||
} | } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
VectorPlusDouble::VectorPlusDouble(double x, double y, double z) | VectorPlusDouble::VectorPlusDouble(double x, double y, double z) | ||
: x_(x), | : x_(x), | ||
y_(y), | y_(y), | ||
z_(z) | z_(z) | ||
{ } | { } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
{ | { | ||
VectorPlusDouble vector(5., 3.2, -1.); | VectorPlusDouble vector(5., 3.2, -1.); | ||
VectorPlusDouble vector_plus_5 = vector + 5.; | VectorPlusDouble vector_plus_5 = vector + 5.; | ||
vector_plus_5.Print(); | vector_plus_5.Print(); | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
However, pay attention to the fact this operator is not commutative. It is indeed a shortcut to | However, pay attention to the fact this operator is not commutative. It is indeed a shortcut to | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
{ | { | ||
VectorPlusDouble vector(5., 3.2, -1.); | VectorPlusDouble vector(5., 3.2, -1.); | ||
VectorPlusDouble vector_plus_5 = vector.operator+(5.); | VectorPlusDouble vector_plus_5 = vector.operator+(5.); | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
and the following won't compile: | and the following won't compile: | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
{ | { | ||
VectorPlusDouble vector(5., 3.2, -1.); | VectorPlusDouble vector(5., 3.2, -1.); | ||
VectorPlusDouble vector_plus_5 = 5. + vector; // COMPILATION ERROR! | VectorPlusDouble vector_plus_5 = 5. + vector; // COMPILATION ERROR! | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
If you want it to be possible, you have to define the operator with arguments in both orders; you therefore need to use out-of-class prototype of the function (can't show it currently due to Xeus-cling limitation, available [@Coliru](https://coliru.stacked-crooked.com/a/c03fc40e0f0a8ea0)). | If you want it to be possible, you have to define the operator with arguments in both orders; you therefore need to use out-of-class prototype of the function (can't show it currently due to Xeus-cling limitation, available [@Coliru](https://coliru.stacked-crooked.com/a/c03fc40e0f0a8ea0)). | ||
Of course, it is a **good practice** to define one in way of the other: | Of course, it is a **good practice** to define one in way of the other: | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
// Won't compile in Xeus-cling | // Won't compile in Xeus-cling | ||
#include <cstdlib> | #include <cstdlib> | ||
#include <iostream> | #include <iostream> | ||
class VectorPlusDoubleCommutative | class VectorPlusDoubleCommutative | ||
{ | { | ||
public : | public : | ||
VectorPlusDoubleCommutative(double x, double y, double z); | VectorPlusDoubleCommutative(double x, double y, double z); | ||
VectorPlusDoubleCommutative() = default; | VectorPlusDoubleCommutative() = default; | ||
void Print() const; | void Print() const; | ||
friend VectorPlusDoubleCommutative operator+(const VectorPlusDoubleCommutative& v, double value); | friend VectorPlusDoubleCommutative operator+(const VectorPlusDoubleCommutative& v, double value); | ||
friend VectorPlusDoubleCommutative operator+(double value, const VectorPlusDoubleCommutative& v); | friend VectorPlusDoubleCommutative operator+(double value, const VectorPlusDoubleCommutative& v); | ||
private : | private : | ||
double x_ = 0.; | double x_ = 0.; | ||
double y_ = 0.; | double y_ = 0.; | ||
double z_ = 0.; | double z_ = 0.; | ||
}; | }; | ||
void VectorPlusDoubleCommutative::Print() const | void VectorPlusDoubleCommutative::Print() const | ||
{ | { | ||
std::cout << "(" << x_ << ", " << y_ << ", " << z_ << ")" << std::endl; | std::cout << "(" << x_ << ", " << y_ << ", " << z_ << ")" << std::endl; | ||
} | } | ||
VectorPlusDoubleCommutative::VectorPlusDoubleCommutative(double x, double y, double z) | VectorPlusDoubleCommutative::VectorPlusDoubleCommutative(double x, double y, double z) | ||
: x_(x), | : x_(x), | ||
y_(y), | y_(y), | ||
z_(z) | z_(z) | ||
{ } | { } | ||
VectorPlusDoubleCommutative operator+(const VectorPlusDoubleCommutative& v, double value) | VectorPlusDoubleCommutative operator+(const VectorPlusDoubleCommutative& v, double value) | ||
{ | { | ||
VectorPlusDoubleCommutative ret; | VectorPlusDoubleCommutative ret; | ||
ret.x_ = v.x_ + value; | ret.x_ = v.x_ + value; | ||
ret.y_ = v.y_ + value; | ret.y_ = v.y_ + value; | ||
ret.z_ = v.z_ + value; | ret.z_ = v.z_ + value; | ||
return ret; | return ret; | ||
} | } | ||
VectorPlusDoubleCommutative operator+(double value, const VectorPlusDoubleCommutative& v) | VectorPlusDoubleCommutative operator+(double value, const VectorPlusDoubleCommutative& v) | ||
{ | { | ||
return v + value; // good practice: make it rely upon the other `operator+` defined! | return v + value; // good practice: make it rely upon the other `operator+` defined! | ||
} | } | ||
int main(int argc, char** argv) | int main(int argc, char** argv) | ||
{ | { | ||
VectorPlusDoubleCommutative vector(5., 3.2, -1.); | VectorPlusDoubleCommutative vector(5., 3.2, -1.); | ||
VectorPlusDoubleCommutative vector_plus_5 = vector + 5.; | VectorPlusDoubleCommutative vector_plus_5 = vector + 5.; | ||
VectorPlusDoubleCommutative vector_plus_5_commutated = 5. + vector; | VectorPlusDoubleCommutative vector_plus_5_commutated = 5. + vector; | ||
vector_plus_5.Print(); | vector_plus_5.Print(); | ||
vector_plus_5_commutated.Print(); | vector_plus_5_commutated.Print(); | ||
return EXIT_SUCCESS; | return EXIT_SUCCESS; | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
## Limitations | ## Limitations | ||
You cannot change: | You cannot change: | ||
* The number of operators arguments | * The number of operators arguments | ||
* The [precedence rules](https://en.cppreference.com/w/cpp/language/operator_precedence) (between `+` and `*` for instance) | * The [precedence rules](https://en.cppreference.com/w/cpp/language/operator_precedence) (between `+` and `*` for instance) | ||
You can't _invent_ new operators, but only redefine operators in the following list (taken from [Operators in C and C++](https://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B), please consult for more details): | You can't _invent_ new operators, but only redefine operators in the following list (taken from [Operators in C and C++](https://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B), please consult for more details): | ||
* Arithmetic operators: `+ - * / % ++ --` | * Arithmetic operators: `+ - * / % ++ --` | ||
* Comparison operators: `== != <= >= < > <=>` | * Comparison operators: `== != <= >= < > <=>` | ||
* Logical operators: `! && ||` | * Logical operators: `! && ||` | ||
* Bitwise operators: `~ & | ^ << >>` | * Bitwise operators: `~ & | ^ << >>` | ||
* Assignment operators: `= += -= *= /= %= &= |= ^= <<= >>=` | * Assignment operators: `= += -= *= /= %= &= |= ^= <<= >>=` | ||
* Member and pointer operators: `[] * & -> ->*` | * Member and pointer operators: `[] * & -> ->*` | ||
* Other operators: `() , "" new new[] delete delete[]` | * Other operators: `() , "" new new[] delete delete[]` | ||
* Conversion operators - see next section | * Conversion operators - see next section | ||
That list might be incomplete: `""` operator was introduced in C++ 11 (cf. [this blog post](https://www.fluentcpp.com/2016/12/08/strong-types-for-strong-interfaces/)), and `<=>` operator was introduced in C++ 20, for instance. | That list might be incomplete: `""` operator was introduced in C++ 11 (cf. [this blog post](https://www.fluentcpp.com/2016/12/08/strong-types-for-strong-interfaces/)), and `<=>` operator was introduced in C++ 20, for instance. | ||
If not defined, some of them exist by default: | If not defined, some of them exist by default: | ||
``` | ``` | ||
= | = | ||
-> ->* | -> ->* | ||
new delete | new delete | ||
``` | ``` | ||
Some can never be redefined: | Some can never be redefined: | ||
``` | ``` | ||
: :: . .* ? ?: sizeof | : :: . .* ? ?: sizeof | ||
``` | ``` | ||
## Conversion operators | ## Conversion operators | ||
A conversion operator is a method of transforming an object into a given type. When the compiler needs to force the type of an object, implicitly or explicitly, it is this operator that will be called. A conversion operator is required for each type of conversion. | A conversion operator is a method of transforming an object into a given type. When the compiler needs to force the type of an object, implicitly or explicitly, it is this operator that will be called. A conversion operator is required for each type of conversion. | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
#include <iostream> | #include <iostream> | ||
class Rational | class Rational | ||
{ | { | ||
public : | public : | ||
Rational(int numerator, int denominator); | Rational(int numerator, int denominator); | ||
operator int() const; | operator int() const; | ||
operator double() const; | operator double() const; | ||
private : | private : | ||
int numerator_ { }; | int numerator_ { }; | ||
int denominator_ { }; | int denominator_ { }; | ||
}; | }; | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
Rational::Rational(int numerator, int denominator) | Rational::Rational(int numerator, int denominator) | ||
: numerator_(numerator), | : numerator_(numerator), | ||
denominator_(denominator) | denominator_(denominator) | ||
{ } | { } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
Rational::operator int() const | Rational::operator int() const | ||
{ | { | ||
return numerator_ / denominator_; | return numerator_ / denominator_; | ||
} | } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
Rational::operator double() const | Rational::operator double() const | ||
{ | { | ||
return static_cast<double>(numerator_) / denominator_; | return static_cast<double>(numerator_) / denominator_; | ||
} | } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
#include <iostream> | #include <iostream> | ||
{ | { | ||
Rational val_r(15, 7); | Rational val_r(15, 7); | ||
std::cout << "val as double: " << static_cast<double>(val_r) << std::endl ; | std::cout << "val as double: " << static_cast<double>(val_r) << std::endl ; | ||
std::cout << "val as integer: " << static_cast<int>(val_r) << std::endl ; | std::cout << "val as integer: " << static_cast<int>(val_r) << std::endl ; | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
As for constructors, you should add the keyword `explicit` in the class declaration to ensure no implicit conversion occurs: | As for constructors, you should add the keyword `explicit` in the class declaration to ensure no implicit conversion occurs: | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
// Won't run in Xeus-cling, as it's not within the class declaration | // Won't run in Xeus-cling, as it's not within the class declaration | ||
explicit operator int() const; | explicit operator int() const; | ||
explicit operator double() const; | explicit operator double() const; | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
[© Copyright](../COPYRIGHT.md) | [© Copyright](../COPYRIGHT.md) | ||
... | ... |
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
# [Getting started in C++](./) - [Operators](./0-main.ipynb) - [Comparison operators](./2-Comparison.ipynb) | # [Getting started in C++](./) - [Operators](./0-main.ipynb) - [Comparison operators](./2-Comparison.ipynb) | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
## Operator <, >, <=, >= | ## Operator <, >, <=, >= | ||
These 4 operators are **not** implicitly defined for a class (and that is very natural: what would be the rationale to devise the default `<` operator for a `ComplexNumber` or a `Car` class?) | These 4 operators are **not** implicitly defined for a class (and that is very natural: what would be the rationale to devise the default `<` operator for a `ComplexNumber` or a `Car` class?) | ||
Let's take again our `Rational` class to illustrate this: | Let's take again our `Rational` class to illustrate this: | ||
**Xeus-cling** issue: cling doesn't accept operator definition outside of class; please use [@Coliru](https://coliru.stacked-crooked.com/a/6f0dc54ac63f476c): | **Xeus-cling** issue: cling doesn't accept operator definition outside of class; please use [@Coliru](https://coliru.stacked-crooked.com/a/6f0dc54ac63f476c): | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
// Xeus-cling issue: doesn't compile! | // Xeus-cling issue: doesn't compile! | ||
#include <iostream> | #include <iostream> | ||
class Rational | class Rational | ||
{ | { | ||
public : | public : | ||
explicit Rational(int numerator, int denominator); | explicit Rational(int numerator, int denominator); | ||
explicit operator double() const; | explicit operator double() const; | ||
private : | private : | ||
int numerator_ = 0; | int numerator_ = 0; | ||
int denominator_ = 0; | int denominator_ = 0; | ||
}; | }; | ||
Rational::Rational(int numerator, int denominator) | Rational::Rational(int numerator, int denominator) | ||
: numerator_(numerator), | : numerator_(numerator), | ||
denominator_(denominator) | denominator_(denominator) | ||
{ } | { } | ||
Rational::operator double() const | Rational::operator double() const | ||
{ | { | ||
return static_cast<double>(numerator_) / denominator_; | return static_cast<double>(numerator_) / denominator_; | ||
} | } | ||
bool operator<(const Rational& lhs, const Rational& rhs) | bool operator<(const Rational& lhs, const Rational& rhs) | ||
{ | { | ||
return static_cast<double>(lhs) < static_cast<double>(rhs); | return static_cast<double>(lhs) < static_cast<double>(rhs); | ||
} | } | ||
int main(int argc, char** argv) | int main(int argc, char** argv) | ||
{ | { | ||
Rational r1(15, 7); | Rational r1(15, 7); | ||
Rational r2(27, 4); | Rational r2(27, 4); | ||
std::cout << (r1 < r2) << std::endl; | std::cout << (r1 < r2) << std::endl; | ||
return EXIT_SUCCESS; | return EXIT_SUCCESS; | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
Defining `operator<` does not automatically define the others, but a **good practice** is to define the other comparison operators once `operator<` and `operator!=` are defined, and in function of these two as follows: | Defining `operator<` does not automatically define the others, but a **good practice** is to define the other comparison operators once `operator<` and `operator!=` are defined, and in function of these two as follows: | ||
* `operator>(lhs, rhs)` is `operator<(rhs, lhs)` | * `operator>(lhs, rhs)` is `operator<(rhs, lhs)` | ||
* `operator>=(lhs, rhs)` is `!operator<(lhs, rhs)` | * `operator>=(lhs, rhs)` is `!operator<(lhs, rhs)` | ||
* `operator<=(lhs, rhs)` is `!operator>(lhs, rhs)` | * `operator<=(lhs, rhs)` is `!operator>(lhs, rhs)` | ||
As always with operators overloading, make sure your implementation is consistent: it's not because you are allowed by the language to define an operator `<` which is not the negation of `>=` that you should do it! | As always with operators overloading, make sure your implementation is consistent: it's not because you are allowed by the language to define an operator `<` which is not the negation of `>=` that you should do it! | ||
## Operator == and != | ## Operator == and != | ||
* None is defined by default. | * None is defined by default. | ||
* They are independent from each other: defining one **doesn't** define the other one... | * They are independent from each other: defining one **doesn't** define the other one... | ||
* ... but **never** define `operator!=` as something other than `!(operator==)` | * ... but **never** define `operator!=` as something other than `!(operator==)` | ||
* As we've seen above, they are not involved at all in implicit definitions of `<=` or `>=` if only `<` or `>` is explicitly defined. Same remark as the line above though! | * As we've seen above, they are not involved at all in implicit definitions of `<=` or `>=` if only `<` or `>` is explicitly defined. Same remark as the line above though! | ||
* Make sure you're thought well the result of your comparison: | * Make sure you're thought well the result of your comparison: | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
class Vector | class Vector | ||
{ | { | ||
public: | public: | ||
Vector(int x, int y, int z); | Vector(int x, int y, int z); | ||
~Vector(); | ~Vector(); | ||
// Working around the Xeus-cling bug but avoid direct definition in class declaration... | // Working around the Xeus-cling bug but avoid direct definition in class declaration... | ||
// As mentioned previously free functions should be preferred, but still the Xeus-cling bug. | // As mentioned previously free functions should be preferred, but still the Xeus-cling bug. | ||
bool operator==(const Vector& rhs) const | bool operator==(const Vector& rhs) const | ||
{ | { | ||
return array_ == rhs.array_; // BUG! | return array_ == rhs.array_; // BUG! | ||
} | } | ||
private: | private: | ||
int* array_ = nullptr; | int* array_ = nullptr; | ||
}; | }; | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
Vector::Vector(int x, int y, int z) | Vector::Vector(int x, int y, int z) | ||
{ | { | ||
array_ = new int[3]; | array_ = new int[3]; | ||
array_[0] = x; | array_[0] = x; | ||
array_[1] = y; | array_[1] = y; | ||
array_[2] = z; | array_[2] = z; | ||
} | } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
Vector::~Vector() | Vector::~Vector() | ||
{ | { | ||
delete[] array_; | delete[] array_; | ||
} | } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
#include <iostream> | #include <iostream> | ||
{ | { | ||
Vector v1(3, 4, 5); | Vector v1(3, 4, 5); | ||
Vector v2(3, 4, 5); | Vector v2(3, 4, 5); | ||
std::cout << "Are equal? : " << (v1 == v2) << std::endl; | std::cout << "Are equal? : " << (v1 == v2) << std::endl; | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
The issue here is that we compare the `array_` pointers of `v1` and `v2`; the content might be the same but the address in memory is clearly not! So make sure in overloading these operators you are really comparing what you think you are comparing (and remember unit tests are your friends for this kind of checks!). | The issue here is that we compare the `array_` pointers of `v1` and `v2`; the content might be the same but the address in memory is clearly not! So make sure in overloading these operators you are really comparing what you think you are comparing (and remember unit tests are your friends for this kind of checks!). | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
## C++ 20 refinements | ## C++ 20 refinements | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
### Spaceship operator | ### Spaceship operator | ||
Currently (in C++ 17 and below) you have to define all the comparison operators, which can quickly become rather tedious (the following code doesn't work in Xeus-cling; you may use [@Coliru](https://coliru.stacked-crooked.com/a/122ec1b6a6ac3d0f)): | Currently (in C++ 17 and below) you have to define all the comparison operators, which can quickly become rather tedious (the following code doesn't work in Xeus-cling; you may use [@Coliru](https://coliru.stacked-crooked.com/a/122ec1b6a6ac3d0f)): | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
// Xeus-cling issue: doesn't compile! | // Xeus-cling issue: doesn't compile! | ||
#include <iostream> | #include <iostream> | ||
#include <cmath> | #include <cmath> | ||
class Rational | class Rational | ||
{ | { | ||
public : | public : | ||
explicit Rational(int numerator, int denominator); | explicit Rational(int numerator, int denominator); | ||
explicit operator double() const; | explicit operator double() const; | ||
private : | private : | ||
int numerator_ = 0; | int numerator_ = 0; | ||
int denominator_ = 0; | int denominator_ = 0; | ||
}; | }; | ||
Rational::Rational(int numerator, int denominator) | Rational::Rational(int numerator, int denominator) | ||
: numerator_(numerator), | : numerator_(numerator), | ||
denominator_(denominator) | denominator_(denominator) | ||
{ } | { } | ||
Rational::operator double() const | Rational::operator double() const | ||
{ | { | ||
return static_cast<double>(numerator_) / static_cast<double>(denominator_); | return static_cast<double>(numerator_) / static_cast<double>(denominator_); | ||
} | } | ||
bool operator<(const Rational& lhs, const Rational& rhs) | bool operator<(const Rational& lhs, const Rational& rhs) | ||
{ | { | ||
return static_cast<double>(lhs) < static_cast<double>(rhs); | return static_cast<double>(lhs) < static_cast<double>(rhs); | ||
} | } | ||
bool operator==(const Rational& lhs, const Rational& rhs) | bool operator==(const Rational& lhs, const Rational& rhs) | ||
{ | { | ||
return std::fabs(static_cast<double>(lhs) - static_cast<double>(rhs) ) < 1.e-16; // crude... | return std::fabs(static_cast<double>(lhs) - static_cast<double>(rhs) ) < 1.e-16; // crude... | ||
} | } | ||
bool operator!=(const Rational& lhs, const Rational& rhs) | bool operator!=(const Rational& lhs, const Rational& rhs) | ||
{ | { | ||
return !operator==(lhs, rhs); | return !operator==(lhs, rhs); | ||
} | } | ||
bool operator>(const Rational& lhs, const Rational& rhs) | bool operator>(const Rational& lhs, const Rational& rhs) | ||
{ | { | ||
return operator<(rhs, lhs); | return operator<(rhs, lhs); | ||
} | } | ||
bool operator>=(const Rational& lhs, const Rational& rhs) | bool operator>=(const Rational& lhs, const Rational& rhs) | ||
{ | { | ||
return !operator<(lhs, rhs); | return !operator<(lhs, rhs); | ||
} | } | ||
bool operator<=(const Rational& lhs, const Rational& rhs) | bool operator<=(const Rational& lhs, const Rational& rhs) | ||
{ | { | ||
return !operator>(lhs, rhs); | return !operator>(lhs, rhs); | ||
} | } | ||
int main() | int main() | ||
{ | { | ||
Rational a(5, 2); | Rational a(5, 2); | ||
Rational b(17, 5); | Rational b(17, 5); | ||
std::cout << std::boolalpha << "a < b ? -> " << (a < b) << std::endl; | std::cout << std::boolalpha << "a < b ? -> " << (a < b) << std::endl; | ||
std::cout << std::boolalpha << "a > b ? -> " << (a > b) << std::endl; | std::cout << std::boolalpha << "a > b ? -> " << (a > b) << std::endl; | ||
std::cout << std::boolalpha << "a <= b ? -> " << (a <= b) << std::endl; | std::cout << std::boolalpha << "a <= b ? -> " << (a <= b) << std::endl; | ||
std::cout << std::boolalpha << "a >= b ? -> " << (a >= b) << std::endl; | std::cout << std::boolalpha << "a >= b ? -> " << (a >= b) << std::endl; | ||
std::cout << std::boolalpha << "a == b ? -> " << (a == b) << std::endl; | std::cout << std::boolalpha << "a == b ? -> " << (a == b) << std::endl; | ||
std::cout << std::boolalpha << "a != b ? -> " << (a != b) << std::endl; | std::cout << std::boolalpha << "a != b ? -> " << (a != b) << std::endl; | ||
return EXIT_SUCCESS; | return EXIT_SUCCESS; | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
C++ 20 introduces the so-called spaceship operator, which enables defining more concisely all those operators (the following code doesn't work in Xeus-cling; you may use [@Coliru](https://coliru.stacked-crooked.com/a/1cec8ddda8a1eece)): | C++ 20 introduces the so-called spaceship operator, which enables defining more concisely all those operators (the following code doesn't work in Xeus-cling; you may use [@Coliru](https://coliru.stacked-crooked.com/a/1cec8ddda8a1eece)): | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
// Xeus-cling issue: doesn't compile! (and C++ 20 not yet supported there anyway) | // Xeus-cling issue: doesn't compile! (and C++ 20 not yet supported there anyway) | ||
#include <iostream> | #include <iostream> | ||
#include <cmath> | #include <cmath> | ||
class Rational20 | class Rational20 | ||
{ | { | ||
public : | public : | ||
explicit Rational20(int numerator, int denominator); | explicit Rational20(int numerator, int denominator); | ||
explicit operator double() const; | explicit operator double() const; | ||
private : | private : | ||
int numerator_ = 0; | int numerator_ = 0; | ||
int denominator_ = 0; | int denominator_ = 0; | ||
}; | }; | ||
Rational20::Rational20(int numerator, int denominator) | Rational20::Rational20(int numerator, int denominator) | ||
: numerator_(numerator), | : numerator_(numerator), | ||
denominator_(denominator) | denominator_(denominator) | ||
{ } | { } | ||
Rational20::operator double() const | Rational20::operator double() const | ||
{ | { | ||
return static_cast<double>(numerator_) / static_cast<double>(denominator_); | return static_cast<double>(numerator_) / static_cast<double>(denominator_); | ||
} | } | ||
bool operator==(const Rational20& lhs, const Rational20& rhs) | bool operator==(const Rational20& lhs, const Rational20& rhs) | ||
{ | { | ||
return std::fabs(static_cast<double>(lhs) - static_cast<double>(rhs)) < 1.e-16; // crude... | return std::fabs(static_cast<double>(lhs) - static_cast<double>(rhs)) < 1.e-16; // crude... | ||
} | } | ||
std::partial_ordering operator<=>(const Rational20& lhs, const Rational20& rhs) | std::partial_ordering operator<=>(const Rational20& lhs, const Rational20& rhs) | ||
{ | { | ||
return static_cast<double>(lhs) <=> static_cast<double>(rhs); | return static_cast<double>(lhs) <=> static_cast<double>(rhs); | ||
} | } | ||
int main() | int main() | ||
{ | { | ||
Rational20 a(5, 2); | Rational20 a(5, 2); | ||
Rational20 b(17, 5); | Rational20 b(17, 5); | ||
std::cout << std::boolalpha << "a < b ? -> " << (a < b) << std::endl; | std::cout << std::boolalpha << "a < b ? -> " << (a < b) << std::endl; | ||
std::cout << std::boolalpha << "a > b ? -> " << (a > b) << std::endl; | std::cout << std::boolalpha << "a > b ? -> " << (a > b) << std::endl; | ||
std::cout << std::boolalpha << "a <= b ? -> " << (a <= b) << std::endl; | std::cout << std::boolalpha << "a <= b ? -> " << (a <= b) << std::endl; | ||
std::cout << std::boolalpha << "a >= b ? -> " << (a >= b) << std::endl; | std::cout << std::boolalpha << "a >= b ? -> " << (a >= b) << std::endl; | ||
std::cout << std::boolalpha << "a == b ? -> " << (a == b) << std::endl; | std::cout << std::boolalpha << "a == b ? -> " << (a == b) << std::endl; | ||
std::cout << std::boolalpha << "a != b ? -> " << (a != b) << std::endl; | std::cout << std::boolalpha << "a != b ? -> " << (a != b) << std::endl; | ||
return EXIT_SUCCESS; | return EXIT_SUCCESS; | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
### Default behaviour | ### Default behaviour | ||
In C++ 17 and below, you also can't define a default behaviour for the comparison operators for your class. | In C++ 17 and below, you also can't define a default behaviour for the comparison operators for your class. | ||
Sometimes it really is appropriate - for the case of `Rational` the default behaviour would probably be something inappropriate, but in other cases this lack of default behaviour may be irksome when you for instance want to check all data attributes are equal: | Sometimes it really is appropriate - for the case of `Rational` the default behaviour would probably be something inappropriate, but in other cases this lack of default behaviour may be irksome when you for instance want to check all data attributes are equal: | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
struct Toolbox | struct Toolbox | ||
{ | { | ||
unsigned int Nscrewdriver_; | unsigned int Nscrewdriver_; | ||
unsigned int Nhammer_; | unsigned int Nhammer_; | ||
unsigned int Nnails_; | unsigned int Nnails_; | ||
}; | }; | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
// Xeus-cling issue: doesn't compile! | // Xeus-cling issue: doesn't compile! | ||
bool operator==(const Toolbox& lhs, const Toolbox& rhs) | bool operator==(const Toolbox& lhs, const Toolbox& rhs) | ||
{ | { | ||
return lhs.Nscrewdriver_ == rhs.Nscrewdriver_ | return lhs.Nscrewdriver_ == rhs.Nscrewdriver_ | ||
&& lhs.Nhammer_ == rhs.Nhammer_ | && lhs.Nhammer_ == rhs.Nhammer_ | ||
&& lhs.Nails_ == rhs.Nnails_; | && lhs.Nails_ == rhs.Nnails_; | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
This is rather cumbersome to type, but it is even dangerous: if at some point we extend the class and add `Nsaw_` for instance, we need not to forget to update the operator as well! | This is rather cumbersome to type, but it is even dangerous: if at some point we extend the class and add `Nsaw_` for instance, we need not to forget to update the operator as well! | ||
C++ 20 will enable default behaviour for the comparison operators, so in this case you would be able to write instead ([@Coliru](https://coliru.stacked-crooked.com/a/fa889df647c73a7f)): | C++ 20 will enable default behaviour for the comparison operators, so in this case you would be able to write instead ([@Coliru](https://coliru.stacked-crooked.com/a/fa889df647c73a7f)): | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
#include <iostream> | #include <iostream> | ||
struct Toolbox | struct Toolbox | ||
{ | { | ||
bool operator==(const Toolbox&) const = default; | bool operator==(const Toolbox&) const = default; | ||
auto operator<=>(const Toolbox&) const = default; | auto operator<=>(const Toolbox&) const = default; | ||
unsigned int Nscrewdriver_; | unsigned int Nscrewdriver_; | ||
unsigned int Nhammer_; | unsigned int Nhammer_; | ||
unsigned int Nnails_; | unsigned int Nnails_; | ||
}; | }; | ||
int main(int argc, char** argv) | int main(int argc, char** argv) | ||
{ | { | ||
Toolbox toolbox1; | Toolbox toolbox1; | ||
toolbox1.Nscrewdriver_ = 5; | toolbox1.Nscrewdriver_ = 5; | ||
toolbox1.Nhammer_ = 4; | toolbox1.Nhammer_ = 4; | ||
toolbox1.Nnails_ = 200; | toolbox1.Nnails_ = 200; | ||
Toolbox toolbox2; | Toolbox toolbox2; | ||
toolbox2.Nscrewdriver_ = 5; | toolbox2.Nscrewdriver_ = 5; | ||
toolbox2.Nhammer_ = 4; | toolbox2.Nhammer_ = 4; | ||
toolbox2.Nnails_ = 200; | toolbox2.Nnails_ = 200; | ||
std::cout << std::boolalpha << (toolbox1 == toolbox2) << std::endl; | std::cout << std::boolalpha << (toolbox1 == toolbox2) << std::endl; | ||
std::cout << std::boolalpha << (toolbox1 < toolbox2) << std::endl; | std::cout << std::boolalpha << (toolbox1 < toolbox2) << std::endl; | ||
return EXIT_SUCCESS; | return EXIT_SUCCESS; | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
As you can experiment, the `operator<` will compare data attributes in order of their declaration as if it was implemented this way: | As you can experiment, the `operator<` will compare data attributes in order of their declaration as if it was implemented this way: | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
// What the operator< would look like if we wrote it ourselves: | // What the operator< would look like if we wrote it ourselves: | ||
bool operator<(const Rational& lhs, const Rational& rhs) | bool operator<(const Rational& lhs, const Rational& rhs) | ||
{ | { | ||
if !(toolbox1.Nscrewdriver_ == toolbox2.Nscrewdriver_) | if !(toolbox1.Nscrewdriver_ == toolbox2.Nscrewdriver_) | ||
return toolbox1.Nscrewdriver_ < toolbox2.Nscrewdriver_; | return toolbox1.Nscrewdriver_ < toolbox2.Nscrewdriver_; | ||
if !(toolbox1.Nhammer_ == toolbox2.Nhammer_) | if !(toolbox1.Nhammer_ == toolbox2.Nhammer_) | ||
return toolbox1.Nhammer_ < toolbox2.Nhammer_; | return toolbox1.Nhammer_ < toolbox2.Nhammer_; | ||
if !(toolbox1.Nnails_ == toolbox2.Nnails_) | if !(toolbox1.Nnails_ == toolbox2.Nnails_) | ||
return toolbox1.Nnails_ < toolbox2.Nnails_; | return toolbox1.Nnails_ < toolbox2.Nnails_; | ||
return false; | return false; | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
[This example](https://www.modernescpp.com/index.php/c-20-more-details-to-the-spaceship-operator), or [that example](https://devblogs.microsoft.com/cppblog/simplify-your-code-with-rocket-science-c20s-spaceship-operator/) show other defaulting of a comparison used directly upon the spaceship operator. | [This example](https://www.modernescpp.com/index.php/c-20-more-details-to-the-spaceship-operator), or [that example](https://devblogs.microsoft.com/cppblog/simplify-your-code-with-rocket-science-c20s-spaceship-operator/) show other defaulting of a comparison used directly upon the spaceship operator. | ||
**Be aware** the default implementation may not be what you expected (see the `Rational` example, where even the default `operator==` is unlikely to be what we want). | **Be aware** the default implementation may not be what you expected (see the `Rational` example, where even the default `operator==` is unlikely to be what we want). | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
[© Copyright](../COPYRIGHT.md) | [© Copyright](../COPYRIGHT.md) | ||
... | ... |
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
# [Getting started in C++](./) - [Operators](./0-main.ipynb) - [Stream operators](./3-Stream.ipynb) | # [Getting started in C++](./) - [Operators](./0-main.ipynb) - [Stream operators](./3-Stream.ipynb) | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
## Operator << | ## Operator << | ||
So far, we have very often defined methods and/or functions named `Print()` to provide a visual printing of the content of a class. | So far, we have very often defined methods and/or functions named `Print()` to provide a visual printing of the content of a class. | ||
However, it's even better if we could use the `<<` syntax directly, and the way to do so is to provide an overload of `operator<<`. | However, it's even better if we could use the `<<` syntax directly, and the way to do so is to provide an overload of `operator<<`. | ||
The recommended way to implement it is to use under the hood a `Print(std::ostream&)` (or whatever you want to call it): | The recommended way to implement it is to use under the hood a `Print(std::ostream&)` (or whatever you want to call it): | ||
**Xeus-cling** issue: cling doesn't accept operator definition outside of class; please use [@Coliru](https://coliru.stacked-crooked.com/a/04f59249cce6c131) | **Xeus-cling** issue: cling doesn't accept operator definition outside of class; please use [@Coliru](https://coliru.stacked-crooked.com/a/04f59249cce6c131) | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
// Doesn't run on Xeus-cling due to definition of an operator outside of a class | // Doesn't run on Xeus-cling due to definition of an operator outside of a class | ||
// A ticket has been opened for this: https://github.com/QuantStack/xeus-cling/issues/214 | // A ticket has been opened for this: https://github.com/QuantStack/xeus-cling/issues/214 | ||
#include <iostream> | #include <iostream> | ||
class Rational | class Rational | ||
{ | { | ||
public : | public : | ||
explicit Rational(int numerator, int denominator); | explicit Rational(int numerator, int denominator); | ||
// Might even be private; in which case friendship needs to be declared | // Might even be private; in which case friendship needs to be declared | ||
void Print(std::ostream& out) const; | void Print(std::ostream& out) const; | ||
private : | private : | ||
int numerator_ = 0; | int numerator_ = 0; | ||
int denominator_ = 0; | int denominator_ = 0; | ||
}; | }; | ||
Rational::Rational(int numerator, int denominator) | Rational::Rational(int numerator, int denominator) | ||
: numerator_(numerator), | : numerator_(numerator), | ||
denominator_(denominator) | denominator_(denominator) | ||
{ } | { } | ||
void Rational::Print(std::ostream& out) const | void Rational::Print(std::ostream& out) const | ||
{ | { | ||
out << numerator_ << " / " << denominator_; | out << numerator_ << " / " << denominator_; | ||
} | } | ||
std::ostream& operator<<(std::ostream& out, const Rational& r) | std::ostream& operator<<(std::ostream& out, const Rational& r) | ||
{ | { | ||
r.Print(out); // see how the bulk of the work is done by the 'Print()' method! | r.Print(out); // see how the bulk of the work is done by the 'Print()' method! | ||
return out; | return out; | ||
} | } | ||
int main() | int main() | ||
{ | { | ||
Rational r1(22, 7); | Rational r1(22, 7); | ||
Rational r2(77, 17); | Rational r2(77, 17); | ||
std::cout << "Rational = " << r1 << std::endl; | std::cout << "Rational = " << r1 << std::endl; | ||
std::cout << "Call may also be chained due to the signature of the function: " << r1 | std::cout << "Call may also be chained due to the signature of the function: " << r1 | ||
<< " and " << r2 << std::endl; | << " and " << r2 << std::endl; | ||
return EXIT_SUCCESS; | return EXIT_SUCCESS; | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
Please notice the slightly weird expected syntax for the `operator<<`: the `std::ostream` appears actually twice; the reason for that is to enable chained calls: | Please notice the slightly weird expected syntax for the `operator<<`: the `std::ostream` appears actually twice; the reason for that is to enable chained calls: | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
// Doesn't run on Xeus-cling! | // Doesn't run on Xeus-cling! | ||
int main() | int main() | ||
{ | { | ||
Rational r1(22, 7); | Rational r1(22, 7); | ||
Rational r2(84, 9); | Rational r2(84, 9); | ||
std::cout << "Rationals = " << r1 << " and " << r2 << std::endl; | std::cout << "Rationals = " << r1 << " and " << r2 << std::endl; | ||
return EXIT_SUCCESS; | return EXIT_SUCCESS; | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
## Operator >> | ## Operator >> | ||
`operator>>` might be overloaded in a similar way; it might be used for instance to create an object from data read in a file. As usual with `operator>>`, you should be cautious and handle well the case in which the flux becomes invalid: | `operator>>` might be overloaded in a similar way; it might be used for instance to create an object from data read in a file. As usual with `operator>>`, you should be cautious and handle well the case in which the flux becomes invalid: | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
// Doesn't run on Xeus-cling due to definition of an operator outside of a class | // Doesn't run on Xeus-cling due to definition of an operator outside of a class | ||
// A ticket has been opened for this: https://github.com/QuantStack/xeus-cling/issues/214 | // A ticket has been opened for this: https://github.com/QuantStack/xeus-cling/issues/214 | ||
#include <iostream> | #include <iostream> | ||
class Rational | class Rational | ||
{ | { | ||
public : | public : | ||
explicit Rational(int numerator, int denominator); | explicit Rational(int numerator, int denominator); | ||
void Print(std::ostream& out) const; | void Print(std::ostream& out) const; | ||
friend std::istream& operator>>(std::istream& in, Rational& r); | friend std::istream& operator>>(std::istream& in, Rational& r); | ||
private: | private: | ||
//! Set method to modify numerator and denominator. I made the arbitrary choice to make it private | //! Set method to modify numerator and denominator. I made the arbitrary choice to make it private | ||
//! to illustrate friendship. | //! to illustrate friendship. | ||
void Set(int numerator, int denominator); | void Set(int numerator, int denominator); | ||
private : | private : | ||
int numerator_ = 0; | int numerator_ = 0; | ||
int denominator_ = 0; | int denominator_ = 0; | ||
}; | }; | ||
Rational::Rational(int numerator, int denominator) | Rational::Rational(int numerator, int denominator) | ||
: numerator_(numerator), | : numerator_(numerator), | ||
denominator_(denominator) | denominator_(denominator) | ||
{ } | { } | ||
void Rational::Print(std::ostream& out) const | void Rational::Print(std::ostream& out) const | ||
{ | { | ||
out << numerator_ << " / " << denominator_; | out << numerator_ << " / " << denominator_; | ||
} | } | ||
std::ostream& operator<<(std::ostream& out, const Rational& r) | std::ostream& operator<<(std::ostream& out, const Rational& r) | ||
{ | { | ||
r.Print(out); | r.Print(out); | ||
return out; | return out; | ||
} | } | ||
void Rational::Set(int numerator, int denominator) | void Rational::Set(int numerator, int denominator) | ||
{ | { | ||
numerator_ = numerator; | numerator_ = numerator; | ||
denominator_ = denominator; | denominator_ = denominator; | ||
} | } | ||
std::istream& operator>>(std::istream& in, Rational& r) | std::istream& operator>>(std::istream& in, Rational& r) | ||
{ | { | ||
int numerator {}, denominator {}; | int numerator {}, denominator {}; | ||
in >> numerator >> denominator; // Notice the operator>> chained call | in >> numerator >> denominator; // Notice the operator>> chained call | ||
if (!in) | if (!in) | ||
{ | { | ||
// If istream is bad; do not modify 'r' | // If istream is bad; do not modify 'r' | ||
return in; | return in; | ||
} | } | ||
r.Set(numerator, denominator); // ok due to friendship! | r.Set(numerator, denominator); // ok due to friendship! | ||
return in; | return in; | ||
} | } | ||
int main() | int main() | ||
{ | { | ||
Rational r1(0,0); | Rational r1(0,0); | ||
std::cout << "Enter a rational by separating calls by a space" << std::endl; | std::cout << "Enter a rational by separating calls by a space" << std::endl; | ||
std::cin >> r1; | std::cin >> r1; | ||
if (std::cin) | if (std::cin) | ||
std::cout << "Value read is " << r1 << std::endl; | std::cout << "Value read is " << r1 << std::endl; | ||
else | else | ||
std::cout << "Invalid input!" << std::endl; | std::cout << "Invalid input!" << std::endl; | ||
return EXIT_SUCCESS; | return EXIT_SUCCESS; | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
This code doesn't run neither in Xeus-cling (due to the operator bug) nor in [Coliru](https://coliru.stacked-crooked.com/) or [Wandbox](https://wandbox.org/) (due to limited `std::cin` support) but you may check it on a local installation. | This code doesn't run neither in Xeus-cling (due to the operator bug) nor in [Coliru](https://coliru.stacked-crooked.com/) or [Wandbox](https://wandbox.org/) (due to limited `std::cin` support) but you may check it on a local installation. | ||
Even then, it is not perfect: it handles correctly the case gibberish is given through `std::cin`, but if you put more than two blocks separated by spaces the first two ones are used and the others aren't dealt with... As I said, proper and complete handling of input stream is though! | Even then, it is not perfect: it handles correctly the case gibberish is given through `std::cin`, but if you put more than two blocks separated by spaces the first two ones are used and the others aren't dealt with... As I said, proper and complete handling of input stream is though! | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
[© Copyright](../COPYRIGHT.md) | [© Copyright](../COPYRIGHT.md) | ||
... | ... |
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
# [Getting started in C++](./) - [Operators](/notebooks/3-Operators/0-main.ipynb) - [Assignment operator and the canonical form of a class](/notebooks/3-Operators/4-CanonicalForm.ipynb) | # [Getting started in C++](./) - [Operators](/notebooks/3-Operators/0-main.ipynb) - [Assignment operator and the canonical form of a class](/notebooks/3-Operators/4-CanonicalForm.ipynb) | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
## Assignment operator | ## Assignment operator | ||
We have not yet addressed one of the most natural operator: the one that might be used to allocate a value to an object. | We have not yet addressed one of the most natural operator: the one that might be used to allocate a value to an object. | ||
### Default behaviour (for a simple case) | ### Default behaviour (for a simple case) | ||
This one is provided by default: | This one is provided by default: | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
#include <iostream> | #include <iostream> | ||
class Vector | class Vector | ||
{ | { | ||
public: | public: | ||
Vector(double x, double y, double z); | Vector(double x, double y, double z); | ||
Vector& operator=(const Vector&) = default; | Vector& operator=(const Vector&) = default; | ||
void Print(std::ostream& out) const; | void Print(std::ostream& out) const; | ||
private: | private: | ||
double x_ = 0.; | double x_ = 0.; | ||
double y_ = 0.; | double y_ = 0.; | ||
double z_ = 0.; | double z_ = 0.; | ||
}; | }; | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
Vector::Vector(double x, double y, double z) | Vector::Vector(double x, double y, double z) | ||
: x_(x), | : x_(x), | ||
y_(y), | y_(y), | ||
z_(z) | z_(z) | ||
{ } | { } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
void Vector::Print(std::ostream& out) const | void Vector::Print(std::ostream& out) const | ||
{ | { | ||
out << "(" << x_ << ", " << y_ << ", " << z_ << ")"; | out << "(" << x_ << ", " << y_ << ", " << z_ << ")"; | ||
} | } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
{ | { | ||
Vector v1(3., 5., 7.); | Vector v1(3., 5., 7.); | ||
Vector v2(-4., -16., 0.); | Vector v2(-4., -16., 0.); | ||
v2 = v1; | v2 = v1; | ||
v2.Print(std::cout); | v2.Print(std::cout); | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
### The pointer case | ### The pointer case | ||
So end of the story? Not exactly... Let's write the same and store the values in a dynamic array instead (of course you shouldn't do that in a real case: `std::vector` or `std::array` would avoid the hassle below!): | So end of the story? Not exactly... Let's write the same and store the values in a dynamic array instead (of course you shouldn't do that in a real case: `std::vector` or `std::array` would avoid the hassle below!): | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
#include <iostream> | #include <iostream> | ||
class Vector2 | class Vector2 | ||
{ | { | ||
public: | public: | ||
Vector2(double x, double y, double z); | Vector2(double x, double y, double z); | ||
~Vector2(); | ~Vector2(); | ||
void Print(std::ostream& out) const; | void Print(std::ostream& out) const; | ||
private: | private: | ||
double* array_ = nullptr; | double* array_ = nullptr; | ||
}; | }; | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
Vector2::Vector2(double x, double y, double z) | Vector2::Vector2(double x, double y, double z) | ||
{ | { | ||
array_ = new double[3]; | array_ = new double[3]; | ||
array_[0] = x; | array_[0] = x; | ||
array_[1] = y; | array_[1] = y; | ||
array_[2] = z; | array_[2] = z; | ||
} | } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
Vector2::~Vector2() | Vector2::~Vector2() | ||
{ | { | ||
delete[] array_; | delete[] array_; | ||
} | } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
void Vector2::Print(std::ostream& out) const | void Vector2::Print(std::ostream& out) const | ||
{ | { | ||
out << "(" << array_[0] << ", " << array_[1] << ", " << array_[2] << ")"; | out << "(" << array_[0] << ", " << array_[1] << ", " << array_[2] << ")"; | ||
} | } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
{ | { | ||
// Dynamic allocation here just to be able to make our point due to the explicit call of destructor with `delete` | // Dynamic allocation here just to be able to make our point due to the explicit call of destructor with `delete` | ||
Vector2* v1 = new Vector2(3., 5., 7.); | Vector2* v1 = new Vector2(3., 5., 7.); | ||
Vector2* v2 = new Vector2(-4., -16., 0.); | Vector2* v2 = new Vector2(-4., -16., 0.); | ||
v2 = v1; | v2 = v1; | ||
std::cout << "Delete v1 @ " << std::hex << v1 << std::endl; | std::cout << "Delete v1 @ " << std::hex << v1 << std::endl; | ||
delete v1; | delete v1; | ||
std::cout << "Delete v2 @ " << std::hex << v2 << std::endl; | std::cout << "Delete v2 @ " << std::hex << v2 << std::endl; | ||
delete v2; // WARNING: Comment this line for not kernel to crash | delete v2; // WARNING: Comment this line for not kernel to crash | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
At the time of this writing, this makes the kernel crash... In a more advanced environment ([Wandbox](https://wandbox.org) for instance) the reason appears more clearly: | At the time of this writing, this makes the kernel crash... In a more advanced environment ([Wandbox](https://wandbox.org) for instance) the reason appears more clearly: | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
```txt | ```txt | ||
** Error in `./prog.exe': double free or corruption (fasttop): 0x0000000001532da0 ** | ** Error in `./prog.exe': double free or corruption (fasttop): 0x0000000001532da0 ** | ||
``` | ``` | ||
So what's the deal? The default operator copies all the data attributes from `v1` to `v2`... which here amounts only to the `array_` pointer. But it really copies that: the pointer itself, _not_ the data pointed by this pointer. So in fine v1 and v2 points to the same area of memory, and the issue is that we attempt to free the same memory twice. | So what's the deal? The default operator copies all the data attributes from `v1` to `v2`... which here amounts only to the `array_` pointer. But it really copies that: the pointer itself, _not_ the data pointed by this pointer. So in fine v1 and v2 points to the same area of memory, and the issue is that we attempt to free the same memory twice. | ||
One way to solve this is not to use a dynamic array - for instance you would be cool with a `std::vector`. But we can do so properly by hand as well (in practice don't - stick with `std::vector`!): | One way to solve this is not to use a dynamic array - for instance you would be cool with a `std::vector`. But we can do so properly by hand as well (in practice don't - stick with `std::vector`!): | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
#include <iostream> | #include <iostream> | ||
class Vector3 | class Vector3 | ||
{ | { | ||
public: | public: | ||
Vector3(double x, double y, double z); | Vector3(double x, double y, double z); | ||
~Vector3(); | ~Vector3(); | ||
// Then again the Xeus-cling issue with out of class operator definition. | // Then again the Xeus-cling issue with out of class operator definition. | ||
Vector3& operator=(const Vector3& rhs) | Vector3& operator=(const Vector3& rhs) | ||
{ | { | ||
// Array already initialized in constructor; just change its content. | // Array already initialized in constructor; just change its content. | ||
for (auto i = 0ul; i < 3ul; ++i) | for (auto i = 0ul; i < 3ul; ++i) | ||
array_[i] = rhs.array_[i]; | array_[i] = rhs.array_[i]; | ||
return *this; // The (logical) return value for such a method. | return *this; // The (logical) return value for such a method. | ||
} | } | ||
void Print(std::ostream& out) const; | void Print(std::ostream& out) const; | ||
private: | private: | ||
double* array_ = nullptr; | double* array_ = nullptr; | ||
}; | }; | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
Vector3::Vector3(double x, double y, double z) | Vector3::Vector3(double x, double y, double z) | ||
{ | { | ||
array_ = new double[3]; | array_ = new double[3]; | ||
array_[0] = x; | array_[0] = x; | ||
array_[1] = y; | array_[1] = y; | ||
array_[2] = z; | array_[2] = z; | ||
} | } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
Vector3::~Vector3() | Vector3::~Vector3() | ||
{ | { | ||
delete[] array_; | delete[] array_; | ||
} | } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
void Vector3::Print(std::ostream& out) const | void Vector3::Print(std::ostream& out) const | ||
{ | { | ||
out << "(" << array_[0] << ", " << array_[1] << ", " << array_[2] << ")"; | out << "(" << array_[0] << ", " << array_[1] << ", " << array_[2] << ")"; | ||
} | } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
{ | { | ||
Vector3 v1(3., 5., 7.); | Vector3 v1(3., 5., 7.); | ||
Vector3 v2(-4., -16., 0.); | Vector3 v2(-4., -16., 0.); | ||
v2 = v1; | v2 = v1; | ||
v1.Print(std::cout); | v1.Print(std::cout); | ||
std::cout << std::endl; | std::cout << std::endl; | ||
v2.Print(std::cout); | v2.Print(std::cout); | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
### Uncopyable class | ### Uncopyable class | ||
In fact when I said by default an assignment operator is made available for the class, I was overly simplifying the issue. Let's consider for instance a class with a reference data attribute: | In fact when I said by default an assignment operator is made available for the class, I was overly simplifying the issue. Let's consider for instance a class with a reference data attribute: | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
class ClassWithRef | class ClassWithRef | ||
{ | { | ||
public: | public: | ||
ClassWithRef(int& index); | ClassWithRef(int& index); | ||
private: | private: | ||
int& index_; | int& index_; | ||
}; | }; | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
ClassWithRef::ClassWithRef(int& index) | ClassWithRef::ClassWithRef(int& index) | ||
: index_(index) | : index_(index) | ||
{ } | { } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
{ | { | ||
int a = 5; | int a = 5; | ||
ClassWithRef obj(a); | ClassWithRef obj(a); | ||
} | } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
{ | { | ||
int a = 5; | int a = 5; | ||
int b = 7; | int b = 7; | ||
ClassWithRef obj1(a); | ClassWithRef obj1(a); | ||
ClassWithRef obj2(b); | ClassWithRef obj2(b); | ||
obj2 = obj1; // COMPILATION ERROR | obj2 = obj1; // COMPILATION ERROR | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
A class with a reference can't be copied: the reference is to be set at construction, and be left unchanged later. So a copy is out of the question, hence the compilation error. | A class with a reference can't be copied: the reference is to be set at construction, and be left unchanged later. So a copy is out of the question, hence the compilation error. | ||
The same is true as well for a class with an uncopyable data attribute: the class is then automatically uncopyable as well. | The same is true as well for a class with an uncopyable data attribute: the class is then automatically uncopyable as well. | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
### Copy constructor | ### Copy constructor | ||
Please notice however that it is still possible to allocate a new object which would be a copy of another one with a **copy constructor**. The syntax is given below: | Please notice however that it is still possible to allocate a new object which would be a copy of another one with a **copy constructor**. The syntax is given below: | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
class ClassWithRef2 | class ClassWithRef2 | ||
{ | { | ||
public: | public: | ||
ClassWithRef2(int& index); | ClassWithRef2(int& index); | ||
ClassWithRef2(const ClassWithRef2& ) = default; | ClassWithRef2(const ClassWithRef2& ) = default; | ||
private: | private: | ||
int& index_; | int& index_; | ||
}; | }; | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
ClassWithRef2::ClassWithRef2(int& index) | ClassWithRef2::ClassWithRef2(int& index) | ||
: index_(index) | : index_(index) | ||
{ } | { } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
{ | { | ||
int a = 5; | int a = 5; | ||
ClassWithRef2 obj1(a); | ClassWithRef2 obj1(a); | ||
ClassWithRef2 obj2(obj1); // ok | ClassWithRef2 obj2(obj1); // ok | ||
ClassWithRef2 obj3 {obj1}; // ok | ClassWithRef2 obj3 {obj1}; // ok | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
There are effectively two ways to copy an object: | There are effectively two ways to copy an object: | ||
* With an assignment operator. | * With an assignment operator. | ||
* With a copy constructor. | * With a copy constructor. | ||
It is NOT the same underlying method which is called under the hood, you might have a different behaviour depending on which one is called or not! | It is NOT the same underlying method which is called under the hood, you might have a different behaviour depending on which one is called or not! | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
### The dangers of copy constructions... and how I avoid them | ### The dangers of copy constructions... and how I avoid them | ||
Copy construction may in fact be quite dangerous: | Copy construction may in fact be quite dangerous: | ||
* As we've just seen, assignment and construction may differ in implementation, which is not a good thing. There are ways to define one in function of the other (see for instance item 11 of [Effective C++](../bibliography.ipynb#Effective-C++-/-More-Effective-C++)) but they aren't that trivial. | * As we've just seen, assignment and construction may differ in implementation, which is not a good thing. There are ways to define one in function of the other (see for instance item 11 of [Effective C++](../bibliography.ipynb#Effective-C++-/-More-Effective-C++)) but they aren't that trivial. | ||
* Depending on somewhat complicated rules that have evolved with standard versions, some might or might not be defined implicitly. | * Depending on somewhat complicated rules that have evolved with standard versions, some might or might not be defined implicitly. | ||
* More importantly, assignment operators may be a nightmare to maintain. Imagine you have a class for which you overload manually the assignment operator and/or the copy constructor. If later you add a new data attribute, you have to make sure not to forget to add it in both implementations; if you forget once you will enter the realm of undefined behaviour... and good luck for you to find the origin of the bug! | * More importantly, assignment operators may be a nightmare to maintain. Imagine you have a class for which you overload manually the assignment operator and/or the copy constructor. If later you add a new data attribute, you have to make sure not to forget to add it in both implementations; if you forget once you will enter the realm of undefined behaviour... and good luck for you to find the origin of the bug! | ||
To avoid that I took the extreme rule to (almost) never overload those myself: | To avoid that I took the extreme rule to (almost) never overload those myself: | ||
* As explained briefly above, using appropriate container may remove the need. `Vector3` could be written with a `std::array` instead of the dynamic array, and the STL object will be properly copied with default behaviour! | * As explained briefly above, using appropriate container may remove the need. `Vector3` could be written with a `std::array` instead of the dynamic array, and the STL object will be properly copied with default behaviour! | ||
* I define explicitly in my classes the behaviour of these operators with `= default` or `= delete` syntaxes. More often than not, my objects have no business being copied and `= delete` is really my default choice (this keyword indicates to the compiler the operator should not be provided for the class). | * I define explicitly in my classes the behaviour of these operators with `= default` or `= delete` syntaxes. More often than not, my objects have no business being copied and `= delete` is really my default choice (this keyword indicates to the compiler the operator should not be provided for the class). | ||
I don't pretend it is the universal choice, just my way to avoid the potential issues with manually overloaded copy operators (some would say the [Open-closed principle](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle) would avoid the most problematic one, but in the real world I find it difficult to stick with this principle...) | I don't pretend it is the universal choice, just my way to avoid the potential issues with manually overloaded copy operators (some would say the [Open-closed principle](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle) would avoid the most problematic one, but in the real world I find it difficult to stick with this principle...) | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
## Canonical form of a class | ## Canonical form of a class | ||
So a typical class of mine looks like: | So a typical class of mine looks like: | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
class AlmostCanonicalClass | class AlmostCanonicalClass | ||
{ | { | ||
public: // or even protected or private for some of them! | public: // or even protected or private for some of them! | ||
//! Constructor. | //! Constructor. | ||
AlmostCanonicalClass(...); | AlmostCanonicalClass(...); | ||
//! Destructor. | //! Destructor. | ||
~AlmostCanonicalClass() = default; | ~AlmostCanonicalClass() = default; | ||
//! Disable copy constructor. | //! Disable copy constructor. | ||
AlmostCanonicalClass(const AlmostCanonicalClass& ) = delete; | AlmostCanonicalClass(const AlmostCanonicalClass& ) = delete; | ||
//! Disable copy assignment. | //! Disable copy assignment. | ||
AlmostCanonicalClass& operator=(const AlmostCanonicalClass& ) = delete; | AlmostCanonicalClass& operator=(const AlmostCanonicalClass& ) = delete; | ||
}; | }; | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
Your IDE or a script might even be handy to generate this by default (with additional Doxygen comments for good measure). | Your IDE or a script might even be handy to generate this by default (with additional Doxygen comments for good measure). | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
### [Advanced] The true canonical class | ### [Advanced] The true canonical class | ||
Why _almost_ canonical class? Because C++ 11 introduced a very powerful **move semantics** (see the [notebook](/notebooks/5-UsefulConceptsAndSTL/5-MoveSemantics.ipynb) about it) and so the true canonical class is: | Why _almost_ canonical class? Because C++ 11 introduced a very powerful **move semantics** (see the [notebook](/notebooks/5-UsefulConceptsAndSTL/5-MoveSemantics.ipynb) about it) and so the true canonical class is: | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
class TrueCanonicalClass | class TrueCanonicalClass | ||
{ | { | ||
public: // or even protected or private for some of them! | public: // or even protected or private for some of them! | ||
//! Constructor. | //! Constructor. | ||
TrueCanonicalClass(...); | TrueCanonicalClass(...); | ||
//! Destructor. | //! Destructor. | ||
~TrueCanonicalClass() = default; | ~TrueCanonicalClass() = default; | ||
//! Disable copy constructor. | //! Disable copy constructor. | ||
TrueCanonicalClass(const TrueCanonicalClass& ) = delete; | TrueCanonicalClass(const TrueCanonicalClass& ) = delete; | ||
//! Disable copy assignment. | //! Disable copy assignment. | ||
TrueCanonicalClass& operator=(const TrueCanonicalClass& ) = delete; | TrueCanonicalClass& operator=(const TrueCanonicalClass& ) = delete; | ||
//! Disable move constructor. | //! Disable move constructor. | ||
TrueCanonicalClass(TrueCanonicalClass&& ) = delete; | TrueCanonicalClass(TrueCanonicalClass&& ) = delete; | ||
//! Disable move assignment. | //! Disable move assignment. | ||
TrueCanonicalClass& operator=(TrueCanonicalClass&& ) = delete; | TrueCanonicalClass& operator=(TrueCanonicalClass&& ) = delete; | ||
}; | }; | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
In my programs, I like to declare explicitly all of them, using `default` and `delete` to provide automatic implementation for most of them. | In my programs, I like to declare explicitly all of them, using `default` and `delete` to provide automatic implementation for most of them. | ||
I admit it's a bit of boilerplate (and to be honest a script does the job for me in my project...), if you don't want to there are in fact rules that specify which of them you need to define: for instance if a class requires a user-defined destructor, a user-defined copy constructor, or a user-defined copy assignment operator, it almost certainly requires all three. See [this cppreference link](https://en.cppreference.com/w/cpp/language/rule_of_three) for more about these rules and [this blog post](https://www.fluentcpp.com/2019/04/23/the-rule-of-zero-zero-constructor-zero-calorie/) for an opposite point of view. | I admit it's a bit of boilerplate (and to be honest a script does the job for me in my project...), if you don't want to there are in fact rules that specify which of them you need to define: for instance if a class requires a user-defined destructor, a user-defined copy constructor, or a user-defined copy assignment operator, it almost certainly requires all three. See [this cppreference link](https://en.cppreference.com/w/cpp/language/rule_of_three) for more about these rules and [this blog post](https://www.fluentcpp.com/2019/04/23/the-rule-of-zero-zero-constructor-zero-calorie/) for an opposite point of view. | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
[© Copyright](../COPYRIGHT.md) | [© Copyright](../COPYRIGHT.md) | ||
... | ... |
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
# [Getting started in C++](./) - [Operators](./0-main.ipynb) - [Functors](./5-Functors.ipynb) | # [Getting started in C++](./) - [Operators](./0-main.ipynb) - [Functors](./5-Functors.ipynb) | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
## What is a functor? | ## What is a functor? | ||
### Disclaimer: not a functional programming functor! | ### Disclaimer: not a functional programming functor! | ||
First of call, for those of you with a background in functional programming, what C++ calls a `functor` is [not what you are used to](https://en.wikipedia.org/wiki/Functor_(functional_programming)). | First of call, for those of you with a background in functional programming, what C++ calls a `functor` is [not what you are used to](https://en.wikipedia.org/wiki/Functor_(functional_programming)). | ||
### Functor in C++ | ### Functor in C++ | ||
We can provide a class with an `operator()` that allows to give its objects a function behavior. In other words: if you write an expression in which an object is used as if it were a function, it is its execution operator that is invoked. | We can provide a class with an `operator()` that allows to give its objects a function behavior. In other words: if you write an expression in which an object is used as if it were a function, it is its execution operator that is invoked. | ||
We can also see these *functional objects*, or *functors*, as functions to which we would have added state parameters from one call to another. | We can also see these *functional objects*, or *functors*, as functions to which we would have added state parameters from one call to another. | ||
The return type and the arguments are not constrained and might be defined as you wish in the class (`int` is used here for return type, and an `int` is required as argument): | The return type and the arguments are not constrained and might be defined as you wish in the class (`int` is used here for return type, and an `int` is required as argument): | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
class LinearFunction | class LinearFunction | ||
{ | { | ||
public : | public : | ||
LinearFunction(int constant); | LinearFunction(int constant); | ||
int operator()(int value) | int operator()(int value) | ||
{ | { | ||
return constant_ * value; // here due to usual Xeus-cling issue when out of class | return constant_ * value; // here due to usual Xeus-cling issue when out of class | ||
// definition is used for operators | // definition is used for operators | ||
} | } | ||
private : | private : | ||
int constant_ = 0.; | int constant_ = 0.; | ||
} ; | } ; | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
LinearFunction::LinearFunction(int constant) | LinearFunction::LinearFunction(int constant) | ||
: constant_(constant) | : constant_(constant) | ||
{ } | { } | ||
``` | ``` | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
#include <iostream> | #include <iostream> | ||
{ | { | ||
int values[10] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; | int values[10] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; | ||
LinearFunction Double(2); | LinearFunction Double(2); | ||
LinearFunction Triple(3); | LinearFunction Triple(3); | ||
for (int value : values) | for (int value : values) | ||
std::cout << value << "\t" << Double(value) << "\t" << Triple(value) << std::endl; | std::cout << value << "\t" << Double(value) << "\t" << Triple(value) << std::endl; | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
This might seem inconsequential, but they are sometimes extremely useful. | This might seem inconsequential, but they are sometimes extremely useful. | ||
## Functors in STL | ## Functors in STL | ||
STL itself defines some functors: imagine you want to sort a `std::vector` decreasingly; you can't directly put `>` as the comparison operation in `sort` and therefore may use a specific STL-defined `greater`: | STL itself defines some functors: imagine you want to sort a `std::vector` decreasingly; you can't directly put `>` as the comparison operation in `sort` and therefore may use a specific STL-defined `greater`: | ||
%% Cell type:code id: tags: | %% Cell type:code id: tags: | ||
``` C++17 | ``` c++ | ||
#include <algorithm> | #include <algorithm> | ||
#include <iostream> | #include <iostream> | ||
#include <vector> | #include <vector> | ||
{ | { | ||
std::vector<int> values { -980, 12, 2987, -8323, 4, 275, -8936, 27, -988, 92784 }; | std::vector<int> values { -980, 12, 2987, -8323, 4, 275, -8936, 27, -988, 92784 }; | ||
std::sort(values.begin(), values.end(), std::greater<int>()); | std::sort(values.begin(), values.end(), std::greater<int>()); | ||
for (auto value : values) | for (auto value : values) | ||
std::cout << value << " "; | std::cout << value << " "; | ||
} | } | ||
``` | ``` | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
C++ 11 and above limits the need for functors with lambda functions (see [earlier](../1-ProceduralProgramming/4-Functions.ipynb#Lambda-functions)), but in older codes, functors were one of the way to pass your own rules to some STL algorithms, along with pointer to functions. | C++ 11 and above limits the need for functors with lambda functions (see [earlier](../1-ProceduralProgramming/4-Functions.ipynb#Lambda-functions)), but in older codes, functors were one of the way to pass your own rules to some STL algorithms, along with pointer to functions. | ||
%% Cell type:markdown id: tags: | %% Cell type:markdown id: tags: | ||
[© Copyright](../COPYRIGHT.md) | [© Copyright](../COPYRIGHT.md) | ||
... | ... |
Please register or sign in to comment