Mentions légales du service

Skip to content
Snippets Groups Projects
Commit 6428e4e1 authored by ROUVREAU Vincent's avatar ROUVREAU Vincent Committed by GILLES Sebastien
Browse files

Rework spaceship operator, and some phrasings

parent 8faf9764
No related branches found
No related tags found
No related merge requests found
%% Cell type:markdown id: tags:
# [Getting started in C++](./) - [Operators](./0-main.ipynb) - [Comparison operators](./2-Comparison.ipynb)
%% Cell type:markdown id: tags:
<h1>Table of contents<span class="tocSkip"></span></h1>
<div class="toc"><ul class="toc-item"><li><span><a href="#Operator-<,->,-<=,->=" data-toc-modified-id="Operator-<,->,-<=,->=-1">Operator &lt;, &gt;, &lt;=, &gt;=</a></span></li><li><span><a href="#Operator-==-and-!=" data-toc-modified-id="Operator-==-and-!=-2">Operator == and !=</a></span></li><li><span><a href="#C++-20-refinements" data-toc-modified-id="C++-20-refinements-3">C++ 20 refinements</a></span><ul class="toc-item"><li><span><a href="#Spaceship-operator" data-toc-modified-id="Spaceship-operator-3.1">Spaceship operator</a></span></li><li><span><a href="#Default-behaviour" data-toc-modified-id="Default-behaviour-3.2">Default behaviour</a></span></li></ul></li></ul></div>
%% Cell type:markdown id: tags:
## 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?)
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):
%% Cell type:code id: tags:
``` C++17
// Xeus-cling issue: doesn't compile!
#include <iostream>
class Rational
{
public :
explicit Rational(int numerator, int denominator);
explicit operator double() const;
private :
int numerator_ = 0;
int denominator_ = 0;
};
Rational::Rational(int numerator, int denominator)
: numerator_(numerator),
denominator_(denominator)
{ }
Rational::operator double() const
{
return static_cast<double>(numerator_) / denominator_;
}
bool operator<(const Rational& lhs, const Rational& rhs)
{
return static_cast<double>(lhs) < static_cast<double>(rhs);
}
int main(int argc, char** argv)
{
Rational r1(15, 7);
Rational r2(27, 4);
std::cout << (r1 < r2) << std::endl;
return EXIT_SUCCESS;
}
```
%% Cell type:markdown id: tags:
Defining `<` operator does not automatically defines the others, but all may be defined once `operator<` and `operator!=` are defined:
Defining `operator<` does not automatically defines 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<(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 might define an operator `<` which is not the negation of `>=` that you should do it!
## Operator == and !=
* None is defined by default.
* They are independant 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!
* Make sure you're thought well the result of your comparison:
%% Cell type:code id: tags:
``` C++17
class Vector
{
public:
Vector(int x, int y, int z);
~Vector();
// 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.
bool operator==(const Vector& rhs) const
{
return array_ == rhs.array_; // BUG!
}
private:
int* array_ = nullptr;
};
```
%% Cell type:code id: tags:
``` C++17
Vector::Vector(int x, int y, int z)
{
array_ = new int[3];
array_[0] = x;
array_[1] = y;
array_[2] = z;
}
```
%% Cell type:code id: tags:
``` C++17
Vector::~Vector()
{
delete[] array_;
}
```
%% Cell type:code id: tags:
``` C++17
#include <iostream>
{
Vector v1(3, 4, 5);
Vector v2(3, 4, 5);
std::cout << "Are equal? : " << (v1 == v2) << std::endl;
}
```
%% 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!).
%% Cell type:markdown id: tags:
## C++ 20 refinements
%% Cell type:markdown id: tags:
### 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):
%% Cell type:code id: tags:
``` C++17
// Xeus-cling issue: doesn't compile!
#include <iostream>
#include <cmath>
class Rational
{
public :
explicit Rational(int numerator, int denominator);
explicit operator double() const;
private :
int numerator_ = 0;
int denominator_ = 0;
};
Rational::Rational(int numerator, int denominator)
: numerator_(numerator),
denominator_(denominator)
{ }
Rational::operator double() const
{
return static_cast<double>(numerator_) / static_cast<double>(denominator_);
}
bool operator<(const Rational& lhs, const Rational& rhs)
{
return static_cast<double>(lhs) < static_cast<double>(rhs);
}
bool operator==(const Rational& lhs, const Rational& rhs)
{
return std::fabs(static_cast<double>(lhs) - static_cast<double>(rhs) ) < 1.e-16; // crude...
}
bool operator!=(const Rational& lhs, const Rational& rhs)
{
return !operator==(lhs, rhs);
}
bool operator>(const Rational& lhs, const Rational& rhs)
{
return operator<(rhs, lhs);
}
bool operator>=(const Rational& lhs, const Rational& rhs)
{
return !operator<(lhs, rhs);
}
bool operator<=(const Rational& lhs, const Rational& rhs)
{
return !operator>(lhs, rhs);
}
int main()
{
Rational a(5, 2);
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;
return EXIT_SUCCESS;
}
```
%% Cell type:markdown id: tags:
C++ 20 introduces the so-called spaceship operator, which enables defining more concisely all those operators:
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:
``` C++17
// Xeus-cling issue: doesn't compile! (and C++ 20 not yet supported there anyway)
#include <iostream>
#include <cmath>
class Rational20
{
public :
explicit Rational20(int numerator, int denominator);
explicit operator double() const;
private :
int numerator_ = 0;
int denominator_ = 0;
};
Rational20::Rational20(int numerator, int denominator)
: numerator_(numerator),
denominator_(denominator)
{ }
Rational20::operator double() const
{
return static_cast<double>(numerator_) / static_cast<double>(denominator_);
}
bool operator==(const Rational20& lhs, const Rational20& rhs)
{
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)
{
return static_cast<double>(lhs) <=> static_cast<double>(rhs);
}
int main()
{
Rational20 a(5, 2);
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;
return EXIT_SUCCESS;
}
```
%% Cell type:markdown id: tags:
### Default behaviour
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:
%% Cell type:code id: tags:
``` C++17
struct Toolbox
{
unsigned int Nscrewdriver_;
unsigned int Nhammer_;
unsigned int Nnails_;
};
```
%% Cell type:code id: tags:
``` C++17
// Xeus-cling issue: doesn't compile!
bool operator==(const Toolbox& lhs, const Toolbox& rhs)
{
return lhs.Nscrewdriver_ == rhs.Nscrewdriver_
&& lhs.Nhammer_ == rhs.Nhammer_
&& lhs.Nails_ == rhs.Nnails_;
}
```
%% 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!
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/11c22df77c7065fe)):
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:
``` C++17
#include <iostream>
struct Toolbox
{
bool operator==(const Toolbox&) const = default;
// < apparently only works with the internal declaration for operator==
auto operator<=>(const Toolbox&) const = default;
unsigned int Nscrewdriver_;
unsigned int Nhammer_;
unsigned int Nnails_;
};
int main(int argc, char** argv)
{
Toolbox toolbox1;
toolbox1.Nscrewdriver_ = 5;
toolbox1.Nhammer_ = 4;
toolbox1.Nnails_ = 200;
Toolbox toolbox2;
toolbox2.Nscrewdriver_ = 5;
toolbox2.Nhammer_ = 4;
toolbox2.Nnails_ = 200;
std::cout << std::boolalpha << (toolbox1 == toolbox2) << std::endl;
std::cout << std::boolalpha << (toolbox1 < toolbox2) << std::endl;
return EXIT_SUCCESS;
}
```
%% Cell type:markdown id: tags:
[Examples](https://www.modernescpp.com/index.php/c-20-more-details-to-the-spaceship-operator) on [the](https://devblogs.microsoft.com/cppblog/simplify-your-code-with-rocket-science-c20s-spaceship-operator/) Web often show the defaulting of a comparison used directly upon the spaceship operator, which is something I will have to investigate when I upgrade to C++ 20 as it seems at first sight rather dangerous to me (see the `Rational` example, where even the default `operator==` is unlikely to be what we want).
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:
``` C++17
bool operator<(const Rational& lhs, const Rational& rhs)
{
if !(toolbox1.Nscrewdriver_ == toolbox2.Nscrewdriver_)
return toolbox1.Nscrewdriver_ < toolbox2.Nscrewdriver_;
if !(toolbox1.Nhammer_ == toolbox2.Nhammer_)
return toolbox1.Nhammer_ < toolbox2.Nhammer_;
if !(toolbox1.Nnails_ == toolbox2.Nnails_)
return toolbox1.Nnails_ < toolbox2.Nnails_;
return false;
}
```
%% 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.
**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:
© _CNRS 2016_ - _Inria 2018-2021_
_This notebook is an adaptation of a lecture prepared by David Chamont (CNRS) under the terms of the licence [Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)](http://creativecommons.org/licenses/by-nc-sa/4.0/)_
_The present version has been written by Sébastien Gilles and Vincent Rouvreau (Inria)_
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment