4-CanonicalForm.ipynb 18.7 KB
Newer Older
1 2 3 4 5 6
{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
7
    "# [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)"
8 9
   ]
  },
10 11 12 13 14 15 16 17 18 19
  {
   "cell_type": "markdown",
   "metadata": {
    "toc": true
   },
   "source": [
    "<h1>Table of contents<span class=\"tocSkip\"></span></h1>\n",
    "<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>"
   ]
  },
GILLES Sebastien's avatar
GILLES Sebastien committed
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Affectation operator\n",
    "\n",
    "We have not yet addressed one of the most natural operator: the one that might be used to allocate a value to an object. \n",
    "\n",
    "### Default behaviour (for a simple case)\n",
    "\n",
    "This one is provided by default:"
   ]
  },
  {
   "cell_type": "code",
35
   "execution_count": null,
GILLES Sebastien's avatar
GILLES Sebastien committed
36 37 38 39 40 41 42 43 44 45
   "metadata": {},
   "outputs": [],
   "source": [
    "#include <iostream>\n",
    "\n",
    "class Vector\n",
    "{\n",
    "    public:\n",
    "        Vector(double x, double y, double z);\n",
    "    \n",
46 47
    "        Vector& operator=(const Vector&) = default;\n",
    "    \n",
GILLES Sebastien's avatar
GILLES Sebastien committed
48 49 50 51 52 53 54 55 56 57 58 59
    "        void Print(std::ostream& out) const;\n",
    "    \n",
    "    private:\n",
    "    \n",
    "        double x_ = 0.;    \n",
    "        double y_ = 0.;    \n",
    "        double z_ = 0.;    \n",
    "};"
   ]
  },
  {
   "cell_type": "code",
60
   "execution_count": null,
GILLES Sebastien's avatar
GILLES Sebastien committed
61 62 63 64 65 66 67 68 69 70 71 72
   "metadata": {},
   "outputs": [],
   "source": [
    "Vector::Vector(double x, double y, double z)\n",
    ": x_(x),\n",
    "y_(y),\n",
    "z_(z)\n",
    "{ }"
   ]
  },
  {
   "cell_type": "code",
73
   "execution_count": null,
GILLES Sebastien's avatar
GILLES Sebastien committed
74 75 76 77 78 79 80 81 82 83 84
   "metadata": {},
   "outputs": [],
   "source": [
    "void Vector::Print(std::ostream& out) const\n",
    "{\n",
    "    out << \"(\" << x_ << \", \" << y_ << \", \" << z_ << \")\";\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
85 86 87
   "execution_count": null,
   "metadata": {},
   "outputs": [],
GILLES Sebastien's avatar
GILLES Sebastien committed
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
   "source": [
    "{\n",
    "    Vector v1(3., 5., 7.);    \n",
    "    Vector v2(-4., -16., 0.);\n",
    "    \n",
    "    v2 = v1;\n",
    "    v2.Print(std::cout);\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### The pointer case\n",
    "\n",
104
    "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!):"
GILLES Sebastien's avatar
GILLES Sebastien committed
105 106 107 108
   ]
  },
  {
   "cell_type": "code",
109
   "execution_count": null,
GILLES Sebastien's avatar
GILLES Sebastien committed
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
   "metadata": {},
   "outputs": [],
   "source": [
    "#include <iostream>\n",
    "\n",
    "class Vector2\n",
    "{\n",
    "    public:\n",
    "        Vector2(double x, double y, double z);\n",
    "    \n",
    "        ~Vector2();\n",
    "    \n",
    "        void Print(std::ostream& out) const;\n",
    "    \n",
    "    private:\n",
    "    \n",
    "        double* array_ = nullptr;\n",
    "};"
   ]
  },
  {
   "cell_type": "code",
132
   "execution_count": null,
GILLES Sebastien's avatar
GILLES Sebastien committed
133 134 135 136 137 138 139 140 141 142 143 144 145 146
   "metadata": {},
   "outputs": [],
   "source": [
    "Vector2::Vector2(double x, double y, double z)\n",
    "{ \n",
    "    array_ = new double[3];\n",
    "    array_[0] = x;\n",
    "    array_[1] = y;\n",
    "    array_[2] = z;\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
147
   "execution_count": null,
GILLES Sebastien's avatar
GILLES Sebastien committed
148 149 150 151 152 153 154 155 156 157 158
   "metadata": {},
   "outputs": [],
   "source": [
    "Vector2::~Vector2()\n",
    "{\n",
    "    delete[] array_;\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
159
   "execution_count": null,
GILLES Sebastien's avatar
GILLES Sebastien committed
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
   "metadata": {},
   "outputs": [],
   "source": [
    "void Vector2::Print(std::ostream& out) const\n",
    "{\n",
    "    out << \"(\" << array_[0] << \", \" << array_[1]  << \", \" << array_[2]  << \")\";\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "{\n",
    "    Vector2 v1(3., 5., 7.);    \n",
    "    Vector2 v2(-4., -16., 0.);\n",
    "    \n",
    "    v2 = v1; // Kernel crash?\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "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",
   "metadata": {},
   "source": [
ROUVREAU Vincent's avatar
ROUVREAU Vincent committed
194
    "```txt\n",
GILLES Sebastien's avatar
GILLES Sebastien committed
195
    "** Error in `./prog.exe': double free or corruption (fasttop): 0x0000000001532da0 **\n",
ROUVREAU Vincent's avatar
ROUVREAU Vincent committed
196
    "```\n",
GILLES Sebastien's avatar
GILLES Sebastien committed
197
    "\n",
ROUVREAU Vincent's avatar
ROUVREAU Vincent committed
198
    "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, and 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.\n",
GILLES Sebastien's avatar
GILLES Sebastien committed
199
    "\n",
200
    "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`!):\n"
GILLES Sebastien's avatar
GILLES Sebastien committed
201 202 203 204
   ]
  },
  {
   "cell_type": "code",
205
   "execution_count": null,
GILLES Sebastien's avatar
GILLES Sebastien committed
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
   "metadata": {},
   "outputs": [],
   "source": [
    "#include <iostream>\n",
    "\n",
    "class Vector3\n",
    "{\n",
    "    public:\n",
    "        Vector3(double x, double y, double z);\n",
    "    \n",
    "        ~Vector3();\n",
    "    \n",
    "        // Then again the Xeus-cling issue with out of class operator definition.\n",
    "        Vector3& operator=(const Vector3& rhs)\n",
    "        {\n",
    "            // Array already initialized in constructor; just change its content.\n",
    "            for (auto i = 0ul; i < 3ul; ++i)\n",
    "                array_[i] = rhs.array_[i];\n",
    "            \n",
    "            return *this; // The (logical) return value for such a method.\n",
    "        }\n",
    "    \n",
    "        void Print(std::ostream& out) const;\n",
    "    \n",
    "    private:\n",
    "    \n",
    "        double* array_ = nullptr;\n",
    "};"
   ]
  },
  {
   "cell_type": "code",
238
   "execution_count": null,
GILLES Sebastien's avatar
GILLES Sebastien committed
239 240 241 242 243 244 245 246 247 248 249 250 251 252
   "metadata": {},
   "outputs": [],
   "source": [
    "Vector3::Vector3(double x, double y, double z)\n",
    "{ \n",
    "    array_ = new double[3];\n",
    "    array_[0] = x;\n",
    "    array_[1] = y;\n",
    "    array_[2] = z;\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
253
   "execution_count": null,
GILLES Sebastien's avatar
GILLES Sebastien committed
254 255 256 257 258 259 260 261 262 263 264
   "metadata": {},
   "outputs": [],
   "source": [
    "Vector3::~Vector3()\n",
    "{\n",
    "    delete[] array_;\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
265
   "execution_count": null,
GILLES Sebastien's avatar
GILLES Sebastien committed
266 267 268 269 270 271 272 273 274 275 276
   "metadata": {},
   "outputs": [],
   "source": [
    "void Vector3::Print(std::ostream& out) const\n",
    "{\n",
    "    out << \"(\" << array_[0] << \", \" << array_[1]  << \", \" << array_[2]  << \")\";\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
277 278 279
   "execution_count": null,
   "metadata": {},
   "outputs": [],
GILLES Sebastien's avatar
GILLES Sebastien committed
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
   "source": [
    "{\n",
    "    Vector3 v1(3., 5., 7.);    \n",
    "    Vector3 v2(-4., -16., 0.);\n",
    "    \n",
    "    v2 = v1;\n",
    "    v1.Print(std::cout);\n",
    "    std::cout << std::endl;\n",
    "    v2.Print(std::cout);    \n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Uncopyable class\n",
    "\n",
GILLES Sebastien's avatar
GILLES Sebastien committed
298
    "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:"
GILLES Sebastien's avatar
GILLES Sebastien committed
299 300 301 302
   ]
  },
  {
   "cell_type": "code",
303
   "execution_count": null,
GILLES Sebastien's avatar
GILLES Sebastien committed
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
   "metadata": {},
   "outputs": [],
   "source": [
    "class ClassWithRef\n",
    "{\n",
    "    public:\n",
    "        \n",
    "        ClassWithRef(int& index);\n",
    "    \n",
    "    private:\n",
    "\n",
    "        int& index_;    \n",
    "};"
   ]
  },
  {
   "cell_type": "code",
321
   "execution_count": null,
GILLES Sebastien's avatar
GILLES Sebastien committed
322 323 324 325 326 327 328 329 330 331
   "metadata": {},
   "outputs": [],
   "source": [
    "ClassWithRef::ClassWithRef(int& index)\n",
    ": index_(index)\n",
    "{ }"
   ]
  },
  {
   "cell_type": "code",
332
   "execution_count": null,
GILLES Sebastien's avatar
GILLES Sebastien committed
333 334 335 336 337 338 339 340 341 342 343
   "metadata": {},
   "outputs": [],
   "source": [
    "{\n",
    "    int a = 5;\n",
    "    ClassWithRef obj(a);    \n",
    "}"
   ]
  },
  {
   "cell_type": "code",
344 345 346
   "execution_count": null,
   "metadata": {},
   "outputs": [],
GILLES Sebastien's avatar
GILLES Sebastien committed
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
   "source": [
    "{\n",
    "    int a = 5;\n",
    "    int b = 7;\n",
    "    ClassWithRef obj1(a);    \n",
    "    ClassWithRef obj2(b);    \n",
    "    \n",
    "    obj2 = obj1; // COMPILATION ERROR \n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "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.\n",
    "\n",
    "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",
   "metadata": {},
   "source": [
    "### Copy constructor\n",
    "\n",
    "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",
378
   "execution_count": null,
GILLES Sebastien's avatar
GILLES Sebastien committed
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397
   "metadata": {},
   "outputs": [],
   "source": [
    "class ClassWithRef2\n",
    "{\n",
    "    public:\n",
    "        \n",
    "        ClassWithRef2(int& index);\n",
    "    \n",
    "        ClassWithRef2(const ClassWithRef2& ) = default;\n",
    "    \n",
    "    private:\n",
    "\n",
    "        int& index_;    \n",
    "};"
   ]
  },
  {
   "cell_type": "code",
398
   "execution_count": null,
GILLES Sebastien's avatar
GILLES Sebastien committed
399 400 401 402 403 404 405 406 407 408
   "metadata": {},
   "outputs": [],
   "source": [
    "ClassWithRef2::ClassWithRef2(int& index)\n",
    ": index_(index)\n",
    "{ }"
   ]
  },
  {
   "cell_type": "code",
409
   "execution_count": null,
GILLES Sebastien's avatar
GILLES Sebastien committed
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
   "metadata": {},
   "outputs": [],
   "source": [
    "{\n",
    "    int a = 5;\n",
    "    ClassWithRef2 obj1(a);    \n",
    "    \n",
    "    ClassWithRef2 obj2(obj1); // ok\n",
    "    ClassWithRef2 obj3 {obj1}; // ok    \n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "There are effectively two ways to copy an object:\n",
    "\n",
    "* With an affectation operator.\n",
    "* With a copy constructor.\n",
    "\n",
431
    "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!"
GILLES Sebastien's avatar
GILLES Sebastien committed
432 433 434 435 436 437 438 439 440 441
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### The dangers of copy constructions... and how I avoid them\n",
    "\n",
    "Copy construction may in fact be quite dangerous:\n",
    "\n",
442
    "* 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.\n",
GILLES Sebastien's avatar
GILLES Sebastien committed
443
    "* Depending on somewhat complicated rules that have evolved with standard versions, some might or might not be defined implicitly.\n",
444
    "* 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!\n",
GILLES Sebastien's avatar
GILLES Sebastien committed
445
    "\n",
GILLES Sebastien's avatar
GILLES Sebastien committed
446
    "To avoid that I took the extreme rule to (almost) never overload those myself:\n",
GILLES Sebastien's avatar
GILLES Sebastien committed
447 448
    "\n",
    "* 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!\n",
449
    "* 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).\n",
GILLES Sebastien's avatar
GILLES Sebastien committed
450 451 452 453 454 455 456 457 458 459 460 461 462
    "\n",
    "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",
   "metadata": {},
   "source": [
    "## Canonical form of a class\n",
    "\n",
    "So a typicall class of mine looks like:"
   ]
  },
463 464 465 466 467
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
GILLES Sebastien's avatar
GILLES Sebastien committed
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
   "source": [
    "class AlmostCanonicalClass\n",
    "{\n",
    "    public:  // or even protected or private for some of them!\n",
    "    \n",
    "        //! Constructor.\n",
    "        AlmostCanonicalClass(...);\n",
    "    \n",
    "        //! Destructor.\n",
    "        ~AlmostCanonicalClass() = default;\n",
    "    \n",
    "        //! Disable copy constructor.\n",
    "        AlmostCanonicalClass(const AlmostCanonicalClass& ) = delete;\n",
    "    \n",
    "        //! Disable copy affectation.\n",
    "        AlmostCanonicalClass& operator=(const AlmostCanonicalClass& ) = delete;    \n",
    "};"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Your IDE or a script might even be handy to generate this by default (with additional Doxygen comments for good measure)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### [Advanced] The true canonical class\n",
    "\n",
500
    "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:"
GILLES Sebastien's avatar
GILLES Sebastien committed
501 502 503 504
   ]
  },
  {
   "cell_type": "code",
505
   "execution_count": null,
GILLES Sebastien's avatar
GILLES Sebastien committed
506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531
   "metadata": {},
   "outputs": [],
   "source": [
    "class TrueCanonicalClass\n",
    "{\n",
    "    public:  // or even protected or private for some of them!\n",
    "    \n",
    "        //! Constructor.\n",
    "        TrueCanonicalClass(...);\n",
    "    \n",
    "        //! Destructor.\n",
    "        ~TrueCanonicalClass() = default;\n",
    "    \n",
    "        //! Disable copy constructor.\n",
    "        TrueCanonicalClass(const TrueCanonicalClass& ) = delete;\n",
    "    \n",
    "        //! Disable copy affectation.\n",
    "        TrueCanonicalClass& operator=(const TrueCanonicalClass& ) = delete;    \n",
    "    \n",
    "        //! Disable move constructor.\n",
    "        TrueCanonicalClass(TrueCanonicalClass&& ) = delete;\n",
    "    \n",
    "        //! Disable move affectation.\n",
    "        TrueCanonicalClass& operator=(TrueCanonicalClass&& ) = delete;    \n",
    "};"
   ]
532
  },
GILLES Sebastien's avatar
GILLES Sebastien committed
533 534 535 536 537 538
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In my programs, I like to declare explicitly all of them, using `default` and `delete` to provide automatic implementation for most of them.\n",
    "\n",
539
    "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."
GILLES Sebastien's avatar
GILLES Sebastien committed
540 541
   ]
  },
542 543 544 545 546 547 548 549 550 551
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# References\n",
    "\n",
    "[<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.\n",
    "\n"
   ]
  },
552 553 554 555 556
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
557
    "© _CNRS 2016_ - _Inria 2018-2021_   \n",
558 559
    "_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)_"
560 561 562 563 564 565 566
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "C++17",
   "language": "C++17",
GILLES Sebastien's avatar
GILLES Sebastien committed
567
   "name": "xcpp17"
568 569 570 571 572 573
  },
  "language_info": {
   "codemirror_mode": "text/x-c++src",
   "file_extension": ".cpp",
   "mimetype": "text/x-c++src",
   "name": "c++",
GILLES Sebastien's avatar
GILLES Sebastien committed
574
   "version": "17"
575 576 577 578 579 580
  },
  "latex_envs": {
   "LaTeX_envs_menu_present": true,
   "autoclose": false,
   "autocomplete": true,
   "bibliofile": "biblio.bib",
581
   "cite_by": "key",
582 583 584 585 586 587 588 589 590 591 592 593 594 595 596
   "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": {},
597
   "number_sections": false,
598
   "sideBar": true,
599 600
   "skip_h1_title": true,
   "title_cell": "Table of contents",
601
   "title_sidebar": "Contents",
602
   "toc_cell": true,
603 604
   "toc_position": {},
   "toc_section_display": true,
605
   "toc_window_display": false
606 607 608
  }
 },
 "nbformat": 4,
GILLES Sebastien's avatar
GILLES Sebastien committed
609
 "nbformat_minor": 4
610
}