{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# [Getting started in C++](./) - [Object programming](./0-main.ipynb) - [Member functions](./2-Member-functions.ipynb)" ] }, { "cell_type": "markdown", "metadata": { "toc": true }, "source": [ "

Table of contents

\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Member functions\n", "\n", "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).\n", "\n", "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*...)\n", "\n", "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", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#include \n", "\n", "struct Vector\n", "{\n", " double x;\n", " double y; \n", " double z;\n", " \n", " void init(double x, double y, double z)\n", " {\n", " this->x = x;\n", " this->y = y;\n", " this->z = z;\n", " }\n", " \n", " double norm()\n", " {\n", " return std::sqrt(x * x + y * y + z * z);\n", " }\n", " \n", "};" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#include \n", "\n", "{\n", " Vector v;\n", " v.init(5., 6., -4.2);\n", " std::cout << v.norm() << std::endl;\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's do a bit of taxonomy here:\n", "\n", "- `init()` and `norm()` are called **member functions** or **methods**. The same remark concerning C++ purist I did for member variables may be applied here.\n", "- **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.\n", "- **Attributes** are in fact the data attributes AND the methods. It is however often used only to designate the data attributes.\n", "\n", "**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", "metadata": {}, "source": [ "## The `this` keyword\n", "\n", "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`.\n", "\n", "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.\n", "\n", "An usual convention is to suffix data attributes with a `_` (**be careful**, attributes prefixed with a `_` is reserved by the C++ standard); doing so remove the need to the explicit `this`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#include \n", "\n", "struct Vector2\n", "{\n", " double x_;\n", " double y_; \n", " double z_;\n", " \n", " void init(double x, double y, double z)\n", " {\n", " x_ = x;\n", " y_ = y;\n", " z_ = z;\n", " }\n", " \n", " double norm()\n", " {\n", " return std::sqrt(x_ * x_ + y_ * y_ + z_ * z_);\n", " }\n", " \n", "};" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#include \n", "\n", "{\n", " Vector2 v;\n", " v.init(5., 6., -4.2);\n", " std::cout << v.norm() << std::endl;\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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", "metadata": {}, "source": [ "## Separating declaration and definition\n", "\n", "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:\n", "\n", "- On one side, usually in a header file:\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "struct Vector3\n", "{\n", " double x_;\n", " double y_; \n", " double z_;\n", " \n", " void init(double x, double y, double z);\n", " \n", " double norm();\n", "};" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- On another side the definition, usually in a source file which includes the header file:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "void Vector3::init(double x, double y, double z)\n", "{\n", " x_ = x;\n", " y_ = y;\n", " z_ = z;\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "double Vector3::norm()\n", "{\n", " return std::sqrt(x_ * x_ + y_ * y_ + z_ * z_);\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#include \n", "\n", "{\n", " Vector3 v;\n", " v.init(5., 6., -4.2);\n", " std::cout << v.norm() << std::endl;\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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", "metadata": {}, "source": [ "## Const methods\n", "\n", "Are we happy here with what we have so far? Unfortunately, not quite...\n", "\n", "If we define a simple free function that print the norm of a `Vector3`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#include \n", "\n", "void print_norm(const Vector3& v)\n", "{\n", " std::cout << v.norm() << std::endl;\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "... we see that doesn't compile. So what is happening?\n", "\n", "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:\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#include \n", "\n", "void print_norm_no_const(Vector3& v) // BAD IDEA!\n", "{\n", " std::cout << v.norm() << std::endl;\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "{\n", " Vector3 v;\n", " v.init(5., 6., -4.2);\n", " print_norm_no_const(v);\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "The sketchy \"patch\" I provided would be akin to ignoring the `const` feature almost entirely when objects are concerned.\n", "\n", "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", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "struct Vector4\n", "{\n", " double x_;\n", " double y_; \n", " double z_;\n", " \n", " void init(double x, double y, double z);\n", " \n", " double norm() const; // notice the additional keyword!\n", " \n", " void dont_put_const_everywhere() const;\n", "};" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "void Vector4::init(double x, double y, double z)\n", "{\n", " x_ = x;\n", " y_ = y;\n", " z_ = z;\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "double Vector4::norm() const\n", "{\n", " return std::sqrt(x_ * x_ + y_ * y_ + z_ * z_);\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#include \n", "\n", "void print_norm(const Vector4& v)\n", "{\n", " std::cout << v.norm() << std::endl;\n", "}\n", "\n", "{\n", " Vector4 v;\n", " v.init(5., 6., -4.2);\n", " print_norm(v);\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "void Vector4::dont_put_const_everywhere() const\n", "{\n", " x_ = 0.; // ERROR!\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `mutable` keyword\n", "\n", "Tread here with extreme caution here! Sometimes, you might want for a method to be mostly unable to modify the state of the class but you still need to modify one or more attribute. You may in this case use the `mutable` keyword when defining this attribute:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "struct Vector5\n", "{\n", " double x_;\n", " double y_; \n", " double z_;\n", " mutable unsigned int Nnorm_calls_;\n", " \n", " void init(double x, double y, double z);\n", " \n", " double norm() const; \n", "};" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "void Vector5::init(double x, double y, double z)\n", "{\n", " x_ = x;\n", " y_ = y;\n", " z_ = z;\n", " Nnorm_calls_ = 0u;\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "double Vector5::norm() const\n", "{\n", " ++Nnorm_calls_;\n", " return std::sqrt(x_ * x_ + y_ * y_ + z_ * z_);\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "{\n", " Vector5 v;\n", " v.init(5., 6., -4.2);\n", " for (int i = 0; i < 5; ++i)\n", " v.norm();\n", " std::cout << \"Method 'norm()' was called \" << v.Nnorm_calls_ << \" times.\" << std::endl; \n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "I must stress again that you should use this in a **last resort**! \n", "\n", "For my part, I have used this in only two contexts:\n", "* Using a work variable that would have been costly to reallocate at each call. This variable was always reset and used within the method that calls it and was not used to share a state between methods.\n", "* For mutexes when using shared memory parallelism. This is way out of the scope of this tutorial." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "© _CNRS 2016_ - _Inria 2018-2021_ \n", "_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/)_ \n", "_The present version has been written by Sébastien Gilles and Vincent Rouvreau (Inria)_" ] } ], "metadata": { "kernelspec": { "display_name": "C++17", "language": "C++17", "name": "xcpp17" }, "language_info": { "codemirror_mode": "text/x-c++src", "file_extension": ".cpp", "mimetype": "text/x-c++src", "name": "c++", "version": "17" }, "latex_envs": { "LaTeX_envs_menu_present": true, "autoclose": false, "autocomplete": true, "bibliofile": "biblio.bib", "cite_by": "apalike", "current_citInitial": 1, "eqLabelWithNumbers": true, "eqNumInitial": 1, "hotkeys": { "equation": "Ctrl-E", "itemize": "Ctrl-I" }, "labels_anchors": false, "latex_user_defs": false, "report_style_numbering": false, "user_envs_cfg": false }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": false, "sideBar": true, "skip_h1_title": true, "title_cell": "Table of contents", "title_sidebar": "Contents", "toc_cell": true, "toc_position": { "height": "calc(100% - 180px)", "left": "10px", "top": "150px", "width": "318.8px" }, "toc_section_display": true, "toc_window_display": false } }, "nbformat": 4, "nbformat_minor": 4 }