Mentions légales du service

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

Merge branch '67_improve_example' into 'master'

#67 Improve example in canonical form notebook, following discussion from the...

See merge request !60
parents 92cf4c52 49ea9a1e
Branches
No related tags found
1 merge request!60#67 Improve example in canonical form notebook, following discussion from the...
%% Cell type:markdown id: tags:
# [Getting started in C++](./) - [Operators](/notebooks/3-Operators/0-main.ipynb) - [Affectation operator and the canonical form of a class](/notebooks/3-Operators/4-CanonicalForm.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="#Affectation-operator" data-toc-modified-id="Affectation-operator-1">Affectation operator</a></span><ul class="toc-item"><li><span><a href="#Default-behaviour-(for-a-simple-case)" data-toc-modified-id="Default-behaviour-(for-a-simple-case)-1.1">Default behaviour (for a simple case)</a></span></li><li><span><a href="#The-pointer-case" data-toc-modified-id="The-pointer-case-1.2">The pointer case</a></span></li><li><span><a href="#Uncopyable-class" data-toc-modified-id="Uncopyable-class-1.3">Uncopyable class</a></span></li><li><span><a href="#Copy-constructor" data-toc-modified-id="Copy-constructor-1.4">Copy constructor</a></span></li><li><span><a href="#The-dangers-of-copy-constructions...-and-how-I-avoid-them" data-toc-modified-id="The-dangers-of-copy-constructions...-and-how-I-avoid-them-1.5">The dangers of copy constructions... and how I avoid them</a></span></li></ul></li><li><span><a href="#Canonical-form-of-a-class" data-toc-modified-id="Canonical-form-of-a-class-2">Canonical form of a class</a></span><ul class="toc-item"><li><span><a href="#[Advanced]-The-true-canonical-class" data-toc-modified-id="[Advanced]-The-true-canonical-class-2.1">[Advanced] The true canonical class</a></span></li></ul></li></ul></div>
%% Cell type:markdown id: tags:
## Affectation 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.
### Default behaviour (for a simple case)
This one is provided by default:
%% Cell type:code id: tags:
``` C++17
#include <iostream>
class Vector
{
public:
Vector(double x, double y, double z);
Vector& operator=(const Vector&) = default;
void Print(std::ostream& out) const;
private:
double x_ = 0.;
double y_ = 0.;
double z_ = 0.;
};
```
%% Cell type:code id: tags:
``` C++17
Vector::Vector(double x, double y, double z)
: x_(x),
y_(y),
z_(z)
{ }
```
%% Cell type:code id: tags:
``` C++17
void Vector::Print(std::ostream& out) const
{
out << "(" << x_ << ", " << y_ << ", " << z_ << ")";
}
```
%% Cell type:code id: tags:
``` C++17
{
Vector v1(3., 5., 7.);
Vector v2(-4., -16., 0.);
v2 = v1;
v2.Print(std::cout);
}
```
%% Cell type:markdown id: tags:
### 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!):
%% Cell type:code id: tags:
``` C++17
#include <iostream>
class Vector2
{
public:
Vector2(double x, double y, double z);
~Vector2();
void Print(std::ostream& out) const;
private:
double* array_ = nullptr;
};
```
%% Cell type:code id: tags:
``` C++17
Vector2::Vector2(double x, double y, double z)
{
array_ = new double[3];
array_[0] = x;
array_[1] = y;
array_[2] = z;
}
```
%% Cell type:code id: tags:
``` C++17
Vector2::~Vector2()
{
delete[] array_;
}
```
%% Cell type:code id: tags:
``` C++17
void Vector2::Print(std::ostream& out) const
{
out << "(" << array_[0] << ", " << array_[1] << ", " << array_[2] << ")";
}
```
%% Cell type:code id: tags:
``` C++17
{
Vector2 v1(3., 5., 7.);
Vector2 v2(-4., -16., 0.);
// 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* v2 = new Vector2(-4., -16., 0.);
v2 = v1; // Kernel crash?
v2 = v1;
std::cout << "Copy done" << std::endl;
delete v1;
std::cout << "v1 deleted" << std::endl;
// delete v2; // Uncommenting this line leads to a kernel crash...
}
```
%% 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:
%% Cell type:markdown id: tags:
```txt
** 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.
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:
``` C++17
#include <iostream>
class Vector3
{
public:
Vector3(double x, double y, double z);
~Vector3();
// Then again the Xeus-cling issue with out of class operator definition.
Vector3& operator=(const Vector3& rhs)
{
// Array already initialized in constructor; just change its content.
for (auto i = 0ul; i < 3ul; ++i)
array_[i] = rhs.array_[i];
return *this; // The (logical) return value for such a method.
}
void Print(std::ostream& out) const;
private:
double* array_ = nullptr;
};
```
%% Cell type:code id: tags:
``` C++17
Vector3::Vector3(double x, double y, double z)
{
array_ = new double[3];
array_[0] = x;
array_[1] = y;
array_[2] = z;
}
```
%% Cell type:code id: tags:
``` C++17
Vector3::~Vector3()
{
delete[] array_;
}
```
%% Cell type:code id: tags:
``` C++17
void Vector3::Print(std::ostream& out) const
{
out << "(" << array_[0] << ", " << array_[1] << ", " << array_[2] << ")";
}
```
%% Cell type:code id: tags:
``` C++17
{
Vector3 v1(3., 5., 7.);
Vector3 v2(-4., -16., 0.);
v2 = v1;
v1.Print(std::cout);
std::cout << std::endl;
v2.Print(std::cout);
}
```
%% Cell type:markdown id: tags:
### Uncopyable class
In fact when I said by default an affectation 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:
``` C++17
class ClassWithRef
{
public:
ClassWithRef(int& index);
private:
int& index_;
};
```
%% Cell type:code id: tags:
``` C++17
ClassWithRef::ClassWithRef(int& index)
: index_(index)
{ }
```
%% Cell type:code id: tags:
``` C++17
{
int a = 5;
ClassWithRef obj(a);
}
```
%% Cell type:code id: tags:
``` C++17
{
int a = 5;
int b = 7;
ClassWithRef obj1(a);
ClassWithRef obj2(b);
obj2 = obj1; // COMPILATION ERROR
}
```
%% 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.
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:
### 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:
%% Cell type:code id: tags:
``` C++17
class ClassWithRef2
{
public:
ClassWithRef2(int& index);
ClassWithRef2(const ClassWithRef2& ) = default;
private:
int& index_;
};
```
%% Cell type:code id: tags:
``` C++17
ClassWithRef2::ClassWithRef2(int& index)
: index_(index)
{ }
```
%% Cell type:code id: tags:
``` C++17
{
int a = 5;
ClassWithRef2 obj1(a);
ClassWithRef2 obj2(obj1); // ok
ClassWithRef2 obj3 {obj1}; // ok
}
```
%% Cell type:markdown id: tags:
There are effectively two ways to copy an object:
* With an affectation operator.
* 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!
%% Cell type:markdown id: tags:
### The dangers of copy constructions... and how I avoid them
Copy construction may in fact be quite dangerous:
* As we've just seen, affectation 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 \cite{Meyers2005}) 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.
* More importantly, affectation operators may be a nightmare to maintain. Imagine you have a class for which you overload manually the affectation 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 real 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:
* 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 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:
## Canonical form of a class
So a typical class of mine looks like:
%% Cell type:code id: tags:
``` C++17
class AlmostCanonicalClass
{
public: // or even protected or private for some of them!
//! Constructor.
AlmostCanonicalClass(...);
//! Destructor.
~AlmostCanonicalClass() = default;
//! Disable copy constructor.
AlmostCanonicalClass(const AlmostCanonicalClass& ) = delete;
//! Disable copy affectation.
AlmostCanonicalClass& operator=(const AlmostCanonicalClass& ) = delete;
};
```
%% 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).
%% Cell type:markdown id: tags:
### [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:
%% Cell type:code id: tags:
``` C++17
class TrueCanonicalClass
{
public: // or even protected or private for some of them!
//! Constructor.
TrueCanonicalClass(...);
//! Destructor.
~TrueCanonicalClass() = default;
//! Disable copy constructor.
TrueCanonicalClass(const TrueCanonicalClass& ) = delete;
//! Disable copy affectation.
TrueCanonicalClass& operator=(const TrueCanonicalClass& ) = delete;
//! Disable move constructor.
TrueCanonicalClass(TrueCanonicalClass&& ) = delete;
//! Disable move affectation.
TrueCanonicalClass& operator=(TrueCanonicalClass&& ) = delete;
};
```
%% 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.
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:
# References
[<a id="cit-Meyers2005" href="#call-Meyers2005">Meyers2005</a>] Scott Meyers, ``_Effective C++: 55 Specific Ways to Improve Your Programs and Designs (3rd Edition)_'', 2005.
%% 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