diff --git a/notebooks/ClassesBasics.ipynb b/notebooks/ClassesBasics.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..624c9ae94fea3594f3ac4020088d24c463314446 --- /dev/null +++ b/notebooks/ClassesBasics.ipynb @@ -0,0 +1,392 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "12fb635a", + "metadata": {}, + "outputs": [], + "source": [ + "#pragma cling add_include_path(\"/p/project/training2312/local/include\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aee2c444", + "metadata": {}, + "outputs": [], + "source": [ + "#include <iostream>\n", + "#include <cmath>" + ] + }, + { + "cell_type": "markdown", + "id": "9e3d6531", + "metadata": {}, + "source": [ + "User defined data types are created using the `struct` or `class` keyword. (They are more or less the same. The minor differences will be pointed out later.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f656858e", + "metadata": {}, + "outputs": [], + "source": [ + "class A{};\n", + "class B{};" + ] + }, + { + "cell_type": "markdown", + "id": "ba64b266", + "metadata": {}, + "source": [ + "The above are the smallest possible classes in C++, not containing anything in them. But they are still **different types**. Objects of type `A` are considered to be of a different unrelated type compared to objects of type `B`. Type identity is not based on what is inside a class. Two type names would be regarded as the same only if one is a type alias of another. Type aliases are created using the keyword `using`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57478793", + "metadata": {}, + "outputs": [], + "source": [ + "using C = A;" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99aa2cb4", + "metadata": {}, + "outputs": [], + "source": [ + "void f(int i, A a)\n", + "{\n", + " std::cout << \"Version of f overloaded for int and A\\n\";\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19aa2811", + "metadata": {}, + "outputs": [], + "source": [ + "void f(int i, B a)\n", + "{\n", + " std::cout << \"Version of f overloaded for int and B\\n\";\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a49aaa4d", + "metadata": {}, + "outputs": [], + "source": [ + "{\n", + " int num{88};\n", + " A xa;\n", + " B xb;\n", + " C xc;\n", + " f(num, xa);\n", + " f(num, xb);\n", + " f(num, xc);\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "acf3527d", + "metadata": {}, + "source": [ + "A user defined data type can optionally define \"member\" fields." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c7fa98de", + "metadata": {}, + "outputs": [], + "source": [ + "struct Example1 {\n", + " int x{}, y{};\n", + "};" + ] + }, + { + "cell_type": "markdown", + "id": "a4e5f919", + "metadata": {}, + "source": [ + "Each \"instance\" of an object of such a type has each of the data fields defined in the `class`. The members (components) of an instance are accessed using a dot notation for references and `->` notation for pointers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d620cad", + "metadata": {}, + "outputs": [], + "source": [ + "{\n", + " Example1 v1{2, 4};\n", + " Example1 v2{98, 3};\n", + " std::cout << \"Components of v1 : \" << v1.x << \", \" << v1.y << \"\\n\";\n", + " std::cout << \"Components of v2 : \" << v2.x << \", \" << v2.y << \"\\n\";\n", + " std::cout << \"Size of an object of type Example1 = \" << sizeof(Example1) << \" bytes\\n\";\n", + " Example1* vp{&v1};\n", + " std::cout << \"Components of the entity vp is pointing to : \" << vp->x << \", \" << vp->y << \"\\n\";\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "04be22fd", + "metadata": {}, + "source": [ + "Classes are supposed to represent conceptual types : think complex numbers, 3D vectors, Atoms, Molecules, distances, ... Nouns we need to describe our problem are candidates to be classes. We model such entities by filling the classes with the right kind of members (properties). These members could be function members as well..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e5cdfd16", + "metadata": {}, + "outputs": [], + "source": [ + "struct Example2 {\n", + " int x{}, y{};\n", + " auto mod() -> int { return x * x + y * y; }\n", + "};" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5cd1391b", + "metadata": {}, + "outputs": [], + "source": [ + "{\n", + " Example2 v1{2, 4};\n", + " Example2 v2{98, 3};\n", + " std::cout << \"Components of v1 : \" << v1.x << \", \" << v1.y << \"\\n\";\n", + " std::cout << \"Components of v2 : \" << v2.x << \", \" << v2.y << \"\\n\";\n", + " std::cout << \"Size of an object of type Example2 = \" << sizeof(Example2) << \" bytes\\n\";\n", + " std::cout << \"mod result from v1 = \" << v1.mod() << \"\\n\";\n", + " std::cout << \"mod result from v2 = \" << v2.mod() << \"\\n\";\n", + " Example2* vp{&v1};\n", + " std::cout << \"mod of the entity vp is point to = \" << vp->mod() << \"\\n\";\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "8d522e25", + "metadata": {}, + "source": [ + "Member functions are accessed using the same notation as data members (dot or `->`). \n", + "\n", + "Only data members contribute to the size of one instance of a class. An object of a class with two `int` members and 7000 member functions will still take 8 bytes to store.\n", + "\n", + "Member functions are defined once for each class. They are not attached to an instance. When we call `v1.mod()`, conceptually it is like `Example2::mod(&v1)`. We can't ourselves write it like this, but the compiler does something similar. The member functions are translated with a hidden first argument named `this`, as if the function was : \n", + "\n", + "```c++\n", + "// only a conceptual model\n", + "auto Example2_conceptual_mod(Example* this) -> int\n", + "{\n", + " return this->x * this->x + this->y * this->y;\n", + "}\n", + "```\n", + "\n", + "The `this` pointer is real. It is available for use inside member functions in real code, akin to `self` in python. But in C++, `this->x` inside a member function can be shortened to `x`. This `this` pointer does not take any space inside the class: it is the address of the instance on the left of the dot, so that the compiler can always forward it correctly for every call to a member function. " + ] + }, + { + "cell_type": "markdown", + "id": "f836bc50", + "metadata": {}, + "source": [ + "In C++, the composition of a class in terms of its data and function members is a compile time constant. Properties of a class can not be added, removed or changed outside the class definition. When we see a variable (instance) of a certain type (class), we know exactly what properties it has, how many bytes it occupies etc., without considering the history of that variable in the program. One specific complex number can not in the course of the program execution acquire a new third component! All instances have the same data fields, and the same set of member functions. This enables regular, predictable and controllable behaviour when we use class type objects in regular code. " + ] + }, + { + "cell_type": "markdown", + "id": "2895d1f5", + "metadata": {}, + "source": [ + "## const member functions\n", + "\n", + "How would we indicate that the function `mod` in the `Example2` class above does not change any data members ? In the conceptual model pseudo-implementation as a free-standing function, we could do that easily:\n", + "\n", + "```c++\n", + "auto Example2_conceptual_mod(const Example2* this) -> int\n", + "{\n", + " return this->x * this->x + this->y * this->y;\n", + "}\n", + "```\n", + "\n", + "This would be possible because we have explicit access to the formal parameter, and we can make it `const`. But in the real code, the `this` pointer is passed implicitly. But we need some way to indicate to the compiler that it should generate the implicitly passed `this` pointer of the type `const Example2*` rather than `Example2*`. This is accomplished using the following notation:\n", + "\n", + "```c++\n", + "struct Example3 {\n", + " int x{}, y{};\n", + " auto mod() const -> int { return x * x + y * y; }\n", + "};\n", + "```\n", + "With that, any attempt to change member variables in `mod()` would result in a compiler error." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8b51549", + "metadata": {}, + "outputs": [], + "source": [ + "struct Example3 {\n", + " int x{}, y{};\n", + " auto mod() const -> int \n", + " {\n", + " // careless change of member...\n", + " x = x + y;\n", + " return x * x + y * y; \n", + " }\n", + "};" + ] + }, + { + "cell_type": "markdown", + "id": "2aeca42d", + "metadata": {}, + "source": [ + "## Operator overloading\n", + "\n", + "For user defined types, we can define the meaning of operators such as `+`, `-`, `*`, `/`, `%`, `<<` ... For instance, we can define addition of two objects of the type `Example2` as follows:" + ] + }, + { + "cell_type": "markdown", + "id": "8c182a0d", + "metadata": {}, + "source": [ + "```c++\n", + "auto operator+(const Example2& e1, const Example2& e2) -> Example2\n", + "{\n", + " return {e1.x + e2.x, e1.y + e2.y};\n", + "}\n", + "```\n", + "However, this is incorrectly not accepted by the jupyter-cling environment. Making the operator into a member function allows this to be executed in this interpreter. In real life code, the free standing operator above, being more symmetric between the two operands, is more elegant and preferable. For now, let's see how operator overloading looks like in practice..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f143d03", + "metadata": {}, + "outputs": [], + "source": [ + "struct Example4 {\n", + " int x{}, y{};\n", + " auto mod() -> int { return x * x + y * y; }\n", + " auto operator+(const Example4& other) const -> Example4\n", + " {\n", + " return {x + other.x, y + other.y };\n", + " }\n", + "};" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6f6832a", + "metadata": {}, + "outputs": [], + "source": [ + "{\n", + " Example4 a{10, 20}, b{3, 4};\n", + " auto c = a + b;\n", + " std::cout << \"c = [\" << c.x << \", \" << c.y << \"]\\n\";\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "e3e9d9d5", + "metadata": {}, + "source": [ + "Being able to overload operators like this allows our class types to behave much like built in types. The overloaded operators need one argument from our class type. Many operators can be overloaded. Most versatile among them is perhaps the function call operator `()`. We can write a class, for which we have an overloaded `()` operator. That allows objects of that class to be used as if they were functions. Such entities are variously called functionals, function objects, function-like objects ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7511ba61", + "metadata": {}, + "outputs": [], + "source": [ + "struct SineWave {\n", + " double amp{1.0}, ome{1.0}, pha{0.0};\n", + " auto operator()(double t) const noexcept -> double\n", + " {\n", + " return amp * sin(ome * t + pha);\n", + " }\n", + "};" + ] + }, + { + "cell_type": "markdown", + "id": "392513c7", + "metadata": {}, + "source": [ + "Notice how the interpreter accepts a trailing return type for the function in this case!\n", + "\n", + "We can create objects of this type, and use them as if they were functions...\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c9af4d5", + "metadata": {}, + "outputs": [], + "source": [ + "SineWave S1{0.5, 0.1, 0.3}, S2{3.0, 1.4, 0.8}, S3{2.0, 1.0, 1.8};\n", + "for (double t=0.; t < 2.0; t += 0.05)\n", + " std::cout << t << \"\\t\" << S1(t) << \"\\t\" << S2(t) << \"\\t\" << S3(t) << \"\\n\";" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af64d070-1caa-413d-bf07-67a976f3710d", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "YourCXX", + "language": "c++", + "name": "yourcxx" + }, + "language_info": { + "codemirror_mode": "c++", + "file_extension": ".c++", + "mimetype": "text/x-c++src", + "name": "c++" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/CtorDtorDemo.ipynb b/notebooks/CtorDtorDemo.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..c09c8363358c91eaf93afd56e01a75eed09e86df --- /dev/null +++ b/notebooks/CtorDtorDemo.ipynb @@ -0,0 +1,169 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1ea45c69", + "metadata": {}, + "source": [ + "## Constructors\n", + "\n", + "Objects of `struct` types can be initialized using the `{` `}` initializer syntax specifying the components. However, in some situations, we may want different non-trivial kinds of initialization. Any kind of initialization can be performed for a class with any number of desired inputs. The initialization functions are called \"Constructors\" and they have the same name as the class. Every time an object comes into existence, it is implicitly or explicitly a call to one of these initialization functions or constructors.\n", + "\n", + "Similarly, every time an object runs out of scope and is to be removed from the program, a different \"destructor\" function is **automatically** called. Constructors are typically overloaded: there are many ways to create an object. The destructor for a class is unique." + ] + }, + { + "cell_type": "markdown", + "id": "5182c33c", + "metadata": {}, + "source": [ + "### Demo with the Vbose class" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "227c3d3b", + "metadata": {}, + "outputs": [], + "source": [ + "#pragma cling add_include_path(\"/p/project/training2312/local/include\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0fccfe95", + "metadata": {}, + "outputs": [], + "source": [ + "#include <iostream>\n", + "#include <vector>\n", + "#include \"Vbose.hh\"" + ] + }, + { + "cell_type": "markdown", + "id": "8c50c3ed", + "metadata": {}, + "source": [ + "In your course material, you will find, in the folder called `code`, a header called `Vbose.hh`. `Vbose` is a class where the special members like constructors, destructors are written in such a way that they emit messages when they are used. Calls to copy/move constructors and destructors usually happen silently behind the scenes. They follow strict rules about scopes, but usually we don't get to see them in action explicitly. The verbose versions of those functions in `Vbose` makes them visible. For instance..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b1e7f56", + "metadata": {}, + "outputs": [], + "source": [ + "{\n", + " std::cout << \"About to create first Vbose object\\n\";\n", + " Vbose one{\"FIRST\"};\n", + " std::cout << \"Now creating another Vbose object using copy construction syntax...\\n\";\n", + " auto two{one};\n", + " std::cout << \"Creating an alias to the first object...\\n\";\n", + " auto& three{one};\n", + " auto four{one + two};\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "a7d5930a", + "metadata": {}, + "source": [ + "The construction of the variable `four` happens using what is known as Return Value Optimization (RVO) which obviates what would otherwise have been a move construction. However, there are other situations where move constructor is really automatically used...\n", + "\n", + "In the following, we have a pre-existing object for `four` instead of creating it newly directly from the result of the `+` operation. The object is old, so it can not be newly constructed using RVO. It is an assignment operation, taking a temporary value as input. What version of the assignment operator do you expect will be used? Run the following cell to see if your expectation is borne out!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37f9894b-1f64-46f2-9f8f-eb31d313d4a1", + "metadata": {}, + "outputs": [], + "source": [ + "{\n", + " std::cout << \"About to create first Vbose object\\n\";\n", + " Vbose one{\"FIRST\"};\n", + " std::cout << \"Now creating another Vbose object using copy construction syntax...\\n\";\n", + " auto two{one};\n", + " std::cout << \"Creating an alias to the first object...\\n\";\n", + " auto& three{one};\n", + " Vbose four{\"FOUR\"};\n", + " four = one + two;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "315cf900", + "metadata": {}, + "source": [ + "`push_back` operations for `std::vector` cause sporadic calls to lots of copy/move operations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58f46887", + "metadata": {}, + "outputs": [], + "source": [ + "{\n", + " std::vector<Vbose> vv;\n", + " // vv.reserve(10); // what happens if you uncomment this line?\n", + " Vbose one{\"FIRST\"};\n", + " for (int i = 0; i < 10; ++i) vv.push_back(one);\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "328486dc", + "metadata": {}, + "source": [ + "Finally, we can use the Vbose class to see copy happening when we loop over a container using a plain `auto` as opposed to `auto&&` in a range based for loop..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de0aa2f3", + "metadata": {}, + "outputs": [], + "source": [ + "{\n", + " using namespace std::string_literals;\n", + " std::vector<Vbose> vv(10, Vbose(\"EXEMPLAR\"));\n", + " for (auto elem: vv) std::cout << elem.value() << \"\\n\";\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5b2db4c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "YourCXX", + "language": "c++", + "name": "yourcxx" + }, + "language_info": { + "codemirror_mode": "c++", + "file_extension": ".c++", + "mimetype": "text/x-c++src", + "name": "c++" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}