Mentions légales du service

Skip to content
Snippets Groups Projects
Commit 5cd5c3f5 authored by GILLES Sebastien's avatar GILLES Sebastien
Browse files

#101 Notebook intro to operator: replace Coliru and comments that Xeus cling...

#101 Notebook intro to operator: replace Coliru and comments that Xeus cling doesn't work by working cells.
parent a5d8e126
No related branches found
No related tags found
1 merge request!114Use CppyyKernel to run the notebooks instead of Xeus-cling.
%% Cell type:markdown id: tags:
# [Getting started in C++](./) - [Operators](./0-main.ipynb) - [Introduction](./1-Intro.ipynb)
%% Cell type:markdown id: tags:
## 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:
%% Cell type:code id: tags:
``` c++
class Vector
{
// Friendship because `Add()` needs to access private members and no accessors were defined.
friend Vector Add(const Vector& v1, const Vector& v2);
public :
Vector(double x, double y, double z);
Vector() = default;
void Print() const;
private :
double x_ = 0.;
double y_ = 0.;
double z_ = 0.;
};
```
%% Cell type:code id: tags:
``` c++
Vector::Vector(double x, double y, double z)
: x_(x),
y_(y),
z_(z)
{ }
```
%% Cell type:code id: tags:
``` c++
Vector Add(const Vector& v1, const Vector& v2)
{
Vector ret;
ret.x_ = v1.x_ + v2.x_;
ret.y_ = v1.y_ + v2.y_;
ret.z_ = v1.z_ + v2.z_;
return ret;
}
```
%% Cell type:code id: tags:
``` c++
#include <iostream>
void Vector::Print() const
{
std::cout << "(" << x_ << ", " << y_ << ", " << z_ << ")" << std::endl;
}
```
%% Cell type:code id: tags:
``` c++
{
Vector v1(3., 5., 7.);
Vector v2(7., 5., 3.);
Vector v3 = Add(v1, v2);
v3.Print();
}
```
%% Cell type:markdown id: tags:
Now the same with a _plain old data type_ is much more natural to write with no (apparent) method:
%% Cell type:code id: tags:
``` c++
{
double x1 = 3.;
double x2 = 7.;
double x3 = x1 + x2;
std::cout << x3 << std::endl;
}
```
%% 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...
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
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:
%% Cell type:code id: tags:
``` c++
// DOESN'T RUN WITH XEUS-CLING
%%cpptoolbox cppyy/cppdef
// < We need to help the kernel interpret properly the code below, which is perfectly valid C++. Don't bother about this magics!
#include <iostream>
class VectorPlus
{
public :
VectorPlus(double x, double y, double z);
VectorPlus() = default;
void Print() const;
// Friendship as free function operator+ wouldn't otherwise be allowed to access data attributes.
friend VectorPlus operator+(const VectorPlus& v1, const VectorPlus& v2);
private :
double x_ = 0.;
double y_ = 0.;
double z_ = 0.;
};
VectorPlus::VectorPlus(double x, double y, double z)
: x_(x),
y_(y),
z_(z)
{ }
void VectorPlus::Print() const
{
std::cout << "(" << x_ << ", " << y_ << ", " << z_ << ")" << std::endl;
}
VectorPlus operator+(const VectorPlus& v1, const VectorPlus& v2)
{
// Provides a symmetric implementation of operator +: both vectors are at the same level!
VectorPlus ret;
ret.x_ = v1.x_ + v2.x_;
ret.y_ = v1.y_ + v2.y_;
ret.z_ = v1.z_ + v2.z_;
return ret;
}
```
%% Cell type:code id: tags:
int main(int argc, char** argv)
{
VectorPlus v1(3., 5., 7.);
VectorPlus v2(7., 5., 3.);
VectorPlus v3 = v1 + v2; // Nicer syntax!
v3.Print();
``` c++
VectorPlus v1(3., 5., 7.);
VectorPlus v2(7., 5., 3.);
VectorPlus v4 = operator+(v1, v2); // but "usual" method syntax is possible as well
v4.Print();
VectorPlus v3 = v1 + v2; // Nicer syntax!
v3.Print();
return EXIT_SUCCESS;
}
VectorPlus v4 = operator+(v1, v2); // but "usual" method syntax is possible as well
v4.Print();
```
%% 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:
%% Cell type:code id: tags:
``` c++
%%cpptoolbox cppyy/cppdef
class VectorPlusAsMethod
{
public :
VectorPlusAsMethod(double x, double y, double z);
VectorPlusAsMethod() = default;
void Print() const;
// I would rather put the definition outside but Xeus-cling doesn't seem to accept this.
VectorPlusAsMethod operator+(const VectorPlusAsMethod& v) const
{
VectorPlusAsMethod ret;
ret.x_ = x_ + v.x_;
ret.y_ = y_ + v.y_;
ret.z_ = z_ + v.z_;
return ret;
}
VectorPlusAsMethod operator+(const VectorPlusAsMethod& v) const;
private :
double x_ = 0.;
double y_ = 0.;
double z_ = 0.;
double x_ {};
double y_ {};
double z_ {};
};
```
%% Cell type:code id: tags:
``` c++
%%cpptoolbox cppyy/cppdef
VectorPlusAsMethod::VectorPlusAsMethod(double x, double y, double z)
: x_(x),
y_(y),
z_(z)
: x_{x},
y_{y},
z_{z}
{ }
```
%% Cell type:code id: tags:
``` c++
#include <iostream>
void VectorPlusAsMethod::Print() const
{
std::cout << "(" << x_ << ", " << y_ << ", " << z_ << ")" << std::endl;
}
```
%% Cell type:code id: tags:
``` c++
VectorPlusAsMethod VectorPlusAsMethod::operator+(const VectorPlusAsMethod& v) const
{
VectorPlusAsMethod ret;
ret.x_ = x_ + v.x_;
ret.y_ = y_ + v.y_;
ret.z_ = z_ + v.z_;
return ret;
}
```
%% Cell type:code id: tags:
``` c++
VectorPlusAsMethod v1(3., 5., 7.);
VectorPlusAsMethod v2(7., 5., 3.);
VectorPlusAsMethod v3 = v1 + v2;
v3.Print();
VectorPlusAsMethod v4 = v1.operator+(v2); // but "usual" method syntax is possible as well
v4.Print();
}
```
%% 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.
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:
## Operator between different types
It is also possible to define an operator which acts upon two objects of different nature:
%% Cell type:code id: tags:
``` c++
%%cpptoolbox cppyy/cppdef
class VectorPlusDouble
{
public :
VectorPlusDouble(double x, double y, double z);
VectorPlusDouble() = default;
void Print() const;
// Defined in the class declaration due to Xeus-cling limitation.
VectorPlusDouble operator+(double value) const
{
VectorPlusDouble ret;
ret.x_ = x_ + value;
ret.y_ = y_ + value;
ret.z_ = z_ + value;
return ret;
}
VectorPlusDouble operator+(double value) const;
private :
double x_ = 0.;
double y_ = 0.;
double z_ = 0.;
double x_ {};
double y_ {};
double z_ {};
};
```
%% Cell type:code id: tags:
``` c++
#include <iostream>
void VectorPlusDouble::Print() const
{
std::cout << "(" << x_ << ", " << y_ << ", " << z_ << ")" << std::endl;
}
```
%% Cell type:code id: tags:
``` c++
%%cpptoolbox cppyy/cppdef
VectorPlusDouble::VectorPlusDouble(double x, double y, double z)
: x_(x),
y_(y),
z_(z)
: x_{x},
y_{y},
z_{z}
{ }
```
%% Cell type:code id: tags:
``` c++
VectorPlusDouble VectorPlusDouble::operator+(double value) const
{
VectorPlusDouble ret;
ret.x_ = x_ + value;
ret.y_ = y_ + value;
ret.z_ = z_ + value;
return ret;
}
```
%% Cell type:code id: tags:
``` c++
{
VectorPlusDouble vector(5., 3.2, -1.);
VectorPlusDouble vector_plus_5 = vector + 5.;
vector_plus_5.Print();
}
```
%% Cell type:markdown id: tags:
However, pay attention to the fact this operator is not commutative. It is indeed a shortcut to
%% Cell type:code id: tags:
``` c++
{
VectorPlusDouble vector(5., 3.2, -1.);
VectorPlusDouble vector_plus_5 = vector.operator+(5.);
}
```
%% Cell type:markdown id: tags:
and the following won't compile:
%% Cell type:code id: tags:
``` c++
{
VectorPlusDouble vector(5., 3.2, -1.);
VectorPlusDouble vector_plus_5 = 5. + vector; // COMPILATION ERROR!
}
```
%% 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.
Of course, it is a **good practice** to define one in way of the other:
Of course, it is a **good practice** in this case to define one in way of the other:
%% Cell type:code id: tags:
``` c++
// Won't compile in Xeus-cling
#include <cstdlib>
#include <iostream>
%%cpptoolbox cppyy/cppdef
class VectorPlusDoubleCommutative
{
public :
VectorPlusDoubleCommutative(double x, double y, double z);
VectorPlusDoubleCommutative() = default;
void Print() const;
friend VectorPlusDoubleCommutative operator+(const VectorPlusDoubleCommutative& v, double value);
friend VectorPlusDoubleCommutative operator+(double value, const VectorPlusDoubleCommutative& v);
private :
double x_ = 0.;
double y_ = 0.;
double z_ = 0.;
double x_ {};
double y_ {};
double z_ {};
};
```
%% Cell type:code id: tags:
``` c++
#include <iostream>
void VectorPlusDoubleCommutative::Print() const
{
std::cout << "(" << x_ << ", " << y_ << ", " << z_ << ")" << std::endl;
}
```
%% Cell type:code id: tags:
``` c++
%%cpptoolbox cppyy/cppdef
VectorPlusDoubleCommutative::VectorPlusDoubleCommutative(double x, double y, double z)
: x_(x),
y_(y),
z_(z)
: x_{x},
y_{y},
z_{z}
{ }
```
%% Cell type:code id: tags:
``` c++
VectorPlusDoubleCommutative operator+(const VectorPlusDoubleCommutative& v, double value)
{
VectorPlusDoubleCommutative ret;
ret.x_ = v.x_ + value;
ret.y_ = v.y_ + value;
ret.z_ = v.z_ + value;
return ret;
}
```
%% Cell type:code id: tags:
``` c++
VectorPlusDoubleCommutative operator+(double value, const VectorPlusDoubleCommutative& v)
{
return v + value; // good practice: make it rely upon the other `operator+` defined!
}
```
%% Cell type:code id: tags:
int main(int argc, char** argv)
``` c++
{
VectorPlusDoubleCommutative vector(5., 3.2, -1.);
VectorPlusDoubleCommutative vector_plus_5 = vector + 5.;
VectorPlusDoubleCommutative vector_plus_5_commutated = 5. + vector;
vector_plus_5.Print();
vector_plus_5_commutated.Print();
return EXIT_SUCCESS;
}
```
%% Cell type:markdown id: tags:
## Limitations
You cannot change:
* The number of operators arguments
* 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):
* Arithmetic operators: `+ - * / % ++ --`
* Comparison operators: `== != <= >= < > <=>`
* Logical operators: `! && ||`
* Bitwise operators: `~ & | ^ << >>`
* Assignment operators: `= += -= *= /= %= &= |= ^= <<= >>=`
* Member and pointer operators: `[] * & -> ->*`
* Other operators: `() , "" new new[] delete delete[]`
* 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.
If not defined, some of them exist by default:
```
=
-> ->*
new delete
```
Some can never be redefined:
```
: :: . .* ? ?: sizeof
```
## 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.
%% Cell type:code id: tags:
``` c++
#include <iostream>
class Rational
{
public :
Rational(int numerator, int denominator);
operator int() const;
operator double() const;
private :
int numerator_ { };
int denominator_ { };
};
```
%% Cell type:code id: tags:
``` c++
Rational::Rational(int numerator, int denominator)
: numerator_(numerator),
denominator_(denominator)
{ }
```
%% Cell type:code id: tags:
``` c++
Rational::operator int() const
{
return numerator_ / denominator_;
}
```
%% Cell type:code id: tags:
``` c++
Rational::operator double() const
{
return static_cast<double>(numerator_) / denominator_;
}
```
%% Cell type:code id: tags:
``` c++
#include <iostream>
{
Rational val_r(15, 7);
std::cout << "val as double: " << static_cast<double>(val_r) << std::endl ;
std::cout << "val as integer: " << static_cast<int>(val_r) << std::endl ;
}
```
%% Cell type:markdown id: tags:
As for constructors, you should add the keyword `explicit` in the class declaration to ensure no implicit conversion occurs:
%% Cell type:code id: tags:
``` c++
// Won't run in Xeus-cling, as it's not within the class declaration
// Won't run in the notebook, as it's not declared within the class declaration
explicit operator int() const;
explicit operator double() const;
```
%% Cell type:markdown id: tags:
[© Copyright](../COPYRIGHT.md)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment