Commit fb1b3e73 authored by GILLES Sebastien's avatar GILLES Sebastien
Browse files

Cleaning-up object programming part.

parent 7ea3c1a8
%% Cell type:markdown id: tags:
# [Getting started in C++](/) - [Object programming](/notebooks/2-ObjectProgramming/0-main.ipynb)
# [Getting started in C++](/) - [Object programming](./0-main.ipynb)
%% Cell type:markdown id: tags:
* [Introduction to the concept of object](/notebooks/2-ObjectProgramming/1-Introduction.ipynb)
* [TP 4](/notebooks/2-ObjectProgramming/1b-TP.ipynb)
* [Member functions](/notebooks/2-ObjectProgramming/2-Member-functions.ipynb)
* [TP 5](/notebooks/2-ObjectProgramming/2b-TP.ipynb)
* [Base constructors and destructor](/notebooks/2-ObjectProgramming/3-constructors-destructor.ipynb)
* [TP 6](/notebooks/2-ObjectProgramming/3b-TP.ipynb)
* [Encapsulation](/notebooks/2-ObjectProgramming/4-encapsulation.ipynb)
* [TP 7](/notebooks/2-ObjectProgramming/4b-TP.ipynb)
* [Static attributes](/notebooks/2-ObjectProgramming/5-static.ipynb)
* [Inheritance and polymorphism](/notebooks/2-ObjectProgramming/6-inheritance.ipynb)
* [TP 8](/notebooks/2-ObjectProgramming/6b-TP.ipynb)
* [Introduction to the concept of object](./1-Introduction.ipynb)
* [TP 4](./1b-TP.ipynb)
* [Member functions](./2-Member-functions.ipynb)
* [TP 5](./2b-TP.ipynb)
* [Base constructors and destructor](./3-constructors-destructor.ipynb)
* [TP 6](./3b-TP.ipynb)
* [Encapsulation](./4-encapsulation.ipynb)
* [TP 7](./4b-TP.ipynb)
* [Static attributes](./5-static.ipynb)
* [Inheritance and polymorphism](./6-inheritance.ipynb)
* [TP 8](./6b-TP.ipynb)
%% Cell type:markdown id: tags:
© _CNRS 2016_ - _Inria 2018-2019_
_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)_
......
%% Cell type:markdown id: tags:
# [Getting started in C++](/) - [Object programming](/notebooks/2-ObjectProgramming/0-main.ipynb) - [Introduction to the concept of object](/notebooks/2-ObjectProgramming/1-Introduction.ipynb)
# [Getting started in C++](/) - [Object programming](./0-main.ipynb) - [Introduction to the concept of object](./1-Introduction.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="#Motivation" data-toc-modified-id="Motivation-1">Motivation</a></span></li><li><span><a href="#The-C-response:-the-struct" data-toc-modified-id="The-C-response:-the-struct-2">The C response: the <code>struct</code></a></span></li><li><span><a href="#Passing-a-struct-to-a-function" data-toc-modified-id="Passing-a-struct-to-a-function-3">Passing a struct to a function</a></span><ul class="toc-item"><li><span><a href="#Pass-by-const-reference" data-toc-modified-id="Pass-by-const-reference-3.1">Pass-by-const-reference</a></span></li><li><span><a href="#Pass-by-pointer" data-toc-modified-id="Pass-by-pointer-3.2">Pass-by-pointer</a></span></li></ul></li><li><span><a href="#Initialization-of-objects" data-toc-modified-id="Initialization-of-objects-4">Initialization of objects</a></span></li></ul></div>
%% Cell type:markdown id: tags:
## Motivation
%% Cell type:markdown id: tags:
Sometimes, there are variables that are bound to be initialized and used together. Let's consider the position of a point in a three-dimensional space:
%% Cell type:code id: tags:
``` C++17
#include <iostream>
#include <cmath> // For std::sqrt
double norm(double v_x, double v_y, double v_z)
{
return std::sqrt( v_x * v_x + v_y * v_y + v_z * v_z );
};
{
double v1_x, v1_y, v1_z;
v1_x = 1.;
v1_y = 5.;
v1_z = -2.;
std::cout << norm(v1_x, v1_y, v1_z) << std::endl;
double v2_x, v2_y, v2_z;
v2_x = 2.;
v2_y = 2.;
v2_z = 4.;
std::cout << norm(v2_x, v2_y, v2_z) << std::endl;
}
```
%% Cell type:markdown id: tags:
The code above is completely oblivious of the close relationship between `x`, `y` and `z`, and for instance the norm function takes three distinct arguments.
This is not just an inconveniency: this can lead to mistake if there is an error in the variables passed:
%% Cell type:code id: tags:
``` C++17
{
double v1_x, v1_y, v1_z;
v1_x = 1.;
v1_y = 5.;
v1_z = -2.;
double v2_x, v2_y, v2_z;
v2_x = 2.;
v2_y = 2.;
v2_z = 4.;
const double norm1 = norm(v1_x, v1_y, v2_z); // probably not what was intended, but the program
// has no way to figure out something is fishy!
}
```
%% Cell type:markdown id: tags:
## The C response: the `struct`
C introduced the `struct` to be able to group nicely data together and limit the risk I exposed above:
%% Cell type:code id: tags:
``` C++17
struct Vector
{
double x;
double y;
double z;
};
```
%% Cell type:code id: tags:
``` C++17
double norm(Vector v)
{
return std::sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
}
{
Vector v1;
v1.x = 1.;
v1.y = 5.;
v1.z = -2.;
std::cout << norm(v1) << std::endl;
Vector v2;
v2.x = 2.;
v2.y = 2.;
v2.z = 4.;
std::cout << norm(v2) << std::endl;
}
```
%% Cell type:markdown id: tags:
Calling `norm` is now both more elegant (only one argument) and less dangerous (I can't mix by mistake coordinates from different objects).
Let's introduce at this point a bit of vocabulary:
- `x`, `y` and `z` in the structure are called **member variables** or **data attributes** (often shorten as **attributes** even if in a class this is actually not completely proper). On a side note: some C++ purists will be adamant only **member variables** should be used; but I rather use **data attributes** which is the term preferred in many others object programming languages.
- `Vector` is a **struct**, which is a somewhat simplified **class** (we will explain the difference when we'll introduce classes).
- `v1` and `v2` are **objects**.
Let's also highlight the `.` syntax which allows to access the attributes of an object (e.g `v1.x`).
%% Cell type:markdown id: tags:
## Passing a struct to a function
In the `norm` function above, we passed as argument an object of `Vector` type by value. When we introduced functions, we saw there were three ways to pass an argument:
* By value.
* By reference.
* By pointers.
I didn't mention there the copy cost of a pass-by-value: copying a plain old data (POD) type such as an `int` or a `double` is actually cheap, and is recommended over a reference. But the story is not the same for an object: the cost of copying the object in the case of a pass-by-value may actually be quite high (imagine if there were an array with thousands of double values for instance) - and that's supposing the object is copyable (but we're not quite ready yet to deal with [that aspect](http://localhost:8888/notebooks/3-Operators/4-CanonicalForm.ipynb#Uncopyable-class)).
I didn't mention there the copy cost of a pass-by-value: copying a plain old data (POD) type such as an `int` or a `double` is actually cheap, and is recommended over a reference. But the story is not the same for an object: the cost of copying the object in the case of a pass-by-value may actually be quite high (imagine if there were an array with thousands of double values inside for instance) - and that's supposing the object is copyable (but we're not quite ready yet to deal with [that aspect](../3-Operators/4-CanonicalForm.ipynb#Uncopyable-class)).
### Pass-by-const-reference
So most of the time it is advised to pass arguments by reference, often along a `const` qualifier if the object is not to be modified by the function:
%% Cell type:code id: tags:
``` C++17
double norm2(const Vector& v) // change the name to avoid ambiguity in runtime with former definition
// Just for Xeus-cling: don't do that in your code!
{
return std::sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
}
{
Vector v1;
v1.x = 1.;
v1.y = 5.;
v1.z = -2.;
std::cout << norm2(v1) << std::endl;
}
```
%% Cell type:markdown id: tags:
### Pass-by-pointer
Of course, if for some reason you prefer to use pointers it is also possible:
%% Cell type:code id: tags:
``` C++17
double norm(const Vector* const v) // can keep the name here: no possible ambiguity
{
return std::sqrt((*v).x * (*v).x + (*v).y * (*v).y + (*v).z * (*v).z);
}
{
Vector v1;
v1.x = 1.;
v1.y = 5.;
v1.z = -2.;
std::cout << norm(&v1) << std::endl;
}
```
%% Cell type:markdown id: tags:
This is more than little verbosy, so a shortcut has been introduced; `->` means you dereference a pointer and then calls the attribute:
%% Cell type:code id: tags:
``` C++17
double norm2(const Vector* const v)
{
return std::sqrt(v->x * v->x + v->y * v->y + v->z * v->z);
}
{
Vector v1;
v1.x = 1.;
v1.y = 5.;
v1.z = -2.;
std::cout << norm2(&v1) << std::endl;
}
```
%% Cell type:markdown id: tags:
## Initialization of objects
So far, we have improved the way the `norm` function is called, but the initialization of a vector is still a bit tedious. Let's wrap up a function to ease that:
%% Cell type:code id: tags:
``` C++17
void init(Vector& v, double x, double y, double z)
{
v.x = x;
v.y = y;
v.z = z;
}
```
%% Cell type:code id: tags:
``` C++17
{
Vector v1;
init(v1, 1., 5., -2.);
std::cout << "Norm = " << norm(v1) << std::endl;
}
```
%% Cell type:markdown id: tags:
© _CNRS 2016_ - _Inria 2018-2019_
© _CNRS 2016_ - _Inria 2018-2020_
_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)_
......
%% Cell type:markdown id: tags:
# [Getting started in C++](/) - [Object programming](/notebooks/2-ObjectProgramming/0-main.ipynb) - [Member functions](/notebooks/2-ObjectProgramming/2-Member-functions.ipynb)
# [Getting started in C++](/) - [Object programming](./0-main.ipynb) - [Member functions](./2-Member-functions.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="#Member-functions" data-toc-modified-id="Member-functions-1">Member functions</a></span></li><li><span><a href="#The-this-keyword" data-toc-modified-id="The-this-keyword-2">The <code>this</code> keyword</a></span></li><li><span><a href="#Separating-declaration-and-definition" data-toc-modified-id="Separating-declaration-and-definition-3">Separating declaration and definition</a></span></li><li><span><a href="#Const-methods" data-toc-modified-id="Const-methods-4">Const methods</a></span><ul class="toc-item"><li><span><a href="#mutable-keyword" data-toc-modified-id="mutable-keyword-4.1"><code>mutable</code> keyword</a></span></li></ul></li></ul></div>
%% Cell type:markdown id: tags:
## Member functions
The struct we used previously would work the same in C code (with the exceptions of references: with a C compiler you would have to stick with pointers).
But when Bjarne Stroustrup created the C++, its main idea was to extend these structs into full-fledged **classes** (to the point the work name of his language was *C with classes*...)
One of the idea that was missing with C `struct` is the possibility to add as well member functions.
One of the idea that was missing with original C `struct` was the possibility to add as well member functions; this is no longer the case:
%% Cell type:code id: tags:
``` C++17
#include <cmath>
struct Vector
{
double x;
double y;
double z;
void init(double x, double y, double z)
{
this->x = x;
this->y = y;
this->z = z;
}
double norm()
{
return std::sqrt(x * x + y * y + z * z);
}
};
```
%% Cell type:code id: tags:
``` C++17
#include <iostream>
{
Vector v;
v.init(5., 6., -4.2);
std::cout << v.norm() << std::endl;
}
```
%%%% Output: stream
8.86792
%% Cell type:markdown id: tags:
Let's do a bit of taxonomy here:
- init() and norm() are called **member functions** or **methods**. The same remark concerning C++ purist I did for member variables may be applied here.
- **Method** is used in other programming languages, but for some reason Julia creators used this exact term for an entirely different concept. So to put in a nutshell a C++ method is akin to a Python one but not to what Julia calls a method.
- **Attributes** are in fact the data attributes AND the methods. It is however often used only to designate the data attributes.
**WARNING**: In C++ you can't complete a class after the fact as you may for instance in Python. So all the methods and data atttributes have to be declared within the struct brackets here; if you need to add something you will have to recompile the class. This means especially you can't add directly a member function to a class provided by a third party library; we'll see shortly the mechanism you may use instead to do your bidding.
**WARNING**: In C++ you can't complete a class after the fact as you could for instance in Python. So all the methods and data atttributes have to be declared within the struct brackets here; if you need to add something you will have to recompile the class. This means especially you can't add directly a member function to a class provided by a third party library; we'll see shortly the mechanism you may use instead to do your bidding.
%% Cell type:markdown id: tags:
## The `this` keyword
The `this->` may have puzzled you: it is a keyword to refer to the current object. So when you call `v.init(...)`, this is an implicit reference to `v`.
In most cases, it might be altogether removed; we have to put it explicitly here solely because we named the `init` parameters with the same name as the data attribute. If not, we could have avoided to mention it completely.
An usual convention is to suffix data attributes with a `_`; doing so remove the need to the explicit this:
%% Cell type:code id: tags:
``` C++17
#include <cmath>
struct Vector2
{
double x_;
double y_;
double z_;
void init(double x, double y, double z)
{
x_ = x;
y_ = y;
z_ = z;
}
double norm()
{
return std::sqrt(x_ * x_ + y_ * y_ + z_ * z_);
}
};
```
%% Cell type:code id: tags:
``` C++17
#include <iostream>
{
Vector2 v;
v.init(5., 6., -4.2);
std::cout << v.norm() << std::endl;
}
```
%%%% Output: stream
8.86792
%% Cell type:markdown id: tags:
That is not to say you should forget altogether the `this` keyword: it might mecessary in some contexts (for templates for instance - see [later](/notebooks/4-Templates/3-Syntax.ipynb)...)
That is not to say you should forget altogether the `this` keyword: it might be necessary in some contexts (for templates for instance - see [later](../4-Templates/3-Syntax.ipynb)...)
%% Cell type:markdown id: tags:
## Separating declaration and definition
We have defined above the method directly in the class declaration; which is not very clean. It is acceptable for a very short method as here, but for a more complex class and method it is better to separate explicitly both. In this case you will have:
- On one side, usually in a header file:
%% Cell type:code id: tags:
``` C++17
struct Vector3
{
double x_;
double y_;
double z_;
void init(double x, double y, double z);
double norm();
};
```
%% Cell type:markdown id: tags:
- On another side the definition, usually in a source file which includes the header file:
%% Cell type:code id: tags:
``` C++17
void Vector3::init(double x, double y, double z)
{
x_ = x;
y_ = y;
z_ = z;
}
```
%% Cell type:code id: tags:
``` C++17
double Vector3::norm()
{
return std::sqrt(x_ * x_ + y_ * y_ + z_ * z_);
}
```
%% Cell type:code id: tags:
``` C++17
#include <iostream>
{
Vector3 v;
v.init(5., 6., -4.2);
std::cout << v.norm() << std::endl;
}
```
%%%% Output: stream
8.86792
%% Cell type:markdown id: tags:
Please notice the `::` syntax which specifies the class for which the implementation is provided. Also pay attention to the fact the `this` may as well be implicitly used.
%% Cell type:markdown id: tags:
## Const methods
Are we happy here with what we have so far? Unfortunately, not quite...
If we define a simple free function that print the norm of a `Vector3`:
%% Cell type:code id: tags:
``` C++17
#include <iostream>
void print_norm(const Vector3& v)
{
std::cout << v.norm() << std::endl;
}
```
%%%% Output: stream
input_line_21:3:18: error: member function 'norm' not viable: 'this' argument has type 'const Vector3', but function is not marked const
std::cout << v.norm() << std::endl;
 ^
input_line_17:1:17: note: 'norm' declared here
double Vector3::norm()
 ^

%%%% Output: error
Interpreter Error:
%% Cell type:markdown id: tags:
... we see that doesn't compile. So what is happening?
The issue here is that the function `print_norm` takes as argument a constant reference, and has to guarantee the underlying object is not modified in the process. A "patch" would be to define it without the const:
%% Cell type:code id: tags:
``` C++17
#include <iostream>
void print_norm_no_const(Vector3& v) // BAD IDEA!
{
std::cout << v.norm() << std::endl;
}
```
%% Cell type:code id: tags:
``` C++17
{
Vector3 v;
v.init(5., 6., -4.2);
print_norm_no_const(v);
}
```
%%%% Output: stream
8.86792
%% Cell type:markdown id: tags:
Why is it such a poor idea? C++ is a compiled language, and this has its (many) pros and (many) cons. One of the advantages is to be able to leverage the compilation to detect at early time something is amiss. Here the compilation error is a good way to see we might be doing something wrong.
The sketchy "patch" I provided would be akin to ignoring the `const` feature almost entirely when objects are concerned.
The proper way is in fact quite the opposite: we may specify when writing a method that it is not allowed to modify the state of the object:
%% Cell type:code id: tags:
``` C++17
struct Vector4
{
double x_;
double y_;
double z_;
void init(double x, double y, double z);
double norm() const; // notice the additional keyword!
void dont_put_const_everywhere() const;
};
```
%% Cell type:code id: tags:
``` C++17
void Vector4::init(double x, double y, double z)
{
x_ = x;
y_ = y;
z_ = z;
}
```
%% Cell type:code id: tags:
``` C++17
double Vector4::norm() const
{
return std::sqrt(x_ * x_ + y_ * y_ + z_ * z_);
}
```
%% Cell type:markdown id: tags:
Please notice `const` needs to be specified both on declaration and on definition: if not provided in definition the signature of the method won't match and the compiler will yell.
%% Cell type:code id: tags:
``` C++17
#include <iostream>
void print_norm(const Vector4& v)
{
std::cout << v.norm() << std::endl;
}
{
Vector4 v;
v.init(5., 6., -4.2);
print_norm(v);
}
```
%%%% Output: stream
8.86792
%% Cell type:markdown id: tags:
Obviously, if we try to ignore a `const` keyword, the compiler will also yell (it is and SHOULD BE very good at this!):
%% Cell type:code id: tags:
``` C++17
void Vector4::dont_put_const_everywhere() const
{
x_ = 0.; // ERROR!
}
```
%%%% Output: stream
input_line_30:3:8: error: cannot assign to non-static data member within const member function 'dont_put_const_everywhere'
x_ = 0.; // ERROR!
 ~~ ^
input_line_30:1:15: note: member function 'Vector4::dont_put_const_everywhere' is declared const here
void Vector4::dont_put_const_everywhere() const
~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

%%%% Output: error
Interpreter Error: