├── .clang_format ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md └── src ├── auto.cpp ├── condition_variable.cpp ├── iterator.cpp ├── move_constructors.cpp ├── move_semantics.cpp ├── mutex.cpp ├── namespaces.cpp ├── references.cpp ├── rwlock.cpp ├── scoped_lock.cpp ├── sets.cpp ├── shared_ptr.cpp ├── spring2024 └── s24_my_ptr.cpp ├── templated_classes.cpp ├── templated_functions.cpp ├── unique_ptr.cpp ├── unordered_maps.cpp ├── vectors.cpp └── wrapper_class.cpp /.clang_format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | ColumnLimit: 120 3 | DerivePointerAlignment: false 4 | PointerAlignment: Right 5 | IndentWidth: 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # CMake 35 | build/ 36 | 37 | # Todo 38 | todo.txt 39 | 40 | # VSCode 41 | .vscode/ 42 | 43 | # Cache 44 | .cache/ -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(Bootcamp 3 | DESCRIPTION "C++ Bootcamp for 15-445/645" 4 | LANGUAGES CXX) 5 | 6 | if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") 7 | if(CMAKE_CXX_COMPILER_VERSION MATCHES "^14.") 8 | message(STATUS "You're using ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}") 9 | else() 10 | message(WARNING "!! We recommend that you use clang-14 for this bootcamp. You're using ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}, a different version.") 11 | endif() 12 | elseif(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") 13 | message(STATUS "You're using ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}") 14 | else() 15 | message(WARNING "!! We recommend that you use clang-14 for this bootcamp. You're using ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}, which is not clang.") 16 | endif() 17 | 18 | set(CMAKE_CXX_STANDARD 17) 19 | set(CMAKE_CXX_STANDARD_REQUIRED True) 20 | 21 | # Compiling move semantics/references executables 22 | add_executable(references src/references.cpp) 23 | add_executable(move_semantics src/move_semantics.cpp) 24 | add_executable(move_constructors src/move_constructors.cpp) 25 | 26 | # Compiling templates executables 27 | add_executable(templated_functions src/templated_functions.cpp) 28 | add_executable(templated_classes src/templated_classes.cpp) 29 | 30 | # Compiling C++ STL executables 31 | add_executable(vectors src/vectors.cpp) 32 | add_executable(sets src/sets.cpp) 33 | add_executable(unordered_maps src/unordered_maps.cpp) 34 | add_executable(unique_ptr src/unique_ptr.cpp) 35 | add_executable(shared_ptr src/shared_ptr.cpp) 36 | add_executable(mutex src/mutex.cpp) 37 | add_executable(scoped_lock src/scoped_lock.cpp) 38 | add_executable(condition_variable src/condition_variable.cpp) 39 | add_executable(rwlock src/rwlock.cpp) 40 | 41 | # Compiling misc executables 42 | add_executable(wrapper_class src/wrapper_class.cpp) 43 | add_executable(iterator src/iterator.cpp) 44 | add_executable(auto src/auto.cpp) 45 | add_executable(namespaces src/namespaces.cpp) 46 | 47 | # Compiling bootcamp demo code 48 | add_executable(s24_my_ptr src/spring2024/s24_my_ptr.cpp) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 15-445/645 C++ Bootcamp 2 | This bootcamp aims to provide a basic introduction to coding in modern C++. 3 | The features of the C++ language are too vast and expansive to cover in one 4 | bootcamp, and quite frankly, it is learned best through experience. The staff 5 | is certain that 15-445 will make you a more confident C++ programmer! 6 | However, we do cover some C++ topics that are necessary to know while doing 7 | the programming assignments. This tutorial does not cover basic C/C++ syntax. 8 | It mainly covers C++ programming features, particularly concepts that do not exist in C. 9 | 10 | Feedback for the C++ bootcamp is always appreciated! Feel free to submit issues/PRs. 11 | 12 | ## Format 13 | The bootcamp consists of C++ code files, located in `src/`, that are meant 14 | to be read in depth. Each of these files can be compiled into an executable 15 | with the same name. Use CMake to build these executables. This set of commands 16 | should build all the executables. After running these commands, these executables 17 | should be located in the `build` directory. 18 | ```console 19 | $ mkdir build 20 | $ cd build 21 | $ cmake .. 22 | $ make -j8 23 | ``` 24 | For instance, the `src/references.cpp` file compiles into the `references` 25 | executable, located in `./build`. The same holds for every file in the source 26 | directory. 27 | 28 | ## Files 29 | There are fifteen files in the `src/` directory, each which cover different 30 | concepts. They are meant to be read in the order below, since each file 31 | builds up on the previous one. However, if you know some modern C++ concepts 32 | and are looking to refresh your knowledge, it is probably okay to start by 33 | reading the files on concepts you are unfamiliar about. 34 | 35 | ### References and Move Semantics 36 | - `references.cpp`: Covers C++ references. 37 | - `move_semantics.cpp`: Covers C++ move semantics. 38 | - `move_constructors.cpp`: Covers C++ class move constructors and move assignment operators. 39 | 40 | ### C++ Templates 41 | - `templated_functions.cpp`: Covers C++ templated functions. 42 | - `templated_classes.cpp` Covers C++ templated classes. 43 | 44 | ### Misc 45 | - `wrapper_class.cpp`: Covers C++ wrapper classes. 46 | - `iterator.cpp`: Covers implementing a basic C++ style iterator. 47 | - `namespaces.cpp`: Covers C++ namespaces. 48 | 49 | ### C++ Standard Library (STL) Containers 50 | - `vectors.cpp`: Covers `std::vector`. 51 | - `set.cpp`: Covers `std::set`. 52 | - `unordered_map.cpp`: Covers `std::unordered_map`. 53 | - `auto.cpp`: Covers the usage of the C++ keyword `auto`, including using `auto` to iterate through C++ STL containers. 54 | 55 | ### C++ Standard Library (STL) Memory 56 | - `unique_ptr.cpp`: Covers `std::unique_ptr`. 57 | - `shared_ptr.cpp`: Covers `std::shared_ptr`. 58 | 59 | ### C++ Standard Library (STL) Synch Primitives 60 | - `mutex.cpp`: Covers `std::mutex`. 61 | - `scoped_lock.cpp`: Covers `std::scoped_lock`. 62 | - `condition_variable.cpp`: Covers `std::condition_variable`. 63 | - `rwlock.cpp`: Covers the usage of several C++ STL synchronization primitive libraries (`std::shared_mutex`, `std::shared_lock`, `std::unique_lock`) to create a reader-writer's lock implementation. 64 | 65 | ### Demo Code for 15-445/645 Bootcamp 66 | - `spring2024/s24_my_ptr.cpp`: Covers the code used in Spring 2024 bootcamp. 67 | 68 | ## Other Resources 69 | There are many other resources that will be helpful while you get accquainted to C++. 70 | I list a few here! 71 | - [https://en.cppreference.com/w/](https://en.cppreference.com/w/): Unofficial but quite accurate summary and examples of both C++ and C standards. 72 | - [https://cplusplus.com/](https://cplusplus.com/): Contains both a C++ language [tutorial](https://cplusplus.com/doc/tutorial/) and a C++ library [reference](https://cplusplus.com/reference/). 73 | - [Modern C++ Tutorial](https://github.com/changkun/modern-cpp-tutorial). This GitHub repo contains 74 | some information and exercises that are useful! 75 | 76 | ## Appendix: C++ Documentation for Topics Covered in the Bootcamp 77 | This documentation may be useful to you! It's very comprehensive (much more comprehensive than this 78 | bootcamp) but it may lack some readability. Overall, I think it's still a good idea to try to read 79 | and understand this documentation, especially when working on the projects. Although the bootcamp 80 | tries to be as comprehensive as possible, it still only covers the bare bones of using modern C++. 81 | 82 | - [References](https://en.cppreference.com/w/cpp/language/reference) 83 | - [std::move](https://en.cppreference.com/w/cpp/utility/move) 84 | - [Move Constructors](https://en.cppreference.com/w/cpp/language/move_constructor) and [Move Assignment Operators](https://en.cppreference.com/w/cpp/language/move_assignment) 85 | - [Templated Functions](https://en.cppreference.com/w/cpp/language/function_template) 86 | - [Templated Classes](https://en.cppreference.com/w/cpp/language/class_template) 87 | - [Iterators](https://en.cppreference.com/w/cpp/iterator) 88 | - [Namespaces](https://en.cppreference.com/w/cpp/language/namespace) 89 | - [std::vector](https://en.cppreference.com/w/cpp/container/vector) 90 | - [std::set](https://en.cppreference.com/w/cpp/container/set) 91 | - [std::unordered_map](https://en.cppreference.com/w/cpp/container/unordered_map) 92 | - [auto](https://en.cppreference.com/w/cpp/language/auto) 93 | - [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 94 | - [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 95 | - [std::mutex](https://en.cppreference.com/w/cpp/thread/mutex) 96 | - [std::scoped_lock](https://en.cppreference.com/w/cpp/thread/scoped_lock) 97 | - [std::condition_variable](https://en.cppreference.com/w/cpp/thread/condition_variable) 98 | - [std::shared_mutex](https://en.cppreference.com/w/cpp/thread/shared_mutex) 99 | - [std::shared_lock](https://en.cppreference.com/w/cpp/thread/shared_lock) 100 | - [std::unique_lock](https://en.cppreference.com/w/cpp/thread/unique_lock) 101 | -------------------------------------------------------------------------------- /src/auto.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file auto.cpp 3 | * @author Abigale Kim (abigalek) 4 | * @brief Tutorial code for usage of the auto keyword. 5 | */ 6 | 7 | // Includes std::cout (printing) for demo purposes. 8 | #include 9 | // Includes the std::set library. 10 | #include 11 | // Includes the C++ string library. 12 | #include 13 | // Includes the std::vector library. 14 | #include 15 | // Includes the std::unordered map library. 16 | #include 17 | 18 | // The C++ auto keyword is a keyword that tells the compiler to infer the type 19 | // of a declared variable via its initialization expression. It can be 20 | // incredibly useful, as it allows for developer efficiency (where the developer 21 | // no longer has to type out long, unruly type names). It is also useful in the 22 | // context of for-each loops. However, using auto poses a risk where the 23 | // developer may not be aware of the types they are using, and therefore at risk 24 | // for buggy and non functional code. So be careful! 25 | 26 | // Basic templated class with very long name, to show the usefulness of auto. 27 | template class Abcdefghijklmnopqrstuvwxyz { 28 | public: 29 | Abcdefghijklmnopqrstuvwxyz(T instance1, U instance2) 30 | : instance1_(instance1), instance2_(instance2) {} 31 | 32 | void print() const { 33 | std::cout << "(" << instance1_ << "," << instance2_ << ")\n"; 34 | } 35 | 36 | private: 37 | T instance1_; 38 | U instance2_; 39 | }; 40 | 41 | // Templated function that returns an object of this class with a very long 42 | // name. 43 | template 44 | Abcdefghijklmnopqrstuvwxyz construct_obj(T instance) { 45 | return Abcdefghijklmnopqrstuvwxyz(instance, instance); 46 | } 47 | 48 | int main() { 49 | // The auto keyword is used to initialize the variable a. Here, the type 50 | // is inferred to be type int. 51 | auto a = 1; 52 | 53 | // Here are more examples of using auto to declare basic variables. 54 | // Depending on the IDE being used, it might say what types a, b, and c 55 | // are. 56 | auto b = 3.2; 57 | auto c = std::string("Hello"); 58 | 59 | // auto is not particularly useful for these prior examples. As one can 60 | // see, typing int a = 1;, float b = 3.2;, and std::string c = "Hello"; 61 | // does not take significant overhead. However, there will definitely 62 | // be cases where the type name is long and complicated, or when the 63 | // type name is heavily templated, and using auto may be helpful. 64 | Abcdefghijklmnopqrstuvwxyz obj = construct_obj(2); 65 | auto obj1 = construct_obj(2); 66 | 67 | // Maybe for one line it does not seem all that convenient, but imagine 68 | // if using a class with a very long name was useful in the code for 69 | // an extended period of time. Then, I'd imagine it would save a lot of 70 | // typing time! 71 | 72 | // One important thing to note about the auto keyword is that it 73 | // defaults to copying objects, which can lower performance. Take the 74 | // following example where we construct a int vector, and want to 75 | // define a variable that is a reference to it. 76 | std::vector int_values = {1, 2, 3, 4}; 77 | 78 | // The following code deep-copies int_values into copy_int_values, 79 | // since auto infers the type as std::vector, not std::vector&. 80 | auto copy_int_values = int_values; 81 | 82 | // However, the following code defines ref_int_values, which is a reference 83 | // to int_values, and therefore does not deep copy the int_values vector. 84 | auto& ref_int_values = int_values; 85 | 86 | // The auto keyword is also useful for iterating through C++ containers. 87 | // For instance, let's construct an unordered map with std::string keys 88 | // and int values, and discuss methods of iterating through it. 89 | std::unordered_map map; 90 | map.insert({{"andy", 445}, {"jignesh", 645}}); 91 | 92 | // One method mentioned in unordered_map.cpp was to iterate through 93 | // a map by using a for loop with an iterator. Compare the readability 94 | // of the two loops below. 95 | std::cout << "Printing elements in map...\n"; 96 | for (std::unordered_map::iterator it = map.begin(); 97 | it != map.end(); ++it) { 98 | std::cout << "(" << it->first << "," << it->second << ")" 99 | << " "; 100 | } 101 | std::cout << std::endl; 102 | 103 | std::cout << "Printing elements in map with auto...\n"; 104 | for (auto it = map.begin(); it != map.end(); ++it) { 105 | std::cout << "(" << it->first << "," << it->second << ")" 106 | << " "; 107 | } 108 | std::cout << std::endl; 109 | 110 | // It is also possible to use the auto keyword to iterate over vectors 111 | // and sets. 112 | std::vector vec = {1, 2, 3, 4}; 113 | std::cout << "Printing elements in vector with auto...\n"; 114 | for (const auto& elem : vec) { 115 | std::cout << elem << " "; 116 | } 117 | std::cout << std::endl; 118 | 119 | std::set set; 120 | for (int i = 1; i <= 10; ++i) { 121 | set.insert(i); 122 | } 123 | 124 | std::cout << "Printing elements in set with auto...\n"; 125 | for (const auto &elem : set) { 126 | std::cout << elem << " "; 127 | } 128 | std::cout << std::endl; 129 | 130 | // Overall, auto is a useful C++ keyword that can be used to write code more 131 | // efficiently, and to write cleaner and more readable code. 132 | // Keep in mind that using auto to iterate through C++ containers is better 133 | // in practice, since it produces more readable code. However, if you're not 134 | // sure of the types that are being used, it is always okay to revert back 135 | // to figuring out the type yourself. 136 | 137 | return 0; 138 | } -------------------------------------------------------------------------------- /src/condition_variable.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file condition_variable.cpp 3 | * @author Abigale Kim (abigalek) 4 | * @brief Tutorial code for C++ STL condition variable. 5 | */ 6 | 7 | // This program shows a small example of the usage of std::condition_variable. 8 | // The std::condition_variable class provides the condition variable 9 | // synchronization primitive. The condition variable primitive allows threads 10 | // to wait until a particular condition before they grab a mutex. It also 11 | // allows other threads to signal waiting threads to alert them that 12 | // the condition may be true. 13 | 14 | // For a more detailed introduction of C style condition variables, see 15 | // https://pages.cs.wisc.edu/~remzi/OSTEP/threads-cv.pdf. 16 | 17 | // This program runs three threads. Two of these threads run a function that 18 | // increments a global count variable by 1, atomically, and notifies the 19 | // waiting thread when the count variable is 2. When the count variable is 20 | // 2, the waiting thread wakes up, acquires the lock, and prints the count 21 | // value. 22 | 23 | // Includes the condition variable library header. 24 | #include 25 | // Includes std::cout (printing) for demo purposes. 26 | #include 27 | // Includes the mutex library header. 28 | #include 29 | // Includes the thread library header. 30 | #include 31 | 32 | // Defining a global count variable, a mutex, and a condition variable to 33 | // be used by both threads. 34 | int count = 0; 35 | std::mutex m; 36 | 37 | // This is the syntax for declaring and default initializing a condition 38 | // variable. 39 | std::condition_variable cv; 40 | 41 | // In this function, a thread increments the count variable by 42 | // 1. It also will notify one waiting thread if the count value is 2. 43 | // It is ran by two of the threads in the main function. 44 | void add_count_and_notify() { 45 | std::scoped_lock slk(m); 46 | count += 1; 47 | if (count == 2) { 48 | cv.notify_one(); 49 | } 50 | } 51 | 52 | // This function, ran by the waiting thread, waits on the condition 53 | // count == 2. After that, it grabs the mutex m and executes code in 54 | // the critical section. 55 | // Condition variables need an std::unique_lock object to be constructed. 56 | // std::unique_lock is a type of C++ STL synchronization primitive that 57 | // gives more flexibility and features, including the usage with 58 | // condition variables. Particularly, it is moveable but not copy-constructible 59 | // or copy-assignable. 60 | void waiter_thread() { 61 | std::unique_lock lk(m); 62 | cv.wait(lk, []{return count == 2;}); 63 | 64 | std::cout << "Printing count: " << count << std::endl; 65 | } 66 | 67 | // The main method constructs three thread objects and has two of them run the 68 | // add_count_and_notify function in parallel. After these threads are finished 69 | // executing, we print the count value, from the waiter thread, showing that 70 | // both increments, along with the conditional acquisition in the waiter 71 | // thread, worked successfully. 72 | int main() { 73 | std::thread t1(add_count_and_notify); 74 | std::thread t2(add_count_and_notify); 75 | std::thread t3(waiter_thread); 76 | t1.join(); 77 | t2.join(); 78 | t3.join(); 79 | return 0; 80 | } -------------------------------------------------------------------------------- /src/iterator.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file iterator.cpp 3 | * @author Abigale Kim (abigalek) 4 | * @brief Tutorial code on the usage and creation of iterators. 5 | */ 6 | 7 | // C++ iterators are objects that point to an element inside a container. 8 | // They can be used to iterate through the objects of that container. 9 | // One example of an iterator that you know is a pointer. A pointer 10 | // can be used to iterate through a C style array. Take the following 11 | // C-style code snippet: 12 | // int *array = malloc(sizeof(int) * 10); 13 | // int *iter = array; 14 | // int zero_elem = *iter; 15 | // iter++; 16 | // int first_elem = *iter; 17 | // As we can see, the ++ operator can be used to iterate through the 18 | // C style array, and the derefence operator returns the value at the 19 | // iterator. 20 | 21 | // The main components of a C++ iterator are its main two operators. The 22 | // dereference operator (*) on an iterator should return the value of the 23 | // element at the current position of the iterator. The ++ (postfix increment) 24 | // operator should increment the iterator's position by 1. As you can see, this 25 | // is true with the pointer being used to iterate through a C style array. 26 | 27 | // There are a few examples about how to use iterators to access elements 28 | // in C++ STL containers in vectors.cpp, sets.cpp, unordered_maps.cpp, 29 | // and auto.cpp. This is because using iterators in C++ to access and modify 30 | // elements in C++ STL containers is considered good style, and worth 31 | // mentioning in these files. 32 | 33 | // This file will mainly focus on the implementation of iterators. In this 34 | // file, we demonstrate implementing C++ iterators by writing a basic doubly 35 | // linked list (DLL) iterator. 36 | 37 | // Includes std::cout (printing) for demo purposes. 38 | #include 39 | 40 | // This is the definition of the Node struct, used in our DLL. 41 | struct Node { 42 | Node(int val) 43 | : next_(nullptr) 44 | , prev_(nullptr) 45 | , value_(val) {} 46 | 47 | Node* next_; 48 | Node* prev_; 49 | int value_; 50 | }; 51 | 52 | // This class implements a C++ style iterator for the doubly linked list class 53 | // DLL. This class's constructor takes in a node that marks the start of the 54 | // iterating. It also implements several operators that increment the iterator 55 | // (i.e. accessing the next element in the DLL) and test for equality between 56 | // two different iterators by comparing their curr_ pointers. 57 | class DLLIterator { 58 | public: 59 | DLLIterator(Node* head) 60 | : curr_(head) {} 61 | 62 | // Implementing a prefix increment operator (++iter). 63 | DLLIterator& operator++() { 64 | curr_ = curr_->next_; 65 | return *this; 66 | } 67 | 68 | // Implementing a postfix increment operator (iter++). The difference 69 | // between a prefix and postfix increment operator is the return value 70 | // of the operator. The prefix operator returns the result of the 71 | // increment, while the postfix operator returns the iterator before 72 | // the increment. 73 | DLLIterator operator++(int) { 74 | DLLIterator temp = *this; 75 | ++*this; 76 | return temp; 77 | } 78 | 79 | // This is the equality operator for the DLLIterator class. It 80 | // tests that the current pointers are the same. 81 | bool operator==(const DLLIterator &itr) const { 82 | return itr.curr_ == this->curr_; 83 | } 84 | 85 | // This is the inequality operator for the DLLIterator class. It 86 | // tests that the current pointers are not the same. 87 | bool operator!=(const DLLIterator &itr) const { 88 | return itr.curr_ != this->curr_; 89 | } 90 | 91 | // This is the dereference operator for the DLLIterator class. It 92 | // returns the value of the element at the current position of the 93 | // iterator. The current position of the iterator is marked by curr_, 94 | // and we can access the value of curr_ by accessing its value field. 95 | int operator*() { 96 | return curr_->value_; 97 | } 98 | 99 | private: 100 | Node* curr_; 101 | }; 102 | 103 | // This is a basic implementation of a doubly linked list. It also includes 104 | // iterator functions Begin and End, which return DLLIterators that can be 105 | // used to iterate through this DLL instance. 106 | class DLL { 107 | public: 108 | // DLL class constructor. 109 | DLL() 110 | : head_(nullptr) 111 | , size_(0) {} 112 | 113 | // Destructor should delete all the nodes by iterating through them. 114 | ~DLL() { 115 | Node *current = head_; 116 | while(current != nullptr) { 117 | Node *next = current->next_; 118 | delete current; 119 | current = next; 120 | } 121 | head_ = nullptr; 122 | } 123 | 124 | // Function for inserting val at the head of the DLL. 125 | void InsertAtHead(int val) { 126 | Node *new_node = new Node(val); 127 | new_node->next_ = head_; 128 | 129 | if (head_ != nullptr) { 130 | head_->prev_ = new_node; 131 | } 132 | 133 | head_ = new_node; 134 | size_ += 1; 135 | } 136 | 137 | // The Begin() function returns an iterator to the head of the DLL, 138 | // which is the first element to access when iterating through. 139 | DLLIterator Begin() { 140 | return DLLIterator(head_); 141 | } 142 | 143 | // The End() function returns an iterator that marks the one-past-the-last 144 | // element of the iterator. In this case, this would be an iterator with 145 | // its current pointer set to nullptr. 146 | DLLIterator End() { 147 | return DLLIterator(nullptr); 148 | } 149 | 150 | Node* head_{nullptr}; 151 | size_t size_; 152 | }; 153 | 154 | // The main function shows the usage of the DLL iterator. 155 | int main() { 156 | // Creating a DLL and inserting elements into it. 157 | DLL dll; 158 | dll.InsertAtHead(6); 159 | dll.InsertAtHead(5); 160 | dll.InsertAtHead(4); 161 | dll.InsertAtHead(3); 162 | dll.InsertAtHead(2); 163 | dll.InsertAtHead(1); 164 | 165 | // We can iterate through our DLL via both our prefix and postfix operators. 166 | std::cout << "Printing elements of the DLL dll via prefix increment operator\n"; 167 | for (DLLIterator iter = dll.Begin(); iter != dll.End(); ++iter) { 168 | std::cout << *iter << " "; 169 | } 170 | std::cout << std::endl; 171 | 172 | std::cout << "Printing elements of the DLL dll via postfix increment operator\n"; 173 | for (DLLIterator iter = dll.Begin(); iter != dll.End(); iter++) { 174 | std::cout << *iter << " "; 175 | } 176 | std::cout << std::endl; 177 | 178 | return 0; 179 | } -------------------------------------------------------------------------------- /src/move_constructors.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file move_constructors.cpp 3 | * @author Abigale Kim (abigalek) 4 | * @brief Tutorial code for move constructors and move assignment operators. 5 | */ 6 | 7 | // Move constructors and move assignment operators are methods implemented 8 | // inside of classes to effectively move resources from one object to the 9 | // other, typically using std::move. These class methods take in another 10 | // object of the same type, and move its resources to the instance 11 | // where the method is called. In this file, we will explore implementing 12 | // and using move constructors and move assignment operators. 13 | 14 | // Includes std::cout (printing) for demo purposes. 15 | #include 16 | // Includes the utility header for std::move. 17 | #include 18 | // Includes the C++ string library. 19 | #include 20 | // Includes the header for uint32_t. 21 | #include 22 | // Includes the header for std::vector. We'll cover vectors more in 23 | // containers.cpp, but what suffices to know for now is that vectors are 24 | // essentially dynamic arrays, and the type std::vector is an array 25 | // of strings. Mainly, vectors take up a non-negligible amount of memory, and 26 | // are here to show the performance benefits of using std::move. 27 | #include 28 | 29 | // Basic person class, with an implemented move constructor and move assignment 30 | // operator, and a deleted copy constructor and copy assignment operator. This 31 | // means that once an Person object is instantiated, it cannot be copied. It 32 | // must be moved from one lvalue to another. Classes without copy operators are 33 | // useful when it is imperative to only have one defined instance of a class. 34 | // For instance, if a class manages a dynamically allocated memory block, then 35 | // creating more than one instance of this class, without proper handling, can 36 | // result in double deletion or memory leaks. 37 | class Person { 38 | public: 39 | Person() : age_(0), nicknames_({}), valid_(true) {} 40 | 41 | // Keep in mind that this constructor takes in a std::vector 42 | // rvalue. This makes the constructor more efficient because it doesn't deep 43 | // copy the vector instance when constructing the person object. 44 | Person(uint32_t age, std::vector &&nicknames) 45 | : age_(age), nicknames_(std::move(nicknames)), valid_(true) {} 46 | 47 | // Move constructor for class Person. It takes in a rvalue with type Person, 48 | // and moves the contents of the rvalue passed in as an argument to this 49 | // Person object instance. Note the usage of std::move. In order to ensure 50 | // that nicknames in object person is moved, and not deep copied, we use 51 | // std::move. std::move will cast the lvalue person.nicknames_ to an rvalue, 52 | // which represents the value itself. Also note that I don't call std::move 53 | // on the age_ field. Since it's an integer type, it's too small to incur a 54 | // significant copying cost. Generally, for numeric types, it's okay to copy 55 | // them, but for other types, such as strings and object types, one should 56 | // move the class instance unless copying is necessary. 57 | Person(Person &&person) 58 | : age_(person.age_), nicknames_(std::move(person.nicknames_)), 59 | valid_(true) { 60 | std::cout << "Calling the move constructor for class Person.\n"; 61 | // The moved object's validity tag is set to false. 62 | person.valid_ = false; 63 | } 64 | 65 | // Move assignment operator for class Person. 66 | Person &operator=(Person &&other) { 67 | std::cout << "Calling the move assignment operator for class Person.\n"; 68 | age_ = other.age_; 69 | nicknames_ = std::move(other.nicknames_); 70 | valid_ = true; 71 | 72 | // The moved object's validity tag is set to false. 73 | other.valid_ = false; 74 | return *this; 75 | } 76 | 77 | // We delete the copy constructor and the copy assignment operator, 78 | // so this class cannot be copy-constructed. 79 | Person(const Person &) = delete; 80 | Person &operator=(const Person &) = delete; 81 | 82 | uint32_t GetAge() { return age_; } 83 | 84 | // This ampersand at the return type implies that we return a reference 85 | // to the string at nicknames_[i]. This also implies that we don't copy 86 | // the resulting string, and the memory address this returns under the 87 | // hood is actually the one pointing to the nicknames_ vector's memory. 88 | std::string &GetNicknameAtI(size_t i) { return nicknames_[i]; } 89 | 90 | void PrintValid() { 91 | if (valid_) { 92 | std::cout << "Object is valid." << std::endl; 93 | } else { 94 | std::cout << "Object is invalid." << std::endl; 95 | } 96 | } 97 | 98 | private: 99 | uint32_t age_; 100 | std::vector nicknames_; 101 | // Keeping track of whether an object's data is valid, i.e. whether 102 | // all of its data has been moved to another instance. 103 | bool valid_; 104 | }; 105 | 106 | int main() { 107 | // Let's see how move constructors and move assignment operators can be 108 | // implemented and used in a class. First, we create an instance of the class 109 | // Person. Note that the object andy is a valid object. 110 | Person andy(15445, {"andy", "pavlo"}); 111 | std::cout << "Printing andy's validity: "; 112 | andy.PrintValid(); 113 | 114 | // To move the contents of the andy object to another object, we can use 115 | // std::move in a couple ways. This method calls the move assignment operator. 116 | Person andy1; 117 | andy1 = std::move(andy); 118 | 119 | // Note that andy1 is valid, while andy is not a valid object. 120 | std::cout << "Printing andy1's validity: "; 121 | andy1.PrintValid(); 122 | std::cout << "Printing andy's validity: "; 123 | andy.PrintValid(); 124 | 125 | // This method calls the move constructor. After this operation, the contents 126 | // of the original andy object have moved to andy1, then moved to andy2. The 127 | // andy and andy1 lvalues are effectively defunct (and should not be used, 128 | // unless they are re-initialized). 129 | Person andy2(std::move(andy1)); 130 | 131 | // Note that andy2 is valid, while andy1 is not a valid object. 132 | std::cout << "Printing andy2's validity: "; 133 | andy2.PrintValid(); 134 | std::cout << "Printing andy1's validity: "; 135 | andy1.PrintValid(); 136 | 137 | // However, note that because the copy assignment operator is deleted, this code 138 | // will not compile. The first line of this code constructs a new object via the 139 | // default constructor, and the second line invokes the copy assignment operator 140 | // to re-initialize andy3 with the deep-copied contents of andy2. Try uncommenting 141 | // these lines of code to see the resulting compiler errors. 142 | // Person andy3; 143 | // andy3 = andy2; 144 | 145 | // Because the copy constructor is deleted, this code will not compile. Try 146 | // uncommenting this code to see the resulting compiler errors. 147 | // Person andy4(andy2); 148 | 149 | return 0; 150 | } 151 | -------------------------------------------------------------------------------- /src/move_semantics.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file move_semantics.cpp 3 | * @author Abigale Kim (abigalek) 4 | * @brief Tutorial code for move semantics. 5 | */ 6 | 7 | // Move semantics in C++ are a useful concept that allows for the efficient 8 | // and optimized transfer of ownership of data between objects. One of the 9 | // main goals of move semantics is to increase performance, since moving an 10 | // object is faster and more efficient than deep copying the object. 11 | 12 | // To understand move semantics, one must understand the concept of lvalues 13 | // and rvalues. A simplified definition of lvalues is that lvalues are objects 14 | // that refer to a location in memory. Rvalues are anything that is not a 15 | // lvalue. 16 | 17 | // std::move is the most common way of moving an object from one lvalue to 18 | // another. std::move casts an expression to a rvalue. This allows for us to 19 | // interact with a lvalue as a rvalue, and allows for the ownership to be 20 | // transferred from one lvalue to another. 21 | 22 | // In the code below, we include some examples for identifying whether 23 | // expressions in C++ are lvalues or rvalues, how to use std::move, and passing 24 | // rvalues references into functions. 25 | 26 | // Includes std::cout (printing) for demo purposes. 27 | #include 28 | // Includes the utility header for std::move. 29 | #include 30 | // Includes the header for std::vector. We'll cover vectors more in 31 | // containers.cpp, but what suffices to know for now is that vectors are 32 | // essentially dynamic arrays, and the type std::vector is an array of 33 | // ints. Mainly, vectors take up a non-negligible amount of memory, and are here 34 | // to show the performance benefits of using std::move. 35 | #include 36 | 37 | // Function that takes in a rvalue reference as an argument. 38 | // It seizes ownership of the vector passed in, appends 3 to 39 | // the back of it, and prints the values in the vector. 40 | void move_add_three_and_print(std::vector &&vec) { 41 | std::vector vec1 = std::move(vec); 42 | vec1.push_back(3); 43 | for (const int &item : vec1) { 44 | std::cout << item << " "; 45 | } 46 | std::cout << "\n"; 47 | } 48 | 49 | // Function that takes in a rvalue reference as an argument. 50 | // It appends 3 to the back of the vector passed in as an argument, 51 | // and prints the values in the vector. Notably, it does not seize 52 | // ownership of the vector. Therefore, the argument passed in would 53 | // still be usable in the callee context. 54 | void add_three_and_print(std::vector &&vec) { 55 | vec.push_back(3); 56 | for (const int &item : vec) { 57 | std::cout << item << " "; 58 | } 59 | std::cout << "\n"; 60 | } 61 | 62 | int main() { 63 | // Take this expression. Note that 'a' is a lvalue, since it's a variable that 64 | // refers to a specific space in memory (where 'a' is stored). 10 is a rvalue. 65 | int a = 10; 66 | 67 | // Let's see a basic example of moving data from one lvalue to another. 68 | // We define a vector of integers here. 69 | std::vector int_array = {1, 2, 3, 4}; 70 | 71 | // Now, we move the values of this array to another lvalue. 72 | std::vector stealing_ints = std::move(int_array); 73 | 74 | // Rvalue references are references that refer to the data itself, as opposed 75 | // to a lvalue. Calling std::move on a lvalue (such as stealing_ints) will 76 | // result in the expression being cast to a rvalue reference. 77 | std::vector &&rvalue_stealing_ints = std::move(stealing_ints); 78 | 79 | // However, note that after this, it is still possible to access the data in 80 | // stealing_ints, since that is the lvalue that owns the data, not 81 | // rvalue_stealing_ints. 82 | std::cout << "Printing from stealing_ints: " << stealing_ints[1] << std::endl; 83 | 84 | // It is possible to pass in a rvalue reference into a function. However, 85 | // once the rvalue is moved from the lvalue in the caller context to a lvalue 86 | // in the callee context, it is effectively unusable to the caller. 87 | // Essentially, after move_add_three_and_print is called, we cannot use the 88 | // data in int_array2. It no longer belongs to the int_array2 lvalue. 89 | std::vector int_array2 = {1, 2, 3, 4}; 90 | std::cout << "Calling move_add_three_and_print...\n"; 91 | move_add_three_and_print(std::move(int_array2)); 92 | 93 | // It would be unwise to try to do anything with int_array2 here. Uncomment 94 | // the code to try it out! (On my machine, this segfaults...) NOTE: THIS MIGHT 95 | // WORK FOR YOU. THIS DOES NOT MEAN THAT THIS IS WISE TO DO! 96 | // std::cout << int_array2[1] << std::endl; 97 | 98 | // If we don't move the lvalue in the caller context to any lvalue in the 99 | // callee context, then effectively the function treats the rvalue reference 100 | // passed in as a reference, and the lvalue in this context still owns the 101 | // vector data. 102 | std::vector int_array3 = {1, 2, 3, 4}; 103 | std::cout << "Calling add_three_and_print...\n"; 104 | add_three_and_print(std::move(int_array3)); 105 | 106 | // As seen here, we can print from this array. 107 | std::cout << "Printing from int_array3: " << int_array3[1] << std::endl; 108 | 109 | return 0; 110 | } 111 | -------------------------------------------------------------------------------- /src/mutex.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file mutex.cpp 3 | * @author Abigale Kim (abigalek) 4 | * @brief Tutorial code for C++ STL mutex. 5 | */ 6 | 7 | // This program shows a small example of the usage of std::mutex. The 8 | // std::mutex class provides the mutex synchronization primitive. 9 | 10 | // Includes std::cout (printing) for demo purposes. 11 | #include 12 | // Includes the mutex library header. 13 | #include 14 | // Includes the thread library header. 15 | #include 16 | 17 | // Defining a global count variable and a mutex to be used by both threads. 18 | int count = 0; 19 | 20 | // This is the syntax for declaring and default initializing a mutex. 21 | std::mutex m; 22 | 23 | // The add_count function allows for a thread to increment the count variable 24 | // by 1, atomically. 25 | void add_count() { 26 | // Acquire the lock before accessing count, the shared resource. 27 | m.lock(); 28 | count += 1; 29 | // Release the lock after accessing count, the shared resource. 30 | m.unlock(); 31 | } 32 | 33 | // The main method constructs two thread objects and has them both run the 34 | // add_count function in parallel. After these threads are finished executing, 35 | // we print the count value, showing that both increments worked successfully. 36 | // The std::thread library is the C++ STL library used to construct threads. 37 | // You may view it as a C++ equivalent of the pthread library in C. 38 | int main() { 39 | std::thread t1(add_count); 40 | std::thread t2(add_count); 41 | t1.join(); 42 | t2.join(); 43 | 44 | std::cout << "Printing count: " << count << std::endl; 45 | return 0; 46 | } -------------------------------------------------------------------------------- /src/namespaces.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file namespaces.cpp 3 | * @author Abigale Kim (abigalek) 4 | * @brief Tutorial code on the usage of namespaces. 5 | */ 6 | 7 | // Namespaces provide scope to identifiers (the names of functions, types, 8 | // variables). They are used to organize code into logical groups (e.g. a 9 | // library might be one namespace). They also can prevent naming collisons 10 | // between different identifiers. For instance, the C++ standard library 11 | // uses the std namespace. This is why the print function in C++ is identified 12 | // by std::cout, because the cout function is declared in the std 13 | // namespace. C++ uses the :: operator for scope resolution, and therefore 14 | // it is useful in differentiating which namespace a function, type, 15 | // or class is declared in. 16 | 17 | // In this file, we will introduce namespaces, namespace syntax, the using 18 | // keyword, and calling functions from other namespaces. 19 | 20 | // Includes std::cout (printing) for demo purposes. 21 | #include 22 | 23 | // This is the syntax to declare a namespace. 24 | namespace ABC { 25 | // We define a function spam in the ABC namespace. This is used in line 57. 26 | void spam(int a) { 27 | std::cout << "Hello from ABC::spam: " << a << std::endl; 28 | } 29 | 30 | // namespace DEF is a nested namespace, because it is declared inside namespace 31 | // ABC. The syntax for declaring a nested namespace is identical to the syntax of 32 | // declaring a non-nested namespace. 33 | namespace DEF { 34 | // We define a function bar inside the N::M namespace. 35 | void bar(float a) { 36 | std::cout << "Hello from ABC::DEF::bar: " << a << std::endl; 37 | } 38 | 39 | // We define a function uses_bar inside the ABC::DEF namespace. However, since 40 | // bar is in the same namespace as uses_bar (they're both in the ABC::DEF 41 | // namespace), the function bar is just referred to by its name in the 42 | // function uses_bar. 43 | void uses_bar(float a) { 44 | std::cout << "Hello from uses_bar: "; 45 | bar(a); 46 | } 47 | 48 | // We define a function uses_spam in the ABC::DEF namespace. To refer to 49 | // ABC::spam from the ABC::DEF namespace, we have no other option but to 50 | // refer to it by its full identifier. Attempting to refer to it by spam 51 | // will result in a compilation error, stating that no function called spam 52 | // or ABC::DEF::spam exists. Note that it is possible to refer to every 53 | // function by its full identifier, but doing this makes coding speed 54 | // inefficient. 55 | void uses_spam(int a) { 56 | std::cout << "Hello from uses_spam: "; 57 | ABC::spam(a); 58 | 59 | // Try uncommenting this code, which calls spam(a), here. 60 | // spam(a); 61 | } 62 | } 63 | 64 | // We define a function uses_DEF_bar inside the ABC namespace, but not inside the 65 | // DEF namespace. Since bar and uses_DEF_bar are both in the ABC namespace, the 66 | // function bar is referred to by DEF::bar (which is the differentiating 67 | // namespace) in the uses_DEF_bar function. 68 | void uses_DEF_bar(float a) { 69 | std::cout << "Hello from uses_DEF_bar: "; 70 | DEF::bar(a); 71 | } 72 | } 73 | 74 | // Both namespace A and namespace B have a function named foo with the same 75 | // arguments and return value. This code will compile because overall, the 76 | // two foo functions have different full identifiers, which are A::foo and 77 | // B::foo. 78 | namespace A { 79 | void foo(int a) { 80 | std::cout << "Hello from A::foo: " << a << std::endl; 81 | } 82 | } 83 | 84 | namespace B { 85 | void foo(int a) { 86 | std::cout << "Hello from B::foo: " << a << std::endl; 87 | } 88 | 89 | void peloton(int a) { 90 | std::cout << "Hello from B::peloton: " << a << std::endl; 91 | } 92 | } 93 | 94 | namespace C { 95 | void eggs(int a) { 96 | std::cout << "Hello from C::eggs: " << a << std::endl; 97 | } 98 | } 99 | 100 | 101 | // One of the uses of the using keyword is to bring the current namespace into 102 | // the current scope. This statement will bring namespace B into the current 103 | // scope. This means that B::foo can be referred to as foo in code anywhere 104 | // below this line of code. In main, we will see that B::foo can be referred to 105 | // by B::foo and by foo. 106 | using namespace B; 107 | 108 | // Another use of the using keyword is to bring certain members of a namespace 109 | // into the current scope. This statement will bring C::eggs into the current 110 | // scope. This means that eggs can be referred to as eggs anywhere below this 111 | // line of code. 112 | using C::eggs; 113 | 114 | int main() { 115 | // The following line of code calls function spam (line 25). Calling spam(2) 116 | // will not work, as there is no function with the identifier spam, just 117 | // ABC::spam. 118 | ABC::spam(2); 119 | 120 | // The following line of code calls function bar. 121 | ABC::DEF::bar(4.45); 122 | 123 | // The following line of code calls uses_bar. As expected, uses_bar will 124 | // print "Hello from uses_bar", and then call ABC::DEF::bar. 125 | ABC::DEF::uses_bar(6.45); 126 | 127 | // The following line of code calls uses_spam. As expected, uses_spam will 128 | // print "Hello from uses_spam", and then call ABC::spam. 129 | ABC::DEF::uses_spam(37); 130 | 131 | // The following line of code calls uses_DEF_bar. As expected, uses_DEF_bar 132 | // will print "Hello from uses_DEF_bar", and then call ABC::DEF::bar. 133 | ABC::uses_DEF_bar(3.12); 134 | 135 | // Now, let's talk the two foo functions. A::foo and B::foo are different 136 | // functions with the same argument, and yet they are allowed to coexist, 137 | // since they have different identifiers. 138 | A::foo(122); 139 | B::foo(150); 140 | 141 | // However, since line 106 brought the B namespace into the current scope, 142 | // it is possible to access anything from the B namespace without the B 143 | // namespace identifier. Therefore, it is possible to call B::foo by 144 | // identifying it as foo. 145 | foo(440); 146 | 147 | // Likewise, since the entire namespace B was brought into the current 148 | // scope, it is also possible to refer to B::peloton, another function in 149 | // namespace B, as peloton. 150 | peloton(721); 151 | 152 | // Keep in mind that using the using keyword to bring an entire namespace 153 | // to the current scope can be risky, as you can risk naming conflicts 154 | // if you are not careful. Therefore, we don't recommend you doing this. 155 | // However, it is possible to bring certain members of a namespace to 156 | // the current scope, such as the example on line 111. Here, we refer 157 | // to C::eggs by identifying it as eggs. 158 | eggs(999); 159 | 160 | return 0; 161 | } 162 | -------------------------------------------------------------------------------- /src/references.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file references.cpp 3 | * @author Abigale Kim (abigalek) 4 | * @brief Tutorial code for references. 5 | */ 6 | 7 | // A reference in C++ is a method of creating an alias to a variable, where 8 | // these aliases refer to the same data in memory. References are useful for 9 | // keeping track of state, passing arguments into functions, and for general 10 | // performance improvements. In general, it is important to understand 11 | // references to do well in this class. 12 | 13 | // Includes std::cout (printing) for demo purposes. 14 | #include 15 | 16 | // A function that takes an int reference and adds 3 to it. 17 | void add_three(int &a) { a = a + 3; } 18 | 19 | int main() { 20 | // Take this expression. Note that b has type int& (int reference), 21 | // since it is a reference to a. This means that a and b both refer to the 22 | // same data. You can declare references by setting your variables type via 23 | // the single ampersand syntax. 24 | int a = 10; 25 | int &b = a; 26 | 27 | // As stated, if we try to print b, we will get 10. 28 | std::cout << "b is " << b << std::endl; 29 | 30 | // References can also be passed into functions. Take the function add_three, 31 | // which takes in an int reference and adds 3 to it. If we call this function, 32 | // on a, since a is being taken as a reference, then a's value in the caller 33 | // context will change value. 34 | add_three(a); 35 | std::cout << "a is " << a << std::endl; 36 | 37 | return 0; 38 | } -------------------------------------------------------------------------------- /src/rwlock.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file rwlock.cpp 3 | * @author Abigale Kim (abigalek) 4 | * @brief Tutorial code for C++ STL std::shared_lock and std::unique_lock 5 | * (particularly usage of them as RWLocks). 6 | */ 7 | 8 | // Although C++ does not have a specific reader-writer's lock library, it is 9 | // possible to emulate one by using the std::shared_mutex, std::shared_lock, 10 | // and std::unique_lock libraries. This program shows a small example on how 11 | // to do this. 12 | 13 | // The std::shared_mutex is a mutex that allows for both shared, read-only 14 | // locking, and exclusive, write-only locking. std::shared_lock can be used 15 | // as an RAII-style read lock, and std::unique_lock can be used as a RAII-style 16 | // write lock. scoped_lock.cpp talks about RAII-style locking in C++. 17 | 18 | // If you would prefer to review the conceptuals of readers-writers locks and 19 | // the reader-writers problem, you can refer to the 15-213/513/613 slides here: 20 | // https://www.cs.cmu.edu/afs/cs/academic/class/15213-s23/www/lectures/25-sync-advanced.pdf 21 | 22 | // Includes std::cout (printing) for demo purposes. 23 | #include 24 | // Includes the mutex library header. 25 | #include 26 | // Includes the shared mutex library header. 27 | #include 28 | // Includes the thread library header. 29 | #include 30 | 31 | // Defining a global count variable and a shared mutex to be used by all threads. 32 | // The std::shared_mutex is a mutex that allows for shared locking, as well as 33 | // exclusive locking. 34 | int count = 0; 35 | std::shared_mutex m; 36 | 37 | // This function uses a std::shared_lock (reader lock equivalent) to gain 38 | // read only, shared access to the count variable, and reads the count 39 | // variable. 40 | void read_value() { 41 | std::shared_lock lk(m); 42 | std::cout << "Reading value " + std::to_string(count) + "\n" << std::flush; 43 | } 44 | 45 | // This function uses a std::unique_lock (write lock equivalent) to gain 46 | // exclusive access to the count variable and write to the value. 47 | void write_value() { 48 | std::unique_lock lk(m); 49 | count += 3; 50 | } 51 | 52 | // The main method constructs six thread objects and has two of them run the 53 | // write_value function, and four of them run the read_value function, all 54 | // in parallel. This means that the output is not deterministic, depending 55 | // on which threads grab the lock first. Run the program a few times, and 56 | // see if you can get different outputs. 57 | int main() { 58 | std::thread t1(read_value); 59 | std::thread t2(write_value); 60 | std::thread t3(read_value); 61 | std::thread t4(read_value); 62 | std::thread t5(write_value); 63 | std::thread t6(read_value); 64 | 65 | t1.join(); 66 | t2.join(); 67 | t3.join(); 68 | t4.join(); 69 | t5.join(); 70 | t6.join(); 71 | 72 | return 0; 73 | } -------------------------------------------------------------------------------- /src/scoped_lock.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file scoped_lock.cpp 3 | * @author Abigale Kim (abigalek) 4 | * @brief Tutorial code for C++ STL scoped lock. 5 | */ 6 | 7 | // This program provides a small example of the use of std::scoped_lock. 8 | // std::scoped_lock is a mutex wrapper class that provides a RAII-style 9 | // method of obtaining and releasing locks. This means that when the object 10 | // is constructed, the locks are acquired, and when the object is destructed, 11 | // the locks are released. 12 | 13 | // Includes std::cout (printing) for demo purposes. 14 | #include 15 | // Includes the mutex library header. 16 | #include 17 | // Includes the thread library header. 18 | #include 19 | 20 | // Defining a global count variable and two mutexes to be used by both threads. 21 | int count = 0; 22 | std::mutex m; 23 | 24 | // The add_count function allows for a thread to increment the count variable 25 | // by 1, atomically. 26 | void add_count() { 27 | // The constructor of std::scoped_lock allows for the thread to acquire the 28 | // mutex m. 29 | std::scoped_lock slk(m); 30 | count += 1; 31 | 32 | // Once the function add_count finishes, the object slk is out of scope, and 33 | // in its destructor, the mutex m is released. 34 | } 35 | 36 | // The main method is identical to the one in mutex.cpp. It constructs the 37 | // thread objects, runs add_count on both threads, and prints the result of 38 | // count after execution. 39 | int main() { 40 | std::thread t1(add_count); 41 | std::thread t2(add_count); 42 | t1.join(); 43 | t2.join(); 44 | 45 | std::cout << "Printing count: " << count << std::endl; 46 | return 0; 47 | } -------------------------------------------------------------------------------- /src/sets.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file sets.cpp 3 | * @author Abigale Kim (abigalek) 4 | * @brief Tutorial code for C++ Standard Library (STL) sets. 5 | */ 6 | 7 | // In this file, we will introduce the container std::set. We won't be able 8 | // to cover every function in this container, but we will try to cover the 9 | // basics of using this container. Look to the intro of vectors.cpp for 10 | // a general overview of C++ STL containers. 11 | 12 | // The std::set container is a data structure that contains a sorted set of 13 | // unique objects of a single type. It is usually implemented as a Red-Black 14 | // tree, if that helps you conceptualize the std::set. The std::set container 15 | // is used to maintain of a set of unique elements. 16 | 17 | // There is documentation on all the other functions, and other containers 18 | // on https://en.cppreference.com/w/cpp/container. You will definitely need this 19 | // resource as you complete the assignments in this class, so you should check 20 | // it out! 21 | 22 | // Includes std::cout (printing) for demo purposes. 23 | #include 24 | // Includes the set container library header. 25 | #include 26 | 27 | int main() { 28 | // We can declare a int set with the following syntax. 29 | std::set int_set; 30 | 31 | // To insert elements, we use the insert function. Here we insert elements 1 32 | // through 10 in our set. There also exists an emplace function that allows 33 | // the user to construct objects in place for set insertion. We cover emplace 34 | // more in vectors.cpp (line 73). 35 | for (int i = 1; i <= 5; ++i) { 36 | int_set.insert(i); 37 | } 38 | 39 | for (int i = 6; i <= 10; ++i) { 40 | int_set.emplace(i); 41 | } 42 | 43 | // To find an element, we can use the find function, which returns an 44 | // iterator that points to the element within the set with the key that is 45 | // equivalent to the key argument. We can then check to see whether this 46 | // returned iterator is equivalent to the end iterator. If the returned 47 | // iterator value is equivalent to the end iterator value, then this would 48 | // imply that the element does not exist. 49 | std::set::iterator search = int_set.find(2); 50 | if (search != int_set.end()) { 51 | std::cout << "Element 2 is in int_set.\n"; 52 | } 53 | 54 | // We can also use the count function, which returns the number of elements 55 | // with the specified key in the set. 56 | if (int_set.count(11) == 0) { 57 | std::cout << "Element 11 is not in the set.\n"; 58 | } 59 | 60 | if (int_set.count(3) == 1) { 61 | std::cout << "Element 3 is in the set.\n"; 62 | } 63 | 64 | // To erase an element, we can use the erase function. The erase function can 65 | // firstly, take a key to erase. For instance, if we want to erase 4 from the 66 | // set, we can call: 67 | int_set.erase(4); 68 | 69 | // We confirm that 4 isn't in the set anymore. 70 | if (int_set.count(4) == 0) { 71 | std::cout << "Element 4 is not in the set.\n"; 72 | } 73 | 74 | // Additionally, if we want to erase an element at a certain position, we can 75 | // pass in an iterator to the erase function. Let's say we want to erase the 76 | // first element from the set. We can pass in an iterator that points to the 77 | // first element from the set to the erase function. 78 | int_set.erase(int_set.begin()); 79 | 80 | // We confirm that 1 isn't in the set anymore. 81 | if (int_set.count(1) == 0) { 82 | std::cout << "Element 1 is not in the set.\n"; 83 | } 84 | 85 | // Lastly, we can erase elements in the set by passing in an iterator range to 86 | // the erase function. For instance, if we want to erase elements that are 87 | // greater than or equal to 9 (so, 9 and 10), we call the following. 88 | int_set.erase(int_set.find(9), int_set.end()); 89 | 90 | // We confirm that 9 and 10 aren't in the set anymore. 91 | if (int_set.count(9) == 0 && int_set.count(10) == 0) { 92 | std::cout << "Elements 9 and 10 are not in the set.\n"; 93 | } 94 | 95 | // We can iterate through the set elements via the set iterator. You cannot 96 | // iterate through a set via indexes of any kind. 97 | std::cout << "Printing the elements of the iterator:\n"; 98 | for (std::set::iterator it = int_set.begin(); it != int_set.end(); 99 | ++it) { 100 | // We can access the element itself by dereferencing the iterator. 101 | std::cout << *it << " "; 102 | } 103 | std::cout << "\n"; 104 | 105 | // Just like std::vector, we can also iterate through the set via a for-each 106 | // loop. 107 | std::cout << "Printing the elements of the iterator with a for-each loop:\n"; 108 | for (const int &elem : int_set) { 109 | std::cout << elem << " "; 110 | } 111 | std::cout << "\n"; 112 | 113 | // We discuss more stylistic and readable ways of iterating through C++ STL 114 | // containers in auto.cpp! Check it out if you are interested. 115 | 116 | return 0; 117 | } -------------------------------------------------------------------------------- /src/shared_ptr.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file shared_ptr.cpp 3 | * @author Abigale Kim (abigalek) 4 | * @brief Tutorial code for usage of a shared pointer. 5 | */ 6 | 7 | // In this file, we'll talk about std::shared_ptr, which is a C++ smart pointer. 8 | // See the intro of unique_ptr.cpp for an introduction on smart pointers. 9 | // std::shared_ptr is a type of smart pointer that retains shared ownership of 10 | // an object through a pointer. This means that multiple shared pointers can 11 | // own the same object, and shared pointers can be copied. 12 | 13 | // Includes std::cout (printing) for demo purposes. 14 | #include 15 | // Includes std::shared_ptr functionality. 16 | #include 17 | // Includes the utility header for std::move. 18 | #include 19 | 20 | // Basic point class. (Will use later) 21 | class Point { 22 | public: 23 | Point() : x_(0), y_(0) {} 24 | Point(int x, int y) : x_(x), y_(y) {} 25 | inline int GetX() { return x_; } 26 | inline int GetY() { return y_; } 27 | inline void SetX(int x) { x_ = x; } 28 | inline void SetY(int y) { y_ = y; } 29 | 30 | private: 31 | int x_; 32 | int y_; 33 | }; 34 | 35 | // Function that modifies a Point object inside a shared pointer 36 | // by passing the shared pointer argument as a reference. 37 | void modify_ptr_via_ref(std::shared_ptr &point) { point->SetX(15); } 38 | 39 | // Function that modifies a Point object inside a shared pointer 40 | // by passing the shared pointer argument as a rvalue reference. 41 | void modify_ptr_via_rvalue_ref(std::shared_ptr &&point) { 42 | point->SetY(645); 43 | } 44 | 45 | void copy_shared_ptr_in_function(std::shared_ptr point) { 46 | std::cout << "Use count of shared pointer is " << point.use_count() 47 | << std::endl; 48 | } 49 | 50 | int main() { 51 | // This is how to initialize an empty shared pointer of type 52 | // std::shared_ptr. 53 | std::shared_ptr s1; 54 | // This is how to initialize a shared pointer with the default constructor. 55 | std::shared_ptr s2 = std::make_shared(); 56 | // This is how to initialize a shared pointer with a custom constructor. 57 | std::shared_ptr s3 = std::make_shared(2, 3); 58 | 59 | // The specific syntax for checking whether a smart pointer is empty is 60 | // covered in unique_ptr.cpp (line 56). Note that s1 is empty, while s2 and 61 | // s3 are not empty. 62 | std::cout << "Pointer s1 is " << (s1 ? "not empty" : "empty") << std::endl; 63 | std::cout << "Pointer s2 is " << (s2 ? "not empty" : "empty") << std::endl; 64 | std::cout << "Pointer s3 is " << (s3 ? "not empty" : "empty") << std::endl; 65 | 66 | // It is possible to copy shared pointers via their copy assignment and copy 67 | // constructor operators. Using these copy operators will increase the 68 | // reference count of the overall object. Also, std::shared_ptr comes with 69 | // a method called use_count which keeps track of the number of objects 70 | // currently interacting with the same internal data as the current shared 71 | // pointer instance. 72 | 73 | // First, the number of references to pointer s3 is obtained. This should be 74 | // 1 because s3 is the only object instance using the data in s3. 75 | std::cout 76 | << "Number of shared pointer object instances using the data in s3: " 77 | << s3.use_count() << std::endl; 78 | 79 | // Then, s4 is copy-constructed from s3. 80 | // This is copy-construction because it is the first time s4 appears. 81 | std::shared_ptr s4 = s3; 82 | 83 | // Now, the number of references to pointer s3's data should be 2, since both 84 | // s4 and s3 have access to s3's data. 85 | std::cout << "Number of shared pointer object instances using the data in s3 " 86 | "after one copy: " 87 | << s3.use_count() << std::endl; 88 | 89 | // Then, s5 is copy-constructed from s4. 90 | std::shared_ptr s5(s4); 91 | 92 | // Now, the number of references to pointer s3's data should be 3, since s5, 93 | // s4, and s3 have access to s3's data. 94 | std::cout << "Number of shared pointer object instances using the data in s3 " 95 | "after two copies: " 96 | << s3.use_count() << std::endl; 97 | 98 | // Modifying s3's data should also change the data in s4 and s5, since they 99 | // refer to the same object instance. 100 | s3->SetX(445); 101 | 102 | std::cout << "Printing x in s3: " << s3->GetX() << std::endl; 103 | std::cout << "Printing x in s4: " << s4->GetX() << std::endl; 104 | std::cout << "Printing x in s5: " << s5->GetX() << std::endl; 105 | 106 | // It is also possible to transfer ownership of a std::shared_ptr by moving 107 | // it. Note that the pointer is empty after the move has occurred. 108 | std::shared_ptr s6 = std::move(s5); 109 | 110 | // Note that s5 is now empty, s6 refers to the same data as s3 and s4, and 111 | // there are still 3 shared pointer instances with access to the same Point 112 | // instance data, not 4. 113 | std::cout << "Pointer s5 is " << (s5 ? "not empty" : "empty") << std::endl; 114 | std::cout << "Number of shared pointer object instances using the data in s3 " 115 | "after two copies and a move: " 116 | << s3.use_count() << std::endl; 117 | 118 | // Similar to unique pointers, shared pointers can also be passed by reference 119 | // and rvalue reference. See unique_ptr.cpp (line 89) for a information on 120 | // passing unique pointers by reference. See references.cpp for more 121 | // information on references. See move_semantics.cpp for more information on 122 | // rvalue references. Here, we present code below that calls functions that 123 | // modify s2 by passing a shared pointer as a reference and as a rvalue 124 | // reference. 125 | modify_ptr_via_ref(s2); 126 | modify_ptr_via_rvalue_ref(std::move(s2)); 127 | 128 | // After running this code, s2 should have x = 15 and y = 645. 129 | std::cout << "Pointer s2 has x=" << s2->GetX() << " and y=" << s2->GetY() 130 | << std::endl; 131 | 132 | // Unlike unique pointers, shared pointers can also be passed by value. In 133 | // this case, the function contains its own copy of a shared pointer, which 134 | // destroys itself after the function is finished. In this example, before s2 135 | // is passed to the function by value, its use count is 1. While it is in the 136 | // function, its use count is 2, because there is another copy of s2's data in 137 | // the shared pointer instance in the function. After the function goes out of 138 | // scope, this object in the function is destroyed, and the use count returns 139 | // to 1. 140 | std::cout 141 | << "Number of shared pointer object instances using the data in s2: " 142 | << s2.use_count() << std::endl; 143 | copy_shared_ptr_in_function(s2); 144 | std::cout << "Number of shared pointer object instances using the data in s2 " 145 | "after calling copy_shared_ptr_in_function: " 146 | << s2.use_count() << std::endl; 147 | 148 | return 0; 149 | } 150 | -------------------------------------------------------------------------------- /src/spring2024/s24_my_ptr.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | // This file contains the code used in the Spring2024 15-445/645 C++ bootcamp. 6 | // It dives deeply into C++ new features like move constructor/assign operator, move semantics, unique_ptr, 7 | // shared_ptr, wrapper class, etc., by implementing a simple version of unique_ptr from scratch. 8 | 9 | // **IMPORTANT NOTES**: 10 | // 1. please read `move_semantics.cpp` and `move_constructors.cpp` in `src` before reading this file! 11 | // 2. please BEGIN your reading from the MAIN function! 12 | 13 | // It is our implementation of std::unique_pointer, and the real implementation is more complex! 14 | // A template allows us to replace any type T, with what we want later in our code. 15 | template 16 | class Pointer { 17 | public: 18 | Pointer() { 19 | ptr_ = new T; 20 | *ptr_ = 0; 21 | std::cout << "New object on the heap: " << *ptr_ << std::endl; 22 | } 23 | Pointer(T val) { 24 | ptr_ = new T; 25 | *ptr_ = val; 26 | std::cout << "New object on the heap: " << val << std::endl; 27 | } 28 | // Destructor is called whenever an instance gets out of scope (just when the stack pops). 29 | ~Pointer() { 30 | if (ptr_) { 31 | std::cout << "Freed: " << *ptr_ << std::endl; 32 | delete ptr_; 33 | } 34 | } 35 | 36 | // Copy constructor is explicitly deleted. 37 | Pointer(const Pointer &) = delete; 38 | // Copy assignment operator is explicitly deleted. 39 | Pointer &operator=(const Pointer &) = delete; 40 | 41 | // Add move constructor: useful when we need to EXTEND the lifetime of an object! 42 | Pointer(Pointer &&another) : ptr_(another.ptr_) { another.ptr_ = nullptr; } 43 | // Add move assign operator: useful when we need to EXTEND the lifetime of an object! 44 | Pointer &operator=(Pointer &&another) { 45 | if (ptr_ == another.ptr_) { // In case `p = std::move(p);` 46 | return *this; 47 | } 48 | if (ptr_) { // We must free the existing pointer before overwriting it! Otherwise we LEAK!! 49 | delete ptr_; 50 | } 51 | ptr_ = another.ptr_; 52 | another.ptr_ = nullptr; // NOTE: L14 avoids freeing nullptr during the destruction. 53 | return *this; 54 | } 55 | 56 | // Overload operator *, in order to make the Pointer feel like a "pointer". 57 | // Note that the line below is an example of the following syntax we can use with our unique ptr type. 58 | // `p1.set_val(10)` -> `*p1 = 10` 59 | T &operator*() { return *ptr_; } 60 | 61 | T get_val() { return *ptr_; } 62 | void set_val(T val) { *ptr_ = val; } 63 | 64 | private: 65 | T *ptr_; 66 | }; 67 | 68 | // INCORRECT version of smart_generator 69 | template 70 | Pointer &dumb_generator(T init) { 71 | Pointer p(init); 72 | return p; // NOOO! A DANGLING REFERENCE! 73 | } 74 | 75 | template 76 | Pointer smart_generator(T init) { 77 | Pointer p(init); 78 | return std::move(p); 79 | // Actually `return p` will also work, since C++ is smart, it knows move construtor should be invoked in this place. 80 | // You can refer to `Automatic l-values returned by value may be moved instead of copied` in 81 | // https://www.learncpp.com/cpp-tutorial/move-constructors-and-move-assignment/ for more information. 82 | } 83 | 84 | void take_ownership(std::unique_ptr p) { 85 | // Do something... 86 | } 87 | 88 | void not_take_ownership(int *p) { 89 | // Never `delete p` here!! 90 | } 91 | 92 | int main() { 93 | /* ====================================================================== 94 | === Part 1: Common errors you come across in bustub ================== 95 | ====================================================================== */ 96 | // When coding in C++/in this class, you will see a variable type called "unique_ptr"... 97 | std::unique_ptr ptr = std::make_unique(3); 98 | // What does this mean? Why we don't use the raw pointer like `int *p = new int`? (The answer is in Part 2) 99 | // And later, when you need to pass this unique_ptr to a function, you may write the following code (please 100 | // try to uncomment next line)... 101 | // take_ownership(ptr); 102 | // It doesn't work. The error is `Call to implicitly-deleted copy constructor of 'std::unique_ptr'`. 103 | // You may search the Internet, and other people tell you to add a thing called `std::move`... 104 | take_ownership(std::move(ptr)); 105 | // It works! Looks great! And you continue coding... 106 | // Later, you may want to use p1 again (please try to uncomment next line)... 107 | // *ptr = 3; 108 | // Another error :(, and it says `segmentation fault`. 109 | // It looks confusing. What exactly happened? 110 | // We will explain it in this bootcamp, by implementing a simple version of unique_ptr from scratch! 111 | 112 | /* ====================================================================== 113 | === Part 2: Why we need unique_ptr rather than the raw pointer ======= 114 | ====================================================================== */ 115 | // What's the problem of merely using the raw pointer? 116 | int *p = new int; // Malloc 117 | *p = 456 * 12 / 34 + 23; 118 | if (*p == 76) { 119 | delete p; // You may forget to add this line, and have `memory leak` problem! 120 | return 0; 121 | } 122 | delete p; // Free 123 | 124 | // Raw pointers are dangerous! If you don't pay attention, you will come across problems like memory leaks, double 125 | // freeing, use after freeing... 126 | // The reason is that in C++, raw pointers have no inherent mechanism to clean up automatically! 127 | // Programmers have to allocate and deallocate heap memory all by themselves, and it is easy to go wrong. 128 | 129 | // We notice that, different from the memory in heap, the local variables in the stack will be created and deleted 130 | // automatically. Can we bind a raw pointer with a local variable in the stack? 131 | // It means, when this local variable is created, the heap memory for this raw pointer is automatically malloced. 132 | // And when this local variable dies, the raw pointer will be freed automatically. (For more details: search RAII) 133 | 134 | // Let's use C++ class to implement it! 135 | // Consider a class whose only job is to hold and "own" a raw pointer, and then deallocate that pointer when the class 136 | // object went out of scope... 137 | // This class is called `smart pointer`, and unique_ptr is one of smart pointers. 138 | // But, why we can't copy unique_ptr? What is std::move? 139 | 140 | /* ====================================================================== 141 | === Part 3: Let's implement a unique_ptr class from scratch ========== 142 | ====================================================================== */ 143 | // We only show the last version of our own unique_ptr class. 144 | // Here is the brief roadmap during the implementation: 145 | // 1. First version: with default copy constructor & assign operator, without move constructor & assign operator 146 | // Problem: `Pointer p2 = p1` will cause `double free` problem 147 | // Copy constructor & assign operator are evil in this case, since it will allow both p1 and p2 to manage the same 148 | // raw pointer! Solution: disable copy constructor & assign operator 149 | // 2. Second version: without copy constructor & assign operator, without move constructor & assign operator 150 | // `Pointer p2 = p1` won't compile, which is good. We can use reference `Pointer &p2 = p1` instead. But... 151 | // Problem: we cannot implement functions like dumb_generator() or smart_generator()! 152 | // Solution: add things called move constructor & assign operator 153 | // 3. Final version: without copy constructor & assign operator, with move constructor & assign operator 154 | // `Pointer p4 = std::move(p3);` 155 | // `std::move` guarantees this line of code invokes the `move constructor`(rather than `copy constructor`), to 156 | // transfer the ownership of the raw pointer from p3 to p4! 157 | // After this line, p3 will not be valid anymore! 158 | // The ptr_ in p3 will be nullptr, please don't use p3 anymore unless you reassign it. 159 | // Now you understand what is `std::move`, why copy functions are deleted... And how to use unique_ptr! 160 | // Reference: Chapter 22 in Learncpp Website 161 | // (https://www.learncpp.com/cpp-tutorial/introduction-to-smart-pointers-move-semantics/) 162 | Pointer p1(4); 163 | std::cout << "Hi from p1 " << p1.get_val() << std::endl; 164 | p1.set_val(10); 165 | std::cout << "Hi again from p1 " << p1.get_val() << std::endl; 166 | 167 | { 168 | // Problem in next line: both have the ownership of this raw pointer! Double free here! 169 | // Pointer p2 = p1; // Code for 1st version implementation. 170 | // Solution: never allow to copy ownership of the pointer! Never copy! 171 | // After deleting copy assign operator & constructor, maybe we can use pointer to rewrite `p2 = p1`. 172 | Pointer *p2 = &p1; // Code for 2nd version implementation. 173 | std::cout << "Hi from p2 " << p2->get_val() << std::endl; 174 | // Wait this is dumb, we have a raw pointer again... Maybe we can use C++ reference, which is safer! 175 | // It's semantically the same thing with `Pointer *p2 = &p1`, except the programmer doesn't **know** the pointer 176 | // (i.e. address of p2). 177 | Pointer &p22 = p1; // Code for 2nd version implementation. 178 | std::cout << "Hi from p22 " << p22.get_val() << std::endl; 179 | } 180 | // But reference doesn't solve everything :( 181 | // Sometimes we want to use the heap to extend the scope of the stack, like what dumb_generator() does! 182 | // Ex: pass down one element from a thread to another. 183 | // Please try to uncomment the following code! 184 | // Pointer& dumb_pointer = dumb_generator(2); // Something will go horribly wrong, but what? 185 | // dumb_pointer.set_val(10); // Uh oh... 186 | 187 | // We need a way to "move the ownership". Please check move assign operator/constructor in Pointer class. 188 | // And we change dumb_generator() to smart_generator()... 189 | // Code for final version implementation: 190 | Pointer p3 = smart_generator(2); 191 | p3.set_val(10); 192 | Pointer p4 = std::move(p3); 193 | 194 | // Let's make the user experience better. 195 | // 1. Templates. 196 | Pointer p5(5.1); 197 | std::cout << "Hi from float p5 " << p5.get_val() << std::endl; 198 | // 2. Operator overload. 199 | Pointer c1('a'); 200 | *c1 = 'b'; 201 | std::cout << "Hi from char c1 " << *c1 << std::endl; 202 | 203 | // You may be confused about: 204 | // `Pointer &&` (in the move constructor and assign operator) 205 | // VS 206 | // `Pointer &` (in the copy constructor and assign operator) 207 | 208 | // You have 2 options now. First, consider it as a syntax to distinguish copy and move, and go straight to Part 4; 209 | // Second, here is a quick explanation: 210 | // 1. You need to know lvalue & rvalue. According to Abi, a simplified definition of lvalues is that lvalues are 211 | // objects that refer to a location in memory. Rvalues are anything that is not a lvalue. 212 | // 2. `Pointer &&` is a rvalue reference, while `Pointer &` is a lvalue reference. 213 | // 3. `std::move(p)` will cast p from a lvalue to something, for example, a rvalue. 214 | // 4. For `Pointer p2 = p1`, it will invoke copy constructor, since p1 is a lvalue. 215 | // 5. For `Pointer p2 = std::move(p1)`, it will invoke move constructor, since std::move(p1) is a rvalue. 216 | 217 | /* ====================================================================== 218 | === Part 4: Some important takeaways for unique_ptr & shared_ptr ===== 219 | ====================================================================== */ 220 | // Several important takeaways for unique_ptr: (Reference: https://www.learncpp.com/cpp-tutorial/stdunique_ptr/) 221 | // 1. Always use std::make_unique() to create a unique_ptr. 222 | std::unique_ptr up{std::make_unique(1)}; 223 | // Please avoid writing the following code! 224 | // int *rp = new int; 225 | // std::unique_ptr up1{ rp }; 226 | // std::unique_ptr up2{ rp }; // WRONG! 227 | 228 | // 2. Ways to pass std::unique_ptr to a function. 229 | not_take_ownership(up.get()); 230 | // Unique_ptr `up` is still valid here! 231 | take_ownership(std::move(up)); 232 | // Unique_ptr `up` cannot be used here! 233 | 234 | // Several important takeaways for shared_ptr: (Reference: https://www.learncpp.com/cpp-tutorial/stdshared_ptr/) 235 | // 0. Multiple shared ptrs can have the ownership of a raw pointer at the same time. 236 | // Shared_ptr will count the number of shared ptrs that own the same raw pointer, 237 | // and free the raw pointer **only if** count == 0. 238 | std::shared_ptr sp1{std::make_shared(1)}; 239 | { 240 | // You can use copy constructor & assign operator for shared_ptr. 241 | std::shared_ptr sp2 = sp1; 242 | std::cout << "Count: " << sp1.use_count() << std::endl; // Output: 2 243 | } 244 | std::cout << "Count: " << sp1.use_count() << std::endl; // Output: 1 245 | // 1. Always make a copy of an existing std::shared_ptr. 246 | int *rp = new int; 247 | std::shared_ptr sp3{rp}; 248 | // std::shared_ptr sp4{ rp }; // WRONG! 249 | std::shared_ptr sp4{sp3}; 250 | // 2. Always use std::make_shared() to create a shared_ptr. 251 | 252 | return 0; 253 | } 254 | -------------------------------------------------------------------------------- /src/templated_classes.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file templated_classes.cpp 3 | * @author Abigale Kim (abigalek) 4 | * @brief Tutorial code for templated classes. 5 | */ 6 | 7 | // Includes std::cout (printing). 8 | #include 9 | 10 | // Templates can be also used to implement classes. For instance, here is a 11 | // basic templated class that stores one element of a templated type and 12 | // prints it when the print function is called. 13 | template 14 | class Foo { 15 | public: 16 | Foo(T var) : var_(var) {} 17 | void print() { 18 | std::cout << var_ << std::endl; 19 | } 20 | private: 21 | T var_; 22 | }; 23 | 24 | // You can also pass in multiple type names via templates into classes. 25 | // For instance, here's another basic templated class that stores two 26 | // elements of a templated type and prints them when the print function 27 | // is called. 28 | template 29 | class Foo2 { 30 | public: 31 | Foo2(T var1, U var2) 32 | : var1_(var1) 33 | , var2_(var2) {} 34 | void print() { 35 | std::cout << var1_ << " and " << var2_ << std::endl; 36 | } 37 | private: 38 | T var1_; 39 | U var2_; 40 | }; 41 | 42 | // It is also possible to create specialized templated classes, that do 43 | // different things for different types. Take the following contrived example, 44 | // which instantiates a class with a print function that outputs the value of 45 | // the variable stored if it's any other type but float. If the class is 46 | // instantiated with a float type, it prints out hello float and the variable 47 | // the class stores in its var_ field. 48 | template 49 | class FooSpecial { 50 | public: 51 | FooSpecial(T var) : var_(var) {} 52 | void print() { 53 | std::cout << var_ << std::endl; 54 | } 55 | private: 56 | T var_; 57 | }; 58 | 59 | // Specialized templated class, specialized on the float type. 60 | template<> 61 | class FooSpecial { 62 | public: 63 | FooSpecial(float var) : var_(var) {} 64 | void print() { 65 | std::cout << "hello float! " << var_ << std::endl; 66 | } 67 | private: 68 | float var_; 69 | }; 70 | 71 | // Template parameters don't have to be types. They can also be values! 72 | template 73 | class Bar { 74 | public: 75 | Bar() {} 76 | void print_int() { 77 | std::cout << "print int: " << T << std::endl; 78 | } 79 | }; 80 | 81 | int main() { 82 | // First, let us construct an object from a templated class. The Foo 83 | // class template is instantiated with an int template argument. This 84 | // would make a's type class Foo instead of Foo. a's print 85 | // function works as expected. 86 | Foo a(3); 87 | std::cout << "Calling print on Foo a(3): "; 88 | a.print(); 89 | 90 | // It is also possible for a templated class to interpret the type 91 | // of its arguments. Once again, if you're a beginner, think twice 92 | // before doing this if you are unsure of the types you are 93 | // instantiating your class with. 94 | Foo b(3.4f); 95 | std::cout << "Calling print on Foo b(3.4f): "; 96 | b.print(); 97 | 98 | // Second, we construct an object from a templated class with multiple 99 | // type arguments. 100 | Foo2 c(3, 3.2f); 101 | std::cout << "Calling print on Foo2 c(3, 3.2f): "; 102 | c.print(); 103 | 104 | // Let's see what happens when we instantiate FooSpecial both with 105 | // and without the float type argument. As expected when we call 106 | // print from d, it prints the variable and not "hello float". 107 | // When we call print from e, which is an instance of the 108 | // instantiated FooSpecial class, it prints hello float! 109 | FooSpecial d(5); 110 | std::cout << "Calling print on FooSpecial d(5): "; 111 | d.print(); 112 | 113 | FooSpecial e(4.5); 114 | std::cout << "Calling print on FooSpecial e(4.5): "; 115 | e.print(); 116 | 117 | // Lastly, let's see what happens when we construct an object from a 118 | // templated class with non-type arguments. 119 | Bar<150> f; 120 | std::cout << "Calling print_int on Bar<150> f: "; 121 | f.print_int(); 122 | 123 | // Once again, these are contrived examples, but it is still important 124 | // to understand them you'll be seeing code similar to this in the Bustub 125 | // codebase, so it's good to understand templated classes in these contexts! 126 | 127 | return 0; 128 | } -------------------------------------------------------------------------------- /src/templated_functions.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file templated_functions.cpp 3 | * @author Abigale Kim (abigalek) 4 | * @brief Tutorial code for templated functions. 5 | */ 6 | 7 | // Includes std::cout (printing) for demo purposes. 8 | #include 9 | 10 | // Templates are a language feature in C++ that allow you to write code that 11 | // can work with multiple data types, without actually specifying those types. 12 | // In C++, you can create both templated functions and templated classes. We'll 13 | // talk about templated functions in this file. 14 | 15 | // Here is a basic templated function that adds two numbers. 16 | // Syntax note: You will see code with both template and 17 | // template. Although these statements are equivalent, there are 18 | // differences between the class and typename keywords. This blog article covers 19 | // this difference, but you won't need to know this for the class: 20 | // https://mariusbancila.ro/blog/2021/03/15/typename-or-class/ 21 | template T add(T a, T b) { return a + b; } 22 | 23 | // It is possible to pass multiple type names via templates into functions. 24 | // This function will print both of these values out. 25 | template 26 | void print_two_values(T a, U b) { 27 | std::cout << a << " and " << b << std::endl; 28 | } 29 | 30 | // It is also possible to create specialized templated functions, that do 31 | // different things for different types. Take the following contrived example, 32 | // which prints the type if its a float type, but just prints hello world for 33 | // all other types. 34 | template void print_msg() { std::cout << "Hello world!\n"; } 35 | 36 | // Specialized templated function, specialized on the float type. 37 | template <> void print_msg() { 38 | std::cout << "print_msg called with float type!\n"; 39 | } 40 | 41 | // Lastly, template parameters do not have to be classes. Take this basic (yet 42 | // very contrived) function that takes in a bool as a template parameter and 43 | // does different things to the argument depending on the boolean argument. 44 | template int add3(int a) { 45 | if (T) { 46 | return a + 3; 47 | } 48 | 49 | return a; 50 | } 51 | 52 | int main() { 53 | // First, let's see the add function called on both ints and floats. 54 | std::cout << "Printing add(3, 5): " << add(3, 5) << std::endl; 55 | std::cout << "Printing add(2.8, 3.7): " << add(2.8, 3.7) 56 | << std::endl; 57 | 58 | // It is also possible for a templated function to interpret the type of its 59 | // arguments, although if you're a beginner in modern C++, it's preferred you 60 | // don't do this because then you might not be sure of the types being passed 61 | // into your functions. 62 | std::cout << "Printing add(3, 5): " << add(3, 5) << std::endl; 63 | 64 | // Second, let's see the print_two_values function being called with two 65 | // different types. 66 | std::cout << "Printing print_two_values(3, 3.2): "; 67 | print_two_values(3, 3.2); 68 | 69 | // Let's see what happens when we called print_msg with and without the float 70 | // type being passed in. As expected, the first call to print_msg prints out 71 | // the general output, while the second one, with the float argument, 72 | // recognizes its type parameter and calls the specialized function. 73 | std::cout << "Calling print_msg(): "; 74 | print_msg(); 75 | std::cout << "Calling print_msg(): "; 76 | print_msg(); 77 | 78 | // add3 has the specified behavior for both a true and false templated 79 | // argument, as we can see here. 80 | std::cout << "Printing add3(3): " << add3(3) << std::endl; 81 | std::cout << "Printing add3(3): " << add3(3) << std::endl; 82 | 83 | // Lastly, it's important to note that most of these are contrived examples, 84 | // and it is possible to code some of these functions (e.g. passing a boolean 85 | // as an argument instead of a templated argument) without using templates. 86 | // However, in the class, you'll be seeing code similar to this in the 87 | // codebase, so it's good to understand templated functions in these contexts! 88 | 89 | return 0; 90 | } -------------------------------------------------------------------------------- /src/unique_ptr.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file unique_ptr.cpp 3 | * @author Abigale Kim (abigalek) 4 | * @brief Tutorial code for usage of a unique pointer. 5 | */ 6 | 7 | // A smart pointer is a type of data structure used for memory management (and 8 | // sometimes other features) in languages that don't have memory management 9 | // built in (e.g C++) An example of a language that has memory management built 10 | // in is any language with garbage collection, like Java or Python. Two of the 11 | // modern C++ standard library's smart pointers (and the ones that you will use 12 | // in class) are std::unique_ptr and std::shared_ptr. Both std::unique_ptr and 13 | // std::shared_ptr handle memory allocation and deallocation automatically, and 14 | // contain raw pointers under the hood. In other words, they are wrapper classes 15 | // over raw pointers. In this file, we'll talk about std::unique_ptr. 16 | // std::unique_ptr is a type of smart pointer that retains sole ownership of an 17 | // object This means that no two instances of std::unique_ptr can manage the 18 | // same object. 19 | 20 | // Includes std::cout (printing) for demo purposes. 21 | #include 22 | // Includes std::unique_ptr functionality. 23 | #include 24 | // String library for printing help for demo purposes. 25 | #include 26 | // Including the utility header for std::move. 27 | #include 28 | 29 | // Basic point class. (Will use later) 30 | class Point { 31 | public: 32 | Point() : x_(0), y_(0) {} 33 | Point(int x, int y) : x_(x), y_(y) {} 34 | inline int GetX() { return x_; } 35 | inline int GetY() { return y_; } 36 | inline void SetX(int x) { x_ = x; } 37 | inline void SetY(int y) { y_ = y; } 38 | 39 | private: 40 | int x_; 41 | int y_; 42 | }; 43 | 44 | // Function that takes in a unique pointer reference and changes its x value to 45 | // 445. 46 | void SetXTo445(std::unique_ptr &ptr) { ptr->SetX(445); } 47 | 48 | int main() { 49 | // This is how to initialize an empty unique pointer of type 50 | // std::unique_ptr. 51 | std::unique_ptr u1; 52 | // This is how to initialize a unique pointer with the default constructor. 53 | std::unique_ptr u2 = std::make_unique(); 54 | // This is how to initialize a unique pointer with a custom constructor. 55 | std::unique_ptr u3 = std::make_unique(2, 3); 56 | 57 | // Here, for std::unique_ptr instance u, we use the statement (u ? "not empty" 58 | // : "empty") to determine if the pointer u contains managed data. The main 59 | // gist of this is that the std::unique_ptr class has a conversion function on 60 | // its objects to a boolean type, and so this function is called whenever we 61 | // treat the std::unique_ptr as a boolean. For instance, this can be used in 62 | // the following example. 63 | if (u1) { 64 | // This won't print because u1 is empty. 65 | std::cout << "u1's value of x is " << u1->GetX() << std::endl; 66 | } 67 | 68 | if (u2) { 69 | // This will print because u2 is not empty, and contains a managed Point 70 | // instance. 71 | std::cout << "u2's value of x is " << u2->GetX() << std::endl; 72 | } 73 | 74 | // Note that u1 is empty and u2 and u3 are not empty, since they were 75 | // initialized with a Point instance. 76 | std::cout << "Pointer u1 is " << (u1 ? "not empty" : "empty") << std::endl; 77 | std::cout << "Pointer u2 is " << (u2 ? "not empty" : "empty") << std::endl; 78 | std::cout << "Pointer u3 is " << (u3 ? "not empty" : "empty") << std::endl; 79 | 80 | // Since instances of std::unique_ptr can have only one owner, it has no copy 81 | // constructor. Therefore, this code won't compile. Uncomment it to try! 82 | // std::unique_ptr u4 = u3; 83 | 84 | // However, it's possible to transfer ownership of unique pointers via 85 | // std::move. 86 | std::unique_ptr u4 = std::move(u3); 87 | 88 | // Note that because u3 is an lvalue, it no longer contains any managed 89 | // object. It is an empty unique pointer. Let's retest for emptyness. 90 | std::cout << "Pointer u3 is " << (u3 ? "not empty" : "empty") << std::endl; 91 | std::cout << "Pointer u4 is " << (u4 ? "not empty" : "empty") << std::endl; 92 | 93 | // Lastly, let's talk about how to pass std::unique_ptr instances as arguments 94 | // to functions. Mainly, you should pass it as a reference so that the 95 | // ownership doesn't change. You can see this as an example in the function 96 | // SetXTo445 (line 44 in this file). 97 | SetXTo445(u4); 98 | 99 | // Now, let's print the x value of u4 to confirm that the change occured, but 100 | // the ownership of the Point instance has been retained to u4. 101 | std::cout << "Pointer u4's x value is " << u4->GetX() << std::endl; 102 | 103 | return 0; 104 | } -------------------------------------------------------------------------------- /src/unordered_maps.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file unordered_maps.cpp 3 | * @author Abigale Kim (abigalek) 4 | * @brief Tutorial code for C++ Standard Library (STL) unordered_map. 5 | */ 6 | 7 | // In this file, we will introduce the container std::unordered_map. We won't 8 | // be able to cover every function in this container, but we will try to cover 9 | // the bare bones of using this container. Look to the intro of vectors.cpp for 10 | // a general overview of C++ STL containers. 11 | 12 | // There is documentation on all the other functions, and other containers 13 | // on https://en.cppreference.com/w/cpp/container. You will definitely need this 14 | // resource as you complete the assignments in this class, so you should check 15 | // it out! 16 | 17 | // Includes std::cout (printing) for demo purposes. 18 | #include 19 | // Includes the unordered_map container library header. 20 | #include 21 | // Includes the C++ string library. 22 | #include 23 | // Includes std::make_pair. 24 | #include 25 | 26 | int main() { 27 | // The std::unordered_map is a data structure that contains key-value pairs 28 | // with unique keys. Essentially, this means you can use it as a hash table 29 | // in your code. 30 | 31 | // You can declare a unordered_map with string keys and int values with 32 | // the following syntax. 33 | std::unordered_map map; 34 | 35 | // The insert function is used to insert items into an unordered map. 36 | map.insert({"foo", 2}); 37 | 38 | // The insert function also takes in a std::pair as the argument. An 39 | // std::pair is a generic pair type, and you can create one by calling 40 | // std::make_pair with 2 arguments. std::make_pair is defined in the header 41 | // , and constructs an instance of the generic pair type. 42 | map.insert(std::make_pair("jignesh", 445)); 43 | 44 | // You can also insert multiple elements at a time by passing in an 45 | // initializer list of pairs. 46 | map.insert({{"spam", 1}, {"eggs", 2}, {"garlic rice", 3}}); 47 | 48 | // It is also possible to insert an element via array-style syntax, 49 | // even if the element did not exist previously. 50 | map["bacon"] = 5; 51 | 52 | // You can also update an element in the unordered map with the same syntax. 53 | map["spam"] = 15; 54 | 55 | // The find function is used to find elements in an unordered map. It returns 56 | // an iterator pointing to the found element if the element exists, and 57 | // returns an iterator pointing to the end of the unordered map container 58 | // otherwise. 59 | std::unordered_map::iterator result = map.find("jignesh"); 60 | if (result != map.end()) { 61 | // This is one way of accessing the key/value pair from the iterator. 62 | std::cout << "Found key " << result->first << " with value " 63 | << result->second << std::endl; 64 | 65 | // Dereferencing the iterator is another method of accessing the key/value 66 | // pair from the iterator. 67 | std::pair pair = *result; 68 | std::cout << "DEREF: Found key " << pair.first << " with value " 69 | << pair.second << std::endl; 70 | } 71 | 72 | // The count function returns the number of elements in an unordered map with 73 | // the specified key in the unordered map. 74 | size_t count = map.count("spam"); 75 | if (count == 1) { 76 | std::cout 77 | << "A key-value pair with key spam exists in the unordered map.\n"; 78 | } 79 | 80 | // The erase function deletes values from the unordered map. It can take a 81 | // key as an argument. 82 | map.erase("eggs"); 83 | 84 | // We confirm that the eggs/2 key-value pair isn't in the map anymore. 85 | if (map.count("eggs") == 0) { 86 | std::cout << "Key-value pair with key eggs does not exist in the unordered " 87 | "map.\n"; 88 | } 89 | 90 | // Additionally, if we want to erase an element at a certain position, we can 91 | // pass in an iterator to the erase function. The following code will erase 92 | // the key-value pair with the key "garlic rice". Note that std::next is an 93 | // iterator function that returns the successor of the iterator passed in as 94 | // its argument. 95 | map.erase(map.find("garlic rice")); 96 | 97 | // We confirm that garlic rice/3 key-value pair isn't in the map anymore. 98 | if (map.count("garlic rice") == 0) { 99 | std::cout << "Key-value pair with key garlic rice does not exist in the " 100 | "unordered map.\n"; 101 | } 102 | 103 | // We can iterate through the unordered map elements via the unordered map 104 | // iterator. You cannot iterate through a unordered map via indexes of any 105 | // kind. 106 | std::cout << "Printing the elements of the iterator:\n"; 107 | for (std::unordered_map::iterator it = map.begin(); 108 | it != map.end(); ++it) { 109 | // We can access the element itself by dereferencing the iterator. 110 | std::cout << "(" << it->first << ", " << it->second << "), "; 111 | } 112 | std::cout << "\n"; 113 | 114 | // Just like std::vector, we can also iterate through the unordered map 115 | // via a for-each loop. 116 | std::cout << "Printing the elements of the iterator with a for-each loop:\n"; 117 | for (const std::pair &elem : map) { 118 | std::cout << "(" << elem.first << ", " << elem.second << "), "; 119 | } 120 | std::cout << "\n"; 121 | 122 | // We discuss more stylistic and readable ways of iterating through C++ STL 123 | // containers in auto.cpp! Check it out if you are interested. 124 | 125 | return 0; 126 | } -------------------------------------------------------------------------------- /src/vectors.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file vectors.cpp 3 | * @author Abigale Kim (abigalek) 4 | * @brief Tutorial code for C++ Standard Library (STL) vectors. 5 | */ 6 | 7 | // The C++ STL contains a container library, which is a generic collection of 8 | // data structure and algorithm implementations that allow users to manipulate 9 | // data structures like stacks, queues, and hash tables easily. Each container 10 | // has its own header and usage. In the C++ standard (up to C++ 23), there are 11 | // currently 20 containers, which is far too many to cover here well. In this 12 | // file, we will introduce the container std::vector. The std::vector container 13 | // is essentially a generic dynamic array (or unbounded array). We won't be 14 | // able to cover every function in this container, but we will try to cover the 15 | // basics of using this container. 16 | 17 | // There is documentation on all the other functions, and other containers on 18 | // https://en.cppreference.com/w/cpp/container. You will definitely need this 19 | // resource as you complete the assignments in this class, so you should check 20 | // it out! 21 | 22 | // Includes std::remove_if to remove elements from vectors. 23 | #include 24 | // Includes std::cout (printing) for demo purposes. 25 | #include 26 | // Includes the vector container library header. 27 | #include 28 | 29 | // Basic point class. (Will use later) 30 | class Point { 31 | public: 32 | Point() : x_(0), y_(0) { 33 | std::cout << "Default constructor for the Point class is called.\n"; 34 | } 35 | 36 | Point(int x, int y) : x_(x), y_(y) { 37 | std::cout << "Custom constructor for the Point class is called.\n"; 38 | } 39 | 40 | inline int GetX() const { return x_; } 41 | inline int GetY() const { return y_; } 42 | inline void SetX(int x) { x_ = x; } 43 | inline void SetY(int y) { y_ = y; } 44 | void PrintPoint() const { 45 | std::cout << "Point value is (" << x_ << ", " << y_ << ")\n"; 46 | } 47 | 48 | private: 49 | int x_; 50 | int y_; 51 | }; 52 | 53 | // A utility function to print the elements of an int vector. The code for this 54 | // should be understandable and similar to the code iterating through elements 55 | // of a vector in the main function. 56 | void print_int_vector(const std::vector &vec) { 57 | for (const int &elem : vec) { 58 | std::cout << elem << " "; 59 | } 60 | std::cout << "\n"; 61 | } 62 | 63 | int main() { 64 | // We can declare a Point vector with the following syntax. 65 | std::vector point_vector; 66 | 67 | // It is also possible to initialize the vector via an initializer list. 68 | std::vector int_vector = {0, 1, 2, 3, 4, 5, 6}; 69 | 70 | // There are two functions for appending data to the back of the vector. They 71 | // are push_back and emplace_back. Generally, emplace_back is slightly faster, 72 | // since it forwards the constructor arguments to the object's constructor and 73 | // constructs the object in place, while push_back constructs the object, then 74 | // moves it to the memory in the vector. We can see this here where we add two 75 | // Point objects to our vector. 76 | std::cout << "Appending to the point_vector via push_back:\n"; 77 | point_vector.push_back(Point(35, 36)); 78 | std::cout << "Appending to the point_vector via emplace_back:\n"; 79 | point_vector.emplace_back(37, 38); 80 | 81 | // Let's just add more items to the back of our point_vector. 82 | point_vector.emplace_back(39, 40); 83 | point_vector.emplace_back(41, 42); 84 | 85 | // There are many ways to iterate through a vector. For instance, we can 86 | // iterate through it's indices via the following for loop. Note that it is 87 | // good practice to use an unsigned int type for array or vector indexes. 88 | std::cout << "Printing the items in point_vector:\n"; 89 | for (size_t i = 0; i < point_vector.size(); ++i) { 90 | point_vector[i].PrintPoint(); 91 | } 92 | 93 | // We can also iterate through it via a for-each loop. Note that I use 94 | // references to iterate through it so that the items we iterate through are 95 | // the items in the original vector. If we iterate through references of the 96 | // vector elements, we can also modify the data in the vector. 97 | for (Point &item : point_vector) { 98 | item.SetY(445); 99 | } 100 | 101 | // Let's see if our changes went through. Note that I use the const reference 102 | // syntax to ensure that the data I'm accessing is read only. 103 | for (const Point &item : point_vector) { 104 | item.PrintPoint(); 105 | } 106 | 107 | // Now, we show how to erase elements from a vector. First, we can erase 108 | // elements by their position via the erase function. For instance, if we want 109 | // to delete int_vector[2], we can call the following function with the 110 | // following arguments. The argument passed into this erase function has 111 | // the type std::vector::iterator. An iterator for a C++ STL container 112 | // is an object that points to an element within the container. For instance, 113 | // int_vector.begin() is an iterator object that points to the first element 114 | // in the vector. The vector iterator also has a plus operator that takes 115 | // a vector iterator and an integer. The plus operator will increase the 116 | // index of the element that the iterator is pointing to by the number passed 117 | // in. Therefore, int_vector.begin() + 2 is pointing to the third element in 118 | // the vector, or the element at int_vector[2]. 119 | // If you are confused about iterators, it may be helpful to read the header of 120 | // iterator.cpp. 121 | int_vector.erase(int_vector.begin() + 2); 122 | std::cout << "Printing the elements of int_vector after erasing " 123 | "int_vector[2] (which is 2)\n"; 124 | print_int_vector(int_vector); 125 | 126 | // We can also erase elements in a range via the erase function. If we want to 127 | // delete elements starting from index 1 to the end of the array, then we can 128 | // do so the following. Note that int_vector.end() is an iterator pointing to 129 | // the end of the vector. It does not point to the last valid index of the 130 | // vector. It points to the end of a vector and cannot be accessed for data. 131 | int_vector.erase(int_vector.begin() + 1, int_vector.end()); 132 | std::cout << "Printing the elements of int_vector after erasing all elements " 133 | "from index 1 through the end\n"; 134 | print_int_vector(int_vector); 135 | 136 | // We can also erase values via filtering, i.e. erasing values if they meet a 137 | // conditional. We can do so by importing another library, the algorithm 138 | // library, which gives us the std::remove_if function, which removes all 139 | // elements meeting a conditional from an iterator range. This does seem 140 | // awfully complicated, but the code can be summarized as follows. 141 | // std::remove_if takes in 3 arguments. Two of those arguments indicate the 142 | // range of elements that we should filter. These are given by 143 | // point_vector.begin() and point_vector.end(), which are iterators that point 144 | // to the beginning and the end of a vector respectively. Therefore, when we 145 | // pass these in, we are implying that we want the whole vector filtered. 146 | // The third argument is a conditional lambda type (see the std::function 147 | // library in C++, or at 148 | // https://en.cppreference.com/w/cpp/utility/functional/function), that takes 149 | // in one argument, which is supposed to represent each element in the vector 150 | // that we are filtering. This function should return a boolean that is true 151 | // if the element is to be filtered out and false otherwise. std::remove_if 152 | // returns an iterator pointing to the first element in the container that 153 | // should be eliminated. Keep in mind that it swaps elements as needed, 154 | // partitioning the elements that need to be deleted after the iterator value 155 | // it returns. When erase is called, it deletes only the elements that 156 | // remove_if has partitioned away to be deleted, up to the end of the vector. 157 | // This outer erase takes a range argument, as we saw in the previous example. 158 | point_vector.erase( 159 | std::remove_if(point_vector.begin(), point_vector.end(), 160 | [](const Point &point) { return point.GetX() == 37; }), 161 | point_vector.end()); 162 | 163 | // After calling remove here, we should see that three elements remain in our 164 | // point vector. Only the one with value (37, 445) is deleted. 165 | std::cout << "Printing the point_vector after (37, 445) is erased:\n"; 166 | for (const Point &item : point_vector) { 167 | item.PrintPoint(); 168 | } 169 | 170 | // We discuss more stylistic and readable ways of iterating through C++ STL 171 | // containers in auto.cpp! Check it out if you are interested. 172 | 173 | return 0; 174 | } 175 | -------------------------------------------------------------------------------- /src/wrapper_class.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file wrapper_class.cpp 3 | * @author Abigale Kim (abigalek) 4 | * @brief Tutorial code on wrapper classes. 5 | */ 6 | 7 | // A C++ wrapper class is a class that manages a resource. A resource 8 | // could be memory, file sockets, or a network connection. Wrapper classes 9 | // often use the RAII (Resource Acquisition is Initialization) C++ 10 | // programming technique. Using this technique implies that the resource's 11 | // lifetime is tied to its scope. When an instance of the wrapper class is 12 | // constructed, this means that the underlying resource it is managing is 13 | // available, and when this instance is destructed, the resource also 14 | // is unavailable. 15 | // Here are a couple resources on RAII that are useful: 16 | // https://en.cppreference.com/w/cpp/language/raii (RAII docs on the CPP 17 | // docs website) 18 | // Interesting Stack Overflow answers to "What is meant by RAII?": 19 | // https://stackoverflow.com/questions/2321511/what-is-meant-by-resource-acquisition-is-initialization-raii 20 | 21 | // In this file, we will look at a basic implementation of a wrapper class that 22 | // manages an int*. We will also look at usage of this class. 23 | 24 | // Includes std::cout (printing) for demo purposes. 25 | #include 26 | // Includes the utility header for std::move. 27 | #include 28 | 29 | // The IntPtrManager class is a wrapper class that manages an int*. The 30 | // resource that this class is managing is the dynamic memory accessible via 31 | // the pointer ptr_. By the principles of the RAII technique, a wrapper class 32 | // object should not be copyable, since one object is supposed to manage one 33 | // resource. Therefore, the copy assignment operator and copy constructor are 34 | // deleted from this class. However, the class is still moveable from different 35 | // lvalues/owners, and has a move constructor and move assignment operator. 36 | // Another reason that wrapper classes forbid copying is because they destroy 37 | // their resource in the destructor, and if two objects are managing the same 38 | // resource, there is a risk of double deletion of the resource. 39 | class IntPtrManager { 40 | public: 41 | // All constructors of a wrapper class are supposed to initialize a resource. 42 | // In this case, this means allocating the memory that we are managing. 43 | // The default value of this pointer's data is 0. 44 | IntPtrManager() { 45 | ptr_ = new int; 46 | *ptr_ = 0; 47 | } 48 | 49 | // Another constructor for this wrapper class that takes a initial value. 50 | IntPtrManager(int val) { 51 | ptr_ = new int; 52 | *ptr_ = val; 53 | } 54 | 55 | // Destructor for the wrapper class. The destructor must destroy the 56 | // resource that it is managing; in this case, the destructor deletes 57 | // the pointer! 58 | ~IntPtrManager() { 59 | // Note that since the move constructor marks objects invalid by setting 60 | // their ptr_ value to nullptr, we have to account for this in the 61 | // destructor. We don't want to be calling delete on a nullptr! 62 | if (ptr_) { 63 | delete ptr_; 64 | } 65 | } 66 | 67 | // Move constructor for this wrapper class. Note that after the move 68 | // constructor is called, effectively moving all of other's data into 69 | // the specified instance being constructed, the other object is no 70 | // longer a valid instance of the IntPtrManager class, since it has 71 | // no memory to manage. 72 | IntPtrManager(IntPtrManager&& other) { 73 | ptr_ = other.ptr_; 74 | other.ptr_ = nullptr; 75 | } 76 | 77 | // Move assignment operator for this wrapper class. Similar techniques as 78 | // the move constructor. 79 | IntPtrManager &operator=(IntPtrManager &&other) { 80 | if (ptr_ == other.ptr_) { 81 | return *this; 82 | } 83 | if (ptr_) { 84 | delete ptr_; 85 | } 86 | ptr_ = other.ptr_; 87 | other.ptr_ = nullptr; 88 | return *this; 89 | } 90 | 91 | // We delete the copy constructor and the copy assignment operator, 92 | // so this class cannot be copy-constructed. 93 | IntPtrManager(const IntPtrManager &) = delete; 94 | IntPtrManager &operator=(const IntPtrManager &) = delete; 95 | 96 | // Setter function. 97 | void SetVal(int val) { 98 | *ptr_ = val; 99 | } 100 | 101 | // Getter function. 102 | int GetVal() const { 103 | return *ptr_; 104 | } 105 | 106 | private: 107 | int *ptr_; 108 | 109 | }; 110 | 111 | int main() { 112 | // We initialize an instance of IntPtrManager. After it is initialized, this 113 | // class is managing an int pointer. 114 | IntPtrManager a(445); 115 | 116 | // Getting the value works as expected. 117 | std::cout << "1. Value of a is " << a.GetVal() << std::endl; 118 | 119 | // Setting the value goes through, and the value can retrieved as expected. 120 | a.SetVal(645); 121 | std::cout << "2. Value of a is " << a.GetVal() << std::endl; 122 | 123 | // Now, we move the instance of this class from the a lvalue to the b lvalue 124 | // via the move constructor. 125 | IntPtrManager b(std::move(a)); 126 | 127 | // Retrieving the value of b works as expected because b is now managing the 128 | // data originally constructed by the constructor that created a. Note that 129 | // calling GetVal() on a will segfault, and a is supposed to effectively be 130 | // empty and unusable in this state. 131 | std::cout << "Value of b is " << b.GetVal() << std::endl; 132 | 133 | // Once this function ends, the destructor for both a and b will be called. 134 | // a's destructor will note that the ptr_ it is managing has been set to 135 | // nullptr, and will do nothing, while b's destructor should free the memory 136 | // it is managing. 137 | 138 | return 0; 139 | } 140 | --------------------------------------------------------------------------------