diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..99b6175 --- /dev/null +++ b/.clang-format @@ -0,0 +1,52 @@ +# Project C++ style (Allman, 4 spaces, 120 columns) +# Compatible with clang-format 18 +BasedOnStyle: Microsoft +Language: Cpp +Standard: c++20 + +# Indent +IndentWidth: 4 +TabWidth: 4 +UseTab: Never + +# Line length +ColumnLimit: 120 + +# Braces: Allman everywhere except lambdas (attached) +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: true + AfterClass: true + AfterControlStatement: Always + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true + +# Pointers/references attached to type (int* p, Foo& f) +PointerAlignment: Left +ReferenceAlignment: Left +DerivePointerAlignment: false + +# Access modifiers outdented from members (public: at class level) +AccessModifierOffset: -4 + +# Includes +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^"' + Priority: 1 + - Regex: '^1 allocations during growth + +### C++ Standard Features +- Default project language is **C++20**; use standard-restriction overrides from the main Socratic rules in chat only when intentionally narrowing a lesson. +- When a lesson targets an older feature set, verify the snippet compiles under the active standard (for example `requires` expressions need C++20). +- Example: Designated aggregate initializers are C++20; without C++20, use a constructor or member-by-member initialization instead. + +### File I/O in Tests +- Use `TearDown()` to clean up test files: `std::remove("test_file.txt")` +- Ensure file operations don't interfere between tests + +## Target Metrics per File + +- **Lines**: 200-400 lines per test file (STRICT - do not exceed 450 lines) +- **Tests**: 3-6 TEST_F blocks +- **Questions**: 10-20 Q/A/R pairs +- **TODOs**: 2-5 fill-in exercises +- **Time estimate**: 2-4 hours per file (for learner) +- **Actual range observed**: 192-415 lines across implemented modules + +**Key Principle**: TEST_F blocks are the primary teaching vehicle. Extract helpers only when they clarify or enable reuse. + +## Validation Checklist + +After implementing each file: + +- [ ] File compiles without errors +- [ ] All tests pass (100%) +- [ ] No linter errors +- [ ] **Q/A/R pattern present at key points (prefer inside TEST_F)** +- [ ] **File is 200-450 lines (not bloated with excessive helpers)** +- [ ] Instrumentation used for observable behavior (EventLog or equivalent in that module) +- [ ] Progressive difficulty (Easy → Hard) +- [ ] TODO markers for learner exercises +- [ ] **Helper functions are minimal and necessary** + +## Example Implementation Order + +For an 8-file module (actual learning_modern_cpp implementation): +1. **Implement file 1** (foundational concept, e.g., lambdas) +2. Uncomment CMakeLists.txt entry +3. Build + test → fix any issues immediately +4. **Implement file 2** (next logical topic, e.g., auto) +5. Uncomment CMakeLists.txt entry +6. Build + test → verify pass +7. **Repeat for remaining files** (3-8) +8. Group by logical progression: C++ standard, then complexity +9. Run all tests together with ctest +10. Update `README.md` (module table) and `docs/LEARNING_PATH.md` if the curriculum or test counts changed + +**Actual order used**: foundational topics first (lambdas, auto, uniform init, delegating ctors), then later topics (structured bindings, optional/variant/any, string_view, if constexpr) + +**Important**: Files are logically ordered for learning progression, but each test file and test case must be **fully independent** — no test should depend on state from another test. + +## Anti-Patterns + +**Don't**: +- Implement all files before building any +- Skip test execution after building +- Continue to next file with failing tests +- Batch-fix errors across multiple files +- Ignore compilation warnings +- Create test dependencies between files +- Create single-use helpers that obscure test logic +- Exceed 450 lines per file + +**Do**: +- Validate each file immediately +- Fix issues in context while fresh +- Ensure each test is fully independent +- Target 200-400 lines per file +- Extract helpers only for clarity or reuse +- Keep Q/A/R close to observable behavior + +## Success Metrics + +Across modules implemented: +- **learning_memory, learning_modern_cpp, learning_raii**: 16 files, 86 tests, ~4,500 lines, 98% first-time pass rate +- **learning_stl**: 5 files, 47 tests, 1,525 lines (305 lines/file avg) ✓ Within target +- **learning_error_handling**: 5 files, 60 tests, 4,049 lines (810 lines/file avg) ✗ Needs refactoring + +## Design Heuristics + +### Extraction Principle +Extract helpers when they **clarify** or enable **reuse**. Keep inline when extraction adds indirection without benefit. + +### File Size Discipline +**Target**: 200-400 lines per file + +**If exceeding 400 lines, ask**: +- Are helpers single-use? +- Can lambdas replace functions? +- Are you covering too many concepts? +- Should this split into two files? diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 0bcc073..e04d81f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -30,33 +30,27 @@ Thank you for your interest in contributing! This repository helps developers le ### Required Dependencies -- **CMake 3.23+** when using **`cmake --preset`** (see `CMakePresets.json`); root `CMakeLists.txt` allows 3.14+ for manual configures -- **C++20 compatible compiler**: - - GCC 14 (reference compiler) - - Clang 17+ -- **GoogleTest** (for testing) +- **CMake 3.21+** when using **`cmake --preset`** (see `CMakePresets.json`); root `CMakeLists.txt` allows 3.21+ for manual configures +- **C++20-compatible compiler** (GCC or Clang with full C++20 support) +- **GoogleTest and GoogleMock** (for testing; see installation) - **Ninja** (recommended build system) -### Optional Dependencies - -- **Asio** (standalone, for multi-threaded shared_ptr tests only) - ### Installation **Ubuntu/Debian:** ```bash sudo apt update -sudo apt install libgtest-dev cmake build-essential ninja-build +sudo apt install cmake build-essential ninja-build ``` **Fedora/RHEL:** ```bash -sudo dnf install gtest-devel cmake gcc-c++ ninja-build +sudo dnf install cmake gcc-c++ ninja-build ``` **macOS:** ```bash -brew install googletest cmake ninja +brew install cmake ninja ``` ### Building @@ -75,66 +69,6 @@ ctest --preset gcc --verbose cmake --build --preset gcc --target test_reference_counting ``` -## Understanding the Socratic Q/A/R Pattern - -This repository uses inline comments for guided learning: - -- `// Q:` - Question posed about the code's behavior -- `// A:` - Space for learner's answer (fill this in) -- `// R:` - Response/feedback on the answer (provided after learner answers) - -### Example - -```cpp -// Q: What is the use_count after this line? -auto sp2 = sp1; -// A: 2 -// R: Correct! Copy construction increments the reference count. -``` - -When contributing Q/A/R content: -- Questions should be specific and answerable by running the code -- Focus on observable behavior (use_count, destructor calls, EventLog output) -- Responses should validate or correct the answer with technical precision -- Use instrumentation (`EventLog::instance().dump()`) to make behavior visible - -## Code Style Guidelines - -We follow specific C++ coding standards defined in `.cursor/rules/`: - -### Syntax Rules - -- **C++20 standard** is required (root `CMakeLists.txt` and presets) -- **Add newlines before opening braces** for readability -- **No trailing whitespace** on any line -- **File must end with exactly one newline** - -### Constructor Initializer Lists - -Use leading comma format: - -```cpp -SignalHandler::SignalHandler(asio::io_context& io_context) -: io_context_(io_context) -, signals_(io_context, SIGUSR1) -{ -} -``` - -- Constructor signature on its own line -- Colon on the next line after signature -- First initializer on same line as colon (or next line) -- Each additional initializer on its own line with **leading comma** -- Opening brace `{` on its own line - -### Documentation - -- **DO NOT** create additional README.md files in subdirectories -- **DO NOT** add large summary sections at the end of code files -- **DO** add concise inline comments for specific code sections -- **DO** use Q/A/R pattern for learning content -- **DO** update the main README.md module table and `docs/LEARNING_PATH.md` when adding or changing modules - ### Comments - Avoid obvious comments that just narrate code @@ -202,33 +136,7 @@ ctest --preset gcc --verbose ./build/gcc/learning_shared_ptr/test_reference_counting --gtest_filter=*BasicCreation* ``` -### Verify Code Style - -```bash -# Check for trailing whitespace -git diff --check - -# Verify file ends with newline -tail -c 1 your_file.cpp | od -An -tx1 -# Should output: 0a (newline) -``` - -### Test Instrumentation Output - -For tests using `EventLog`: - -```cpp -TEST(YourTest, YourCase) -{ - EventLog::instance().clear(); - - // Your test code - - EventLog::instance().dump(); // Verify output is correct - auto events = EventLog::instance().events(); - EXPECT_EQ(events.size(), expected_count); -} -``` +Configure presets write outputs under `build//` (here `gcc`); use `build/clang` if you use the clang preset. ## Submitting a Pull Request @@ -238,7 +146,7 @@ TEST(YourTest, YourCase) - [ ] Code follows style guidelines (no trailing whitespace, correct formatting) - [ ] Q/A/R patterns are preserved and correct (if applicable) - [ ] Main README.md updated (if adding new module) -- [ ] No syntax violations or compiler warnings +- [ ] No syntax violations or compiler warnings (Clang duplicated linker warnings are fine) - [ ] Commit messages are clear and descriptive ### PR Description diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index e98c2c2..fafb0b4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -14,7 +14,7 @@ A clear and concise description of what the bug is. 1. Navigate to module: `learning_*/` 2. Build target: `cmake --build --preset gcc --target ` -3. Run test: `./build/gcc//` +3. Run test: `./build///` (e.g. `./build/gcc/learning_shared_ptr/test_reference_counting`) 4. Observe the error at step... ## Expected Behavior diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 2b7a014..bae713e 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -15,7 +15,7 @@ A clear and concise description of what you'd like to see added to the learning What C++ concept or pattern should this feature cover? - **Topic**: [e.g., coroutines, ranges, concepts, specific STL container internals] -- **C++ Standard**: [e.g., C++20 (repo default), or topic focus such as C++17-era features] +- **C++ Standard**: [C++20 (repo default); note if the topic highlights features from an earlier ISO revision] - **Estimated Difficulty**: [⭐☆☆☆☆ to ⭐⭐⭐⭐⭐] ## Prerequisites diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e71b6aa..ff8971a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,7 @@ jobs: - name: Cache CMake build directory uses: actions/cache@v4 with: + # Matches CMakePresets.json binaryDir: ${sourceDir}/build/ path: build/${{ matrix.preset }} key: cmake-${{ matrix.os }}-${{ matrix.preset }}-${{ hashFiles('CMakeLists.txt', '**/CMakeLists.txt', 'CMakePresets.json') }} restore-keys: | @@ -55,20 +56,3 @@ jobs: - name: Test run: ctest --preset "${{ matrix.preset }}" --output-on-failure - - # format-check: - # runs-on: ubuntu-latest - - # steps: - # - name: Checkout - # uses: actions/checkout@v4 - - # - name: Install clang-format - # run: | - # sudo apt-get update - # sudo apt-get install -y clang-format - - # - name: Check formatting - # run: | - # find learning_* common examples profile_showcase -name '*.cpp' -o -name '*.h' \ - # | xargs clang-format --dry-run --Werror --style=file 2>&1 diff --git a/.gitignore b/.gitignore index 6c88924..a95b2d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,3 @@ -# clang -.clangd -.clang-format -.clang-tidy - # Prerequisites *.d @@ -66,6 +61,11 @@ _deps/ *~ .cursor/projects/ +# Keep shared tooling config tracked in the repo +!.clang-format +!.clang-tidy +!.clangd + # OS .DS_Store Thumbs.db diff --git a/CMakeLists.txt b/CMakeLists.txt index 05700ff..bcb3ea2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,24 @@ cmake_minimum_required(VERSION 3.14) project(cpp_learning LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) enable_testing() -find_package(GTest REQUIRED) +include(FetchContent) + +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.14.0 +) + +# For new CMake + GTest: +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # MSVC-friendly; harmless elsewhere +FetchContent_MakeAvailable(googletest) + find_package(Threads REQUIRED) list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") diff --git a/CMakePresets.json b/CMakePresets.json index e3c3dfd..f4acb89 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -1,6 +1,6 @@ { "version": 3, - "cmakeMinimumRequired": {"major": 3, "minor": 23, "patch": 0}, + "cmakeMinimumRequired": {"major": 3, "minor": 14, "patch": 0}, "configurePresets": [ { "name": "gcc", @@ -25,14 +25,50 @@ "CMAKE_BUILD_TYPE": "Debug", "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" } + }, + { + "name": "gcc-asan", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/gcc-asan", + "cacheVariables": { + "CMAKE_C_COMPILER": "gcc", + "CMAKE_CXX_COMPILER": "g++", + "CMAKE_CXX_STANDARD": "20", + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "CMAKE_C_FLAGS": "-fsanitize=address,undefined -fno-omit-frame-pointer", + "CMAKE_CXX_FLAGS": "-fsanitize=address,undefined -fno-omit-frame-pointer", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address,undefined", + "CMAKE_SHARED_LINKER_FLAGS": "-fsanitize=address,undefined" + } + }, + { + "name": "gcc-tsan", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/gcc-tsan", + "cacheVariables": { + "CMAKE_C_COMPILER": "gcc", + "CMAKE_CXX_COMPILER": "g++", + "CMAKE_CXX_STANDARD": "20", + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "CMAKE_C_FLAGS": "-fsanitize=thread -fno-omit-frame-pointer", + "CMAKE_CXX_FLAGS": "-fsanitize=thread -fno-omit-frame-pointer", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=thread", + "CMAKE_SHARED_LINKER_FLAGS": "-fsanitize=thread" + } } ], "buildPresets": [ {"name": "gcc", "configurePreset": "gcc"}, - {"name": "clang", "configurePreset": "clang"} + {"name": "clang", "configurePreset": "clang"}, + {"name": "gcc-asan", "configurePreset": "gcc-asan"}, + {"name": "gcc-tsan", "configurePreset": "gcc-tsan"} ], "testPresets": [ {"name": "gcc", "configurePreset": "gcc"}, - {"name": "clang", "configurePreset": "clang"} + {"name": "clang", "configurePreset": "clang"}, + {"name": "gcc-asan", "configurePreset": "gcc-asan"}, + {"name": "gcc-tsan", "configurePreset": "gcc-tsan"} ] } diff --git a/README.md b/README.md index 6b4ad4f..a708c13 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![C++20](https://img.shields.io/badge/C++-20-blue.svg)](https://en.cppreference.com/w/cpp/20) [![Platform](https://img.shields.io/badge/Platform-Linux%20%7C%20macOS-lightgrey.svg)]() -[![CMake](https://img.shields.io/badge/CMake-3.23+%20(presets)-064F8C.svg)](https://cmake.org) +[![CMake](https://img.shields.io/badge/CMake-3.21+%20(presets)-064F8C.svg)](https://cmake.org) ## Why This Exists @@ -31,6 +31,22 @@ The repository features an adaptive AI framework that guides learning through ev - **Observable Instrumentation** — EventLog and instrumented types (`Tracked`, `MoveTracked`, `Resource`) capture constructor, destructor, copy, move, and deleter activity so you can verify predictions. - **Adaptive Questioning** — The system adjusts questioning style, hint policy, verification rigor, and response depth based on your selected skill level. +### Developer-driven questions (DQ/DR) + +When **you** ask a question in code, use `// DQ: ` for your question and `// DR: ` for the mentor’s response (see [socratic-software-engineering.mdc](.cursor/rules/socratic-software-engineering.mdc)). Questions asked only in chat are answered in chat; use DQ/DR when you write `// DQ:` in the source. + +Example: + +```cpp +{ + Tracked a("hello"); + Tracked b = std::move(a); + // DQ: Would `a` still be safe to assign to after the move? + // DR: Yes — moved-from objects are in a valid-but-unspecified state, so assignment is safe. + // DR: What you cannot do is rely on `a`'s value. +} +``` + ### Skill Profiles Activate a profile by stating the exact override string (e.g., `"profile: staff"`) in Cursor chat: @@ -82,11 +98,16 @@ Beyond profiles, you can configure pacing, hints, questioning style, feedback mo You fill in TODOs and answer inline questions, run tests, and observe what happens (via `EventLog` and assertions). The adaptive framework adjusts to your profile. For Q/A/R details, instrumentation, and exercise types, see [Teaching Method](docs/TEACHING_METHOD.md). +### Exercise Markers + +- `// TODO:` means learner work is intentionally incomplete. +- `// SOLUTION:` means a reference implementation is intentionally shown for study. + ## Quickstart -1. **Prerequisites** — CMake **3.23+** for [CMakePresets.json](CMakePresets.json) (`cmake --preset gcc`). The root `CMakeLists.txt` allows **3.14+** if you configure manually; C++20 is set in the root file and presets. +1. **Prerequisites** — CMake **3.21+** for [CMakePresets.json](CMakePresets.json) (`cmake --preset gcc`). The root `CMakeLists.txt` allows **3.21+** if you configure manually; **C++20 is required** (set in the root file and presets). 2. **Build** — `cmake --preset gcc` then `cmake --build --preset gcc` -3. **Try it** — `./build/gcc/examples/test_try_it_out` +3. **Try it** — `./build/gcc/examples/test_try_it_out` (replace `gcc` with your configure preset name, e.g. `clang`) 4. **Next** — Start with `learning_shared_ptr` or follow [Full Curriculum](docs/LEARNING_PATH.md). ## Modules (high level) @@ -95,9 +116,9 @@ Registered test counts match each module’s `CMakeLists.txt` (what `ctest` runs | Module | Registered tests | Topic (short) | |--------|------------------|----------------| -| `learning_shared_ptr` | 16 | `shared_ptr` / `weak_ptr`, ownership, aliasing (18 `.cpp` on disk; see Learning Path) | +| `learning_shared_ptr` | 16 | `shared_ptr` / `weak_ptr`, ownership, aliasing (16 `.cpp` under `tests/`) | | `learning_memory` | 4 | Placement new, allocators, pools, alignment | -| `learning_modern_cpp` | 8 | C++11/14/17 features (build is C++20) | +| `learning_modern_cpp` | 8 | Modern C++ evolution (by lesson); **C++20** required | | `learning_raii` | 4 | Scope guards, handles, smart pointers from scratch | | `learning_move_semantics` | 5 | Moves, forwarding, move-only types | | `learning_error_handling` | 5 | Exceptions, optional/result, noexcept | @@ -107,7 +128,7 @@ Registered test counts match each module’s `CMakeLists.txt` (what `ctest` runs | `learning_templates` | 6 | Templates, SFINAE, variadics, traits | | `learning_performance` | 6 | Profiling, cache layout, elision, SSO, constexpr, benchmarks | | `learning_debugging` | 3 | GoogleMock, debugging, benchmark exercise | -| `learning_deadlocks` | **0** | Four scenario files exist; **CMake targets commented out** until you enable them | +| `learning_deadlocks` | 4 | Deadlock scenario suites are registered by default; some scenario cases are intentionally disabled while remaining fixes are in progress | | `examples` | 1 | Onboarding test | | `profile_showcase` | 1 | Move instrumentation demo | @@ -134,7 +155,7 @@ Registered test counts match each module’s `CMakeLists.txt` (what `ctest` runs ## Building and running -Quick path: `cmake --preset gcc`, `cmake --build --preset gcc`, `ctest --preset gcc --verbose`. Dependencies (GoogleTest, optional Asio), targeted tests, and CMake notes: [docs/BUILDING.md](docs/BUILDING.md). +Quick path: `cmake --preset gcc`, `cmake --build --preset gcc`, `ctest --preset gcc --verbose`. Dependencies (GoogleTest/GoogleMock), targeted tests, and CMake notes: [docs/BUILDING.md](docs/BUILDING.md). ## Contributing diff --git a/common/src/instrumentation.cpp b/common/src/instrumentation.cpp index da6f779..5da8d08 100644 --- a/common/src/instrumentation.cpp +++ b/common/src/instrumentation.cpp @@ -1,4 +1,5 @@ #include "instrumentation.h" + #include EventLog& EventLog::instance() @@ -52,27 +53,21 @@ size_t EventLog::count_events(const std::string& substring) const int Tracked::next_id_ = 1; -Tracked::Tracked(const std::string& name) -: name_(name) -, id_(next_id_++) +Tracked::Tracked(const std::string& name) : name_(name), id_(next_id_++) { std::ostringstream oss; oss << "Tracked(" << name_ << ")::ctor [id=" << id_ << "]"; EventLog::instance().record(oss.str()); } -Tracked::Tracked(const Tracked& other) -: name_(other.name_) -, id_(next_id_++) +Tracked::Tracked(const Tracked& other) : name_(other.name_), id_(next_id_++) { std::ostringstream oss; oss << "Tracked(" << name_ << ")::copy_ctor from [id=" << other.id_ << "] to [id=" << id_ << "]"; EventLog::instance().record(oss.str()); } -Tracked::Tracked(Tracked&& other) noexcept -: name_(std::move(other.name_)) -, id_(other.id_) +Tracked::Tracked(Tracked&& other) noexcept : name_(std::move(other.name_)), id_(other.id_) { std::ostringstream oss; oss << "Tracked(" << name_ << ")::move_ctor [id=" << id_ << "]"; diff --git a/common/src/instrumentation.h b/common/src/instrumentation.h index 807cd56..6ce178e 100644 --- a/common/src/instrumentation.h +++ b/common/src/instrumentation.h @@ -1,11 +1,11 @@ #ifndef INSTRUMENTATION_H #define INSTRUMENTATION_H -#include -#include -#include #include #include +#include +#include +#include class EventLog { @@ -43,12 +43,10 @@ class Tracked static int next_id_; }; -template -class LoggingDeleter +template class LoggingDeleter { public: - explicit LoggingDeleter(const std::string& deleter_name = "LoggingDeleter") - : deleter_name_(deleter_name) + explicit LoggingDeleter(const std::string& deleter_name = "LoggingDeleter") : deleter_name_(deleter_name) { } @@ -64,12 +62,10 @@ class LoggingDeleter std::string deleter_name_; }; -template -class LoggingArrayDeleter +template class LoggingArrayDeleter { public: - explicit LoggingArrayDeleter(const std::string& deleter_name = "LoggingArrayDeleter") - : deleter_name_(deleter_name) + explicit LoggingArrayDeleter(const std::string& deleter_name = "LoggingArrayDeleter") : deleter_name_(deleter_name) { } diff --git a/common/src/move_instrumentation.cpp b/common/src/move_instrumentation.cpp index 1178bde..7515af5 100644 --- a/common/src/move_instrumentation.cpp +++ b/common/src/move_instrumentation.cpp @@ -1,22 +1,17 @@ #include "move_instrumentation.h" + #include int MoveTracked::next_id_ = 1; -MoveTracked::MoveTracked(const std::string& name) -: name_(name) -, id_(next_id_++) -, moved_from_(false) +MoveTracked::MoveTracked(const std::string& name) : name_(name), id_(next_id_++), moved_from_(false) { std::ostringstream oss; oss << "MoveTracked(" << name_ << ")::ctor [id=" << id_ << "]"; EventLog::instance().record(oss.str()); } -MoveTracked::MoveTracked(const MoveTracked& other) -: name_(other.name_) -, id_(next_id_++) -, moved_from_(false) +MoveTracked::MoveTracked(const MoveTracked& other) : name_(other.name_), id_(next_id_++), moved_from_(false) { std::ostringstream oss; oss << "MoveTracked(" << name_ << ")::copy_ctor from [id=" << other.id_ << "] to [id=" << id_ << "]"; @@ -24,9 +19,7 @@ MoveTracked::MoveTracked(const MoveTracked& other) } MoveTracked::MoveTracked(MoveTracked&& other) noexcept -: name_(std::move(other.name_)) -, id_(other.id_) -, moved_from_(false) + : name_(std::move(other.name_)), id_(other.id_), moved_from_(false) { std::ostringstream oss; oss << "MoveTracked(" << name_ << ")::move_ctor from [id=" << other.id_ << "]"; @@ -86,20 +79,14 @@ bool MoveTracked::is_moved_from() const int Resource::next_id_ = 1; -Resource::Resource(const std::string& name) -: name_(name) -, id_(next_id_++) -, valid_(true) +Resource::Resource(const std::string& name) : name_(name), id_(next_id_++), valid_(true) { std::ostringstream oss; oss << "Resource(" << name_ << ")::ctor [id=" << id_ << "]"; EventLog::instance().record(oss.str()); } -Resource::Resource(Resource&& other) noexcept -: name_(std::move(other.name_)) -, id_(other.id_) -, valid_(true) +Resource::Resource(Resource&& other) noexcept : name_(std::move(other.name_)), id_(other.id_), valid_(true) { std::ostringstream oss; oss << "Resource(" << name_ << ")::move_ctor from [id=" << other.id_ << "]"; diff --git a/common/src/move_instrumentation.h b/common/src/move_instrumentation.h index db3cbe1..f071065 100644 --- a/common/src/move_instrumentation.h +++ b/common/src/move_instrumentation.h @@ -2,8 +2,9 @@ #define MOVE_INSTRUMENTATION_H #include "instrumentation.h" -#include + #include +#include class MoveTracked { @@ -47,11 +48,10 @@ class Resource static int next_id_; }; -template -MoveTracked make_value(T&& arg) +template MoveTracked make_value(T&& arg) { EventLog::instance().record("make_value() called with " + - std::string(std::is_lvalue_reference::value ? "lvalue" : "rvalue")); + std::string(std::is_lvalue_reference::value ? "lvalue" : "rvalue")); return MoveTracked(std::forward(arg)); } diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 0d2ca2b..e3e4a54 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -6,9 +6,9 @@ Build setup, dependencies, and how to run tests. For project overview, see the [ ## Requirements -- **CMake** — **3.23 or newer** if you use **`cmake --preset`** ([CMakePresets.json](../CMakePresets.json)). The root [CMakeLists.txt](../CMakeLists.txt) declares `cmake_minimum_required(3.14)` for manual configures; presets are the documented workflow. -- **Compiler** — **C++20** (GCC 14 or recent Clang recommended). -- **GoogleTest & GoogleMock** — Required (`find_package(GTest REQUIRED)`). +- **CMake** — **3.21 or newer** if you use **`cmake --preset`** ([CMakePresets.json](../CMakePresets.json)). The root [CMakeLists.txt](../CMakeLists.txt) declares `cmake_minimum_required(3.21)` for manual configures; presets are the documented workflow. +- **Compiler** — **C++20** required (use a C++20-capable GCC or Clang). +- **GoogleTest & GoogleMock** — Install development packages for your OS (below). The build also uses **FetchContent** to obtain GoogleTest when you configure with CMake. - **Threads** — For concurrency tests (and deadlock tests when enabled). --- @@ -27,12 +27,14 @@ ctest --preset gcc --verbose Use `cmake --preset clang` / `cmake --build --preset clang` if you prefer Clang. +Each configure preset writes its build tree under **`build//`** (for example `build/gcc`, `build/clang`, `build/gcc-asan`). Test binaries and `compile_commands.json` live there. + --- ## Running specific tests ```bash -# Run one executable (path matches your preset output directory) +# Run one executable (use the directory for your preset, e.g. build/gcc) ./build/gcc/learning_shared_ptr/test_reference_counting # GoogleTest filter @@ -51,7 +53,7 @@ ctest -R test_reference_counting --output-on-failure ### `learning_deadlocks` -Targets are **not** registered by default (`add_learning_test` lines are commented in `learning_deadlocks/CMakeLists.txt`). After you uncomment them and reconfigure, binaries would appear under `build/gcc/learning_deadlocks/`. See [learning_deadlocks/SUMMARY.txt](../learning_deadlocks/SUMMARY.txt) for scenario layout. +Targets are registered by default. Binaries appear under `build//learning_deadlocks/` (for example `build/gcc/learning_deadlocks/`). Some scenario tests are intentionally disabled while remaining fixes are in progress. Scenarios live in `learning_deadlocks/tests/` (`test_mutex_ordering_deadlocks.cpp`, `test_circular_reference_deadlocks.cpp`, `test_condition_variable_deadlocks.cpp`, `test_ownership_transfer_deadlocks.cpp`). --- @@ -82,10 +84,10 @@ brew install googletest cmake ninja ## Tools and environment -- **Compiler:** GCC 14 / recent Clang with **C++20** -- **Build:** CMake **3.23+** for presets; Ninja recommended (generator in presets) +- **Compiler:** C++20-capable **GCC** or **Clang** +- **Build:** CMake **3.21+** for presets; Ninja recommended (generator in presets) - **Tests:** GoogleTest & GoogleMock -- **IDE:** Cursor (optional Socratic rules in `.cursor/rules/`) +- **IDE:** Cursor (optional Socratic rules in `.cursor/rules/`). `.clangd` points at `build/gcc` for `compile_commands.json`; switch to `build/clang` if you use the clang preset. --- diff --git a/docs/LEARNING_PATH.md b/docs/LEARNING_PATH.md index ea6edc1..aa8ff58 100644 --- a/docs/LEARNING_PATH.md +++ b/docs/LEARNING_PATH.md @@ -11,7 +11,7 @@ These exercises assume you can **read and write C++ at a working-engineer level* - **New to advanced topics but solid on basics:** Use Cursor with `"profile: junior"` for gentler explanations and proactive hints; you still work the same tests. - **Absolute beginners:** Use an introductory C++ resource first, then return here for ownership, concurrency, and related depth. -The project **compiles as C++20**; individual lessons may still name the standard that introduced a feature (C++11, C++14, C++17). +The project **requires C++20**. Lesson comments may still name the ISO C++ revision that introduced a feature, without changing the build standard. --- @@ -21,9 +21,9 @@ Each row is a `learning_*` directory. **Registered tests** are targets listed in | Module | Registered tests | Notes | |--------|-------------------|--------| -| [learning_shared_ptr](../learning_shared_ptr/) | 16 | 18 `.cpp` files under `tests/`; `test_multi_threaded_patterns.cpp` and `test_asio_basics.cpp` are not registered yet (optional Asio). | +| [learning_shared_ptr](../learning_shared_ptr/) | 16 | 16 `.cpp` files under `tests/` (all registered). | | [learning_memory](../learning_memory/) | 4 | Placement new, allocators, pools, alignment. | -| [learning_modern_cpp](../learning_modern_cpp/) | 8 | C++11/14/17 *language features*; build is still C++20. | +| [learning_modern_cpp](../learning_modern_cpp/) | 8 | Modern C++ evolution (by lesson); **C++20** required. | | [learning_raii](../learning_raii/) | 4 | Scope guards, handles, custom managers, pointers from scratch. | | [learning_move_semantics](../learning_move_semantics/) | 5 | Value categories, move, forward, move-only types. | | [learning_error_handling](../learning_error_handling/) | 5 | Exceptions, optional/result, noexcept, etc. | @@ -33,7 +33,7 @@ Each row is a `learning_*` directory. **Registered tests** are targets listed in | [learning_templates](../learning_templates/) | 6 | Specialization, SFINAE, variadics, traits, etc. | | [learning_performance](../learning_performance/) | 6 | Profiling, cache layout, elision, SSO, constexpr, benchmarks. | | [learning_debugging](../learning_debugging/) | 3 | GoogleMock, debugging techniques, benchmark exercise. | -| [learning_deadlocks](../learning_deadlocks/) | **0** | Four test sources exist (16 scenarios); **`add_learning_test` lines are commented out** in `CMakeLists.txt` — not part of default `ctest` until enabled. | +| [learning_deadlocks](../learning_deadlocks/) | 4 | Four deadlock suites are part of default `ctest`; several scenario cases are intentionally disabled while remaining fixes are in progress. | | [examples](../examples/) | 1 | `test_try_it_out` — good first run after build. | | [profile_showcase](../profile_showcase/) | 1 | Demonstrates move instrumentation. | @@ -45,13 +45,13 @@ Each row is a `learning_*` directory. **Registered tests** are targets listed in Dependencies are soft; adjust for your goals. -1. **Orientation** — Build the project, run `./build/gcc/examples/test_try_it_out` (or your preset output directory). +1. **Orientation** — Build the project, run `./build//examples/test_try_it_out` (for example `./build/gcc/examples/test_try_it_out` after `cmake --preset gcc`). 2. **Ownership baseline** — `learning_shared_ptr` (then `learning_raii` / `learning_memory` as needed). 3. **Move semantics** — `learning_move_semantics` (pairs well after shared_ptr). 4. **Modern syntax and STL** — `learning_modern_cpp`, `learning_stl`. 5. **Error handling and templates** — `learning_error_handling`, `learning_templates` (order flexible). 6. **Concurrency** — `learning_concurrency` after you are comfortable with mutex/RAII basics. -7. **Deadlocks (optional lab)** — Uncomment targets in `learning_deadlocks/CMakeLists.txt` when you are ready; complete scenarios one file at a time. +7. **Deadlocks (advanced lab)** — Work through `learning_deadlocks` scenarios one file at a time; some cases are intentionally disabled until remaining fixes are completed. 8. **Patterns, performance, debugging** — `learning_design_patterns`, `learning_performance`, `learning_debugging` in any order that matches your projects. --- diff --git a/docs/TEACHING_METHOD.md b/docs/TEACHING_METHOD.md index 3a12bd4..ef20739 100644 --- a/docs/TEACHING_METHOD.md +++ b/docs/TEACHING_METHOD.md @@ -6,7 +6,7 @@ This document explains the pedagogical approach used in this repository. For an ## Overview -The repository uses a **Socratic teaching methodology**: you are asked questions before receiving explanations. Exercises combine fill-in code, broken implementations to fix, and guided Q/A/R (Question/Answer/Response) patterns. Every exercise uses **instrumentation** so that runtime behavior (constructors, destructors, copies, moves) is observable. You predict what will happen, run the code, see the results, and learn from evidence-based feedback. The codebase **builds as C++20** by default (`CMakeLists.txt` and `CMakePresets.json`); comments and Q/A/R may still refer to the standard that introduced a feature (for example C++11, C++14, or C++17). +The repository uses a **Socratic teaching methodology**: you are asked questions before receiving explanations. Exercises combine fill-in code, broken implementations to fix, and guided Q/A/R (Question/Answer/Response) patterns. Every exercise uses **instrumentation** so that runtime behavior (constructors, destructors, copies, moves) is observable. You predict what will happen, run the code, see the results, and learn from evidence-based feedback. The codebase **requires C++20** (`CMakeLists.txt` and `CMakePresets.json`); comments and Q/A/R may still refer to the ISO C++ revision that introduced a feature, without changing the build standard. --- @@ -56,13 +56,13 @@ The `.cursor/rules/` directory contains AI teaching rules that activate automati ### Customizing Your Experience -The Socratic method is configurable. Tell the AI your preferences in chat: +The Socratic method is configurable. Tell the AI your preferences in chat (override strings match [.cursor/rules/socratic-software-engineering.mdc](../.cursor/rules/socratic-software-engineering.mdc)): -- **Pacing**: "one test at a time" (default), "bulk mode", "self-directed" -- **Hints**: "on request" (default for most profiles), "proactive", "ladder-after-2" -- **Feedback**: inline Q/A/R (default), "chat-only", "mixed mode" -- **Response depth**: depends on profile (e.g. senior: precise technical); override with "beginner-friendly", "mechanism-focused", etc. -- **Verification**: "trust context" (default in the main rule), or "relaxed" / stricter modes as documented in the rule file +- **Pacing**: `pacing: one-test` (default), `pacing: self-directed`, `pacing: bulk-self-assessment` +- **Hints**: `hints: on-request` (default for many profiles), `hints: proactive`, `hints: ladder-after-2` +- **Response depth**: depends on profile (e.g. senior: precise technical); overrides include `depth: beginner-friendly`, `depth: mechanism-focused`, etc. +- **Verification**: `verification: trust-context` (default in the main rule), or `verification: relaxed` as documented there +- **Questioning style**: `questioning: standard` (default), `questioning: adversarial`, `questioning: pathological` See [.cursor/rules/socratic-software-engineering.mdc](../.cursor/rules/socratic-software-engineering.mdc) for authoritative defaults and the full option list. diff --git a/examples/test_try_it_out.cpp b/examples/test_try_it_out.cpp index 2d2113b..f6b4755 100644 --- a/examples/test_try_it_out.cpp +++ b/examples/test_try_it_out.cpp @@ -1,4 +1,5 @@ #include "instrumentation.h" + #include #include @@ -14,21 +15,21 @@ class TryItOutTest : public ::testing::Test TEST_F(TryItOutTest, CopyingSharedPtr) { std::shared_ptr p1 = std::make_shared("Original"); - + // Q: What is the use_count of p1 at this point? // A: // R: - + std::shared_ptr p2 = p1; - + // Q: After copying p1 to p2, what is the use_count of both p1 and p2? // A: // R: - + // Q: How many Tracked objects exist in memory right now? // A: // R: - + auto events = EventLog::instance().events(); EXPECT_EQ(p1.use_count(), 2); EXPECT_EQ(p2.use_count(), 2); @@ -38,21 +39,21 @@ TEST_F(TryItOutTest, CopyingSharedPtr) TEST_F(TryItOutTest, MovingSharedPtr) { std::shared_ptr p1 = std::make_shared("Original"); - + std::shared_ptr p2 = std::move(p1); - + // Q: After the move, what is the use_count of p2? // A: // R: - + // Q: After the move, what is the use_count of p1? // A: // R: - + // Q: Why does the moved-from p1 have use_count 0 but still exist as a valid variable? // A: // R: - + auto events = EventLog::instance().events(); EXPECT_EQ(p2.use_count(), 1); EXPECT_EQ(p1.use_count(), 0); diff --git a/learning_concurrency/tests/test_lock_free_basics.cpp b/learning_concurrency/tests/test_lock_free_basics.cpp index 13e76f1..2b80d17 100644 --- a/learning_concurrency/tests/test_lock_free_basics.cpp +++ b/learning_concurrency/tests/test_lock_free_basics.cpp @@ -2,13 +2,13 @@ // Estimated Time: 6 hours // Difficulty: Hard - -#include #include "instrumentation.h" + #include +#include +#include #include #include -#include class LockFreeBasicsTest : public ::testing::Test { @@ -32,8 +32,7 @@ TEST_F(LockFreeBasicsTest, AtomicOperationsBasics) std::vector threads; for (int i = 0; i < num_threads; ++i) { - threads.emplace_back([&counter, increments_per_thread]() - { + threads.emplace_back([&counter, increments_per_thread]() { for (int j = 0; j < increments_per_thread; ++j) { counter.fetch_add(1, std::memory_order_relaxed); @@ -82,15 +81,16 @@ struct Node class SimpleLockFreeStack { public: - SimpleLockFreeStack() : head_(nullptr) {} + SimpleLockFreeStack() : head_(nullptr) + { + } void push(int value) { Node* new_node = new Node(value); new_node->next = head_.load(std::memory_order_relaxed); - while (!head_.compare_exchange_weak(new_node->next, new_node, - std::memory_order_release, + while (!head_.compare_exchange_weak(new_node->next, new_node, std::memory_order_release, std::memory_order_relaxed)) { EventLog::instance().record("push: CAS retry for value=" + std::to_string(value)); @@ -105,8 +105,7 @@ class SimpleLockFreeStack while (old_head != nullptr) { - if (head_.compare_exchange_weak(old_head, old_head->next, - std::memory_order_release, + if (head_.compare_exchange_weak(old_head, old_head->next, std::memory_order_release, std::memory_order_acquire)) { value = old_head->value; @@ -123,7 +122,9 @@ class SimpleLockFreeStack ~SimpleLockFreeStack() { int value; - while (pop(value)) {} + while (pop(value)) + { + } } private: @@ -140,8 +141,7 @@ TEST_F(LockFreeBasicsTest, CompareExchangeBasics) std::vector threads; for (int i = 0; i < num_threads; ++i) { - threads.emplace_back([&stack, i, items_per_thread]() - { + threads.emplace_back([&stack, i, items_per_thread]() { for (int j = 0; j < items_per_thread; ++j) { stack.push(i * 100 + j); @@ -191,7 +191,9 @@ void producer_thread() void consumer_thread() { - while (!ready.load(std::memory_order_acquire)) {} + while (!ready.load(std::memory_order_acquire)) + { + } int value = data.load(std::memory_order_relaxed); EventLog::instance().record("Consumer: read data=" + std::to_string(value)); EXPECT_EQ(value, 42); @@ -235,7 +237,9 @@ TEST_F(LockFreeBasicsTest, AcquireReleaseSemantics) class SafeLockFreeStack { public: - SafeLockFreeStack() : head_(nullptr) {} + SafeLockFreeStack() : head_(nullptr) + { + } void push(int value) { @@ -267,8 +271,7 @@ TEST_F(LockFreeBasicsTest, DISABLED_SafeLockFreeStackMemoryReclamation) std::vector pushers; for (int i = 0; i < num_threads; ++i) { - pushers.emplace_back([&stack, i, items_per_thread]() - { + pushers.emplace_back([&stack, i, items_per_thread]() { for (int j = 0; j < items_per_thread; ++j) { stack.push(i * 1000 + j); @@ -279,12 +282,13 @@ TEST_F(LockFreeBasicsTest, DISABLED_SafeLockFreeStackMemoryReclamation) std::vector poppers; for (int i = 0; i < num_threads; ++i) { - poppers.emplace_back([&stack, items_per_thread]() - { + poppers.emplace_back([&stack, items_per_thread]() { int value; for (int j = 0; j < items_per_thread; ++j) { - while (!stack.pop(value)) {} + while (!stack.pop(value)) + { + } } }); } @@ -340,8 +344,7 @@ TEST_F(LockFreeBasicsTest, SpinlockBasics) std::vector threads; for (int i = 0; i < num_threads; ++i) { - threads.emplace_back([&spinlock, &shared_value, increments_per_thread]() - { + threads.emplace_back([&spinlock, &shared_value, increments_per_thread]() { for (int j = 0; j < increments_per_thread; ++j) { spinlock.lock(); @@ -391,14 +394,12 @@ TEST_F(LockFreeBasicsTest, SequentialConsistencyVsRelaxed) r1.store(0, std::memory_order_relaxed); r2.store(0, std::memory_order_relaxed); - std::thread t1([&]() - { + std::thread t1([&]() { x.store(1, std::memory_order_relaxed); r1.store(y.load(std::memory_order_relaxed), std::memory_order_relaxed); }); - std::thread t2([&]() - { + std::thread t2([&]() { y.store(1, std::memory_order_relaxed); r2.store(x.load(std::memory_order_relaxed), std::memory_order_relaxed); }); diff --git a/learning_concurrency/tests/test_producer_consumer_advanced.cpp b/learning_concurrency/tests/test_producer_consumer_advanced.cpp index 021cf3e..4f47509 100644 --- a/learning_concurrency/tests/test_producer_consumer_advanced.cpp +++ b/learning_concurrency/tests/test_producer_consumer_advanced.cpp @@ -2,17 +2,17 @@ // Estimated Time: 4 hours // Difficulty: Moderate - -#include #include "instrumentation.h" -#include -#include -#include -#include -#include + #include #include +#include +#include +#include #include +#include +#include +#include class ProducerConsumerTest : public ::testing::Test { @@ -27,8 +27,7 @@ class ProducerConsumerTest : public ::testing::Test // TEST 1: Basic Producer-Consumer with Unbounded Queue - Easy // ============================================================================ -template -class UnboundedQueue +template class UnboundedQueue { public: void push(T value) @@ -63,8 +62,7 @@ TEST_F(ProducerConsumerTest, BasicProducerConsumer) constexpr int num_items = 10; std::atomic consumed{0}; - std::thread producer([&queue, num_items]() - { + std::thread producer([&queue, num_items]() { for (int i = 0; i < num_items; ++i) { queue.push(i); @@ -72,8 +70,7 @@ TEST_F(ProducerConsumerTest, BasicProducerConsumer) } }); - std::thread consumer([&queue, &consumed, num_items]() - { + std::thread consumer([&queue, &consumed, num_items]() { for (int i = 0; i < num_items; ++i) { int val = queue.pop(); @@ -109,8 +106,7 @@ TEST_F(ProducerConsumerTest, MultipleProducersConsumers) std::vector producers; for (int p = 0; p < num_producers; ++p) { - producers.emplace_back([&queue, p, items_per_producer]() - { + producers.emplace_back([&queue, p, items_per_producer]() { for (int i = 0; i < items_per_producer; ++i) { queue.push(p * 100 + i); @@ -121,8 +117,7 @@ TEST_F(ProducerConsumerTest, MultipleProducersConsumers) std::vector consumers; for (int c = 0; c < num_consumers; ++c) { - consumers.emplace_back([&queue, &total_consumed, items_per_producer, num_producers]() - { + consumers.emplace_back([&queue, &total_consumed, items_per_producer, num_producers]() { int items_to_consume = (items_per_producer * num_producers) / num_consumers; for (int i = 0; i < items_to_consume; ++i) { @@ -154,12 +149,10 @@ TEST_F(ProducerConsumerTest, MultipleProducersConsumers) // TEST 3: Bounded Queue with Blocking - Moderate // ============================================================================ -template -class BoundedQueue +template class BoundedQueue { public: - explicit BoundedQueue(size_t capacity) - : capacity_(capacity) + explicit BoundedQueue(size_t capacity) : capacity_(capacity) { } @@ -203,8 +196,7 @@ TEST_F(ProducerConsumerTest, BoundedQueueBlocking) std::atomic producer_blocked{false}; std::atomic items_produced{0}; - std::thread fast_producer([&queue, &producer_blocked, &items_produced]() - { + std::thread fast_producer([&queue, &producer_blocked, &items_produced]() { for (int i = 0; i < 10; ++i) { if (i == 5) @@ -221,8 +213,7 @@ TEST_F(ProducerConsumerTest, BoundedQueueBlocking) EXPECT_TRUE(producer_blocked.load()); EXPECT_LE(items_produced.load(), 6); - std::thread consumer([&queue]() - { + std::thread consumer([&queue]() { for (int i = 0; i < 10; ++i) { queue.pop(); @@ -256,8 +247,7 @@ TEST_F(ProducerConsumerTest, BoundedQueueBlocking) // TODO: 3. Consumers block when queue is empty // TODO: Use std::priority_queue and condition_variable -template -class PriorityQueue +template class PriorityQueue { public: void push(T value, int priority) @@ -279,8 +269,7 @@ TEST_F(ProducerConsumerTest, DISABLED_PriorityQueueOrdering) { PriorityQueue queue; - std::thread producer([&queue]() - { + std::thread producer([&queue]() { queue.push(1, 1); queue.push(3, 3); queue.push(2, 2); @@ -289,8 +278,7 @@ TEST_F(ProducerConsumerTest, DISABLED_PriorityQueueOrdering) producer.join(); std::vector consumed; - std::thread consumer([&queue, &consumed]() - { + std::thread consumer([&queue, &consumed]() { for (int i = 0; i < 3; ++i) { consumed.push_back(queue.pop()); @@ -313,8 +301,7 @@ TEST_F(ProducerConsumerTest, DISABLED_PriorityQueueOrdering) // TEST 5: Spurious Wakeups and Predicate Importance - Hard // ============================================================================ -template -class WakeupDemoQueue +template class WakeupDemoQueue { public: void push(T value) @@ -360,8 +347,7 @@ TEST_F(ProducerConsumerTest, SpuriousWakeupsWithPredicate) { WakeupDemoQueue queue; - std::thread consumer([&queue]() - { + std::thread consumer([&queue]() { int val = queue.pop_with_predicate(); EXPECT_EQ(val, 42); }); @@ -434,10 +420,7 @@ TEST_F(ProducerConsumerTest, NotifyOneVsNotifyAll) std::vector waiters; for (int i = 0; i < num_waiters; ++i) { - waiters.emplace_back([&demo, i]() - { - demo.wait_for_signal(i); - }); + waiters.emplace_back([&demo, i]() { demo.wait_for_signal(i); }); } std::this_thread::sleep_for(std::chrono::milliseconds(10)); diff --git a/learning_concurrency/tests/test_reader_writer_locks.cpp b/learning_concurrency/tests/test_reader_writer_locks.cpp index bf95c9e..eff4cbd 100644 --- a/learning_concurrency/tests/test_reader_writer_locks.cpp +++ b/learning_concurrency/tests/test_reader_writer_locks.cpp @@ -2,15 +2,15 @@ // Estimated Time: 4 hours // Difficulty: Moderate +#include "instrumentation.h" +#include +#include #include -#include "instrumentation.h" -#include -#include #include +#include +#include #include -#include -#include class ReaderWriterLocksTest : public ::testing::Test { @@ -28,7 +28,9 @@ class ReaderWriterLocksTest : public ::testing::Test class SharedCounter { public: - SharedCounter() : value_(0) {} + SharedCounter() : value_(0) + { + } int read() const { @@ -70,10 +72,11 @@ TEST_F(ReaderWriterLocksTest, MultipleReadersNoContention) for (int i = 0; i < num_readers; ++i) { - threads.emplace_back([&counter, &ready_count]() - { + threads.emplace_back([&counter, &ready_count]() { ready_count.fetch_add(1); - while (ready_count.load() < num_readers) {} + while (ready_count.load() < num_readers) + { + } int val = counter.read(); EXPECT_EQ(val, 42); }); @@ -84,8 +87,8 @@ TEST_F(ReaderWriterLocksTest, MultipleReadersNoContention) t.join(); } - auto elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - start).count(); + auto elapsed = + std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); int concurrent_shared_lock_events = EventLog::instance().count_events("acquired shared_lock"); // Compare against a local serialized baseline to reduce cross-platform @@ -95,8 +98,8 @@ TEST_F(ReaderWriterLocksTest, MultipleReadersNoContention) { EXPECT_EQ(counter.read(), 42); } - auto sequential_elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - seq_start).count(); + auto sequential_elapsed = + std::chrono::duration_cast(std::chrono::steady_clock::now() - seq_start).count(); // Q: If readers held exclusive locks, 5 readers × 10ms sleep = 50ms minimum. // Q: With shared_lock, what is the expected minimum elapsed time? Why? @@ -118,14 +121,15 @@ TEST_F(ReaderWriterLocksTest, WriterBlocksReaders) std::atomic writer_finished{false}; std::atomic readers_blocked{0}; - std::thread writer([&counter, &writer_started, &writer_finished]() - { + std::thread writer([&counter, &writer_started, &writer_finished]() { writer_started.store(true); counter.write(100); writer_finished.store(true); }); - while (!writer_started.load()) {} + while (!writer_started.load()) + { + } std::this_thread::sleep_for(std::chrono::milliseconds(2)); constexpr int num_readers = 3; @@ -133,8 +137,7 @@ TEST_F(ReaderWriterLocksTest, WriterBlocksReaders) for (int i = 0; i < num_readers; ++i) { - readers.emplace_back([&counter, &writer_finished, &readers_blocked]() - { + readers.emplace_back([&counter, &writer_finished, &readers_blocked]() { readers_blocked.fetch_add(1); int val = counter.read(); EXPECT_TRUE(writer_finished.load()); @@ -171,19 +174,19 @@ TEST_F(ReaderWriterLocksTest, ReaderBlocksWriter) std::atomic reader_finished{false}; std::atomic writer_blocked{false}; - std::thread reader([&counter, &reader_started, &reader_finished]() - { + std::thread reader([&counter, &reader_started, &reader_finished]() { reader_started.store(true); int val = counter.read(); EXPECT_EQ(val, 50); reader_finished.store(true); }); - while (!reader_started.load()) {} + while (!reader_started.load()) + { + } std::this_thread::sleep_for(std::chrono::milliseconds(2)); - std::thread writer([&counter, &writer_blocked, &reader_finished]() - { + std::thread writer([&counter, &writer_blocked, &reader_finished]() { writer_blocked.store(true); counter.write(200); EXPECT_TRUE(reader_finished.load()); @@ -215,7 +218,9 @@ TEST_F(ReaderWriterLocksTest, ReaderBlocksWriter) class UpgradableCache { public: - UpgradableCache() : value_(0) {} + UpgradableCache() : value_(0) + { + } int get_or_compute(int key) { @@ -241,8 +246,7 @@ TEST_F(ReaderWriterLocksTest, DISABLED_UpgradableLockPattern) for (int i = 0; i < num_threads; ++i) { - threads.emplace_back([&cache, &compute_count]() - { + threads.emplace_back([&cache, &compute_count]() { int val = cache.get_or_compute(1); EXPECT_EQ(val, 1); }); @@ -266,7 +270,9 @@ TEST_F(ReaderWriterLocksTest, DISABLED_UpgradableLockPattern) class StarvationDemo { public: - StarvationDemo() : value_(0) {} + StarvationDemo() : value_(0) + { + } int read() const { @@ -299,8 +305,7 @@ TEST_F(ReaderWriterLocksTest, ReaderWriterStarvation) for (int i = 0; i < num_readers; ++i) { - readers.emplace_back([&demo, &stop]() - { + readers.emplace_back([&demo, &stop]() { while (!stop.load()) { demo.read(); @@ -311,8 +316,7 @@ TEST_F(ReaderWriterLocksTest, ReaderWriterStarvation) std::this_thread::sleep_for(std::chrono::milliseconds(10)); - std::thread writer([&demo, &write_count]() - { + std::thread writer([&demo, &write_count]() { for (int i = 0; i < 5; ++i) { demo.write(i); @@ -344,7 +348,9 @@ TEST_F(ReaderWriterLocksTest, ReaderWriterStarvation) class ExceptionSafeReader { public: - ExceptionSafeReader() : value_(0) {} + ExceptionSafeReader() : value_(0) + { + } int read_with_exception() const { @@ -370,8 +376,7 @@ TEST_F(ReaderWriterLocksTest, SharedLockExceptionSafety) { ExceptionSafeReader reader; - std::thread t1([&reader]() - { + std::thread t1([&reader]() { try { reader.read_with_exception(); @@ -384,10 +389,7 @@ TEST_F(ReaderWriterLocksTest, SharedLockExceptionSafety) t1.join(); - std::thread t2([&reader]() - { - reader.write(100); - }); + std::thread t2([&reader]() { reader.write(100); }); t2.join(); diff --git a/learning_concurrency/tests/test_thread_pools.cpp b/learning_concurrency/tests/test_thread_pools.cpp index 9a81f7c..f9294cc 100644 --- a/learning_concurrency/tests/test_thread_pools.cpp +++ b/learning_concurrency/tests/test_thread_pools.cpp @@ -2,18 +2,18 @@ // Estimated Time: 5 hours // Difficulty: Hard +#include "instrumentation.h" +#include +#include +#include +#include +#include #include -#include "instrumentation.h" +#include +#include #include #include -#include -#include -#include -#include -#include -#include -#include class ThreadPoolsTest : public ::testing::Test { @@ -31,13 +31,11 @@ class ThreadPoolsTest : public ::testing::Test class ThreadPool { public: - explicit ThreadPool(size_t num_threads) - : stop_(false) + explicit ThreadPool(size_t num_threads) : stop_(false) { for (size_t i = 0; i < num_threads; ++i) { - workers_.emplace_back([this, i]() - { + workers_.emplace_back([this, i]() { EventLog::instance().record("Worker " + std::to_string(i) + " started"); while (true) { @@ -62,16 +60,14 @@ class ThreadPool } catch (const std::exception& e) { - EventLog::instance().record("Worker " + std::to_string(i) + - " caught exception: " + e.what()); + EventLog::instance().record("Worker " + std::to_string(i) + " caught exception: " + e.what()); } } }); } } - template - void enqueue(F&& task) + template void enqueue(F&& task) { { std::lock_guard lock(mutex_); @@ -85,14 +81,13 @@ class ThreadPool cv_.notify_one(); } - template + template auto enqueue_future(F&& f, Args&&... args) -> std::future::type> { using return_type = typename std::invoke_result::type; auto task = std::make_shared>( - std::bind(std::forward(f), std::forward(args)...) - ); + std::bind(std::forward(f), std::forward(args)...)); std::future result = task->get_future(); @@ -159,8 +154,7 @@ TEST_F(ThreadPoolsTest, BasicThreadPoolExecution) for (int i = 0; i < num_tasks; ++i) { - pool.enqueue([&completed, i]() - { + pool.enqueue([&completed, i]() { EventLog::instance().record("Task " + std::to_string(i) + " executing"); completed.fetch_add(1); }); @@ -220,8 +214,7 @@ TEST_F(ThreadPoolsTest, ThreadPoolShutdown) for (int i = 0; i < 5; ++i) { - pool.enqueue([&completed, i]() - { + pool.enqueue([&completed, i]() { std::this_thread::sleep_for(std::chrono::milliseconds(20)); completed.fetch_add(1); EventLog::instance().record("Task " + std::to_string(i) + " completed"); @@ -231,10 +224,7 @@ TEST_F(ThreadPoolsTest, ThreadPoolShutdown) std::this_thread::sleep_for(std::chrono::milliseconds(10)); pool.shutdown(); - pool.enqueue([&completed]() - { - completed.fetch_add(1); - }); + pool.enqueue([&completed]() { completed.fetch_add(1); }); pool.wait(); @@ -262,21 +252,14 @@ TEST_F(ThreadPoolsTest, ThreadPoolExceptionHandling) ThreadPool pool(2); std::atomic completed{0}; - pool.enqueue([&completed]() - { + pool.enqueue([&completed]() { completed.fetch_add(1); throw std::runtime_error("Task 1 failed"); }); - pool.enqueue([&completed]() - { - completed.fetch_add(1); - }); + pool.enqueue([&completed]() { completed.fetch_add(1); }); - pool.enqueue([&completed]() - { - completed.fetch_add(1); - }); + pool.enqueue([&completed]() { completed.fetch_add(1); }); std::this_thread::sleep_for(std::chrono::milliseconds(50)); @@ -303,22 +286,19 @@ TEST_F(ThreadPoolsTest, ThreadPoolTaskDependencies) ThreadPool pool(2); std::atomic execution_order{0}; - auto task1 = pool.enqueue_future([&execution_order]() - { + auto task1 = pool.enqueue_future([&execution_order]() { std::this_thread::sleep_for(std::chrono::milliseconds(20)); execution_order.fetch_add(1); EventLog::instance().record("Task 1 completed"); }); - auto task2 = pool.enqueue_future([&execution_order, &task1]() - { + auto task2 = pool.enqueue_future([&execution_order, &task1]() { task1.wait(); execution_order.fetch_add(1); EventLog::instance().record("Task 2 completed (after Task 1)"); }); - auto task3 = pool.enqueue_future([&execution_order, &task1]() - { + auto task3 = pool.enqueue_future([&execution_order, &task1]() { task1.wait(); execution_order.fetch_add(1); EventLog::instance().record("Task 3 completed (after Task 1)"); @@ -359,8 +339,7 @@ class WorkStealingPool // TODO: Initialize per-worker queues and threads } - template - void enqueue(F&& task) + template void enqueue(F&& task) { // TODO: Add task to a worker's queue (round-robin or random) } @@ -381,8 +360,7 @@ TEST_F(ThreadPoolsTest, DISABLED_WorkStealingThreadPool) for (int i = 0; i < 100; ++i) { - pool.enqueue([&completed, i]() - { + pool.enqueue([&completed, i]() { if (i % 10 == 0) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); diff --git a/learning_concurrency/tests/test_thread_safe_singleton.cpp b/learning_concurrency/tests/test_thread_safe_singleton.cpp index 7b430a4..05d16be 100644 --- a/learning_concurrency/tests/test_thread_safe_singleton.cpp +++ b/learning_concurrency/tests/test_thread_safe_singleton.cpp @@ -2,14 +2,14 @@ // Estimated Time: 3 hours // Difficulty: Moderate +#include "instrumentation.h" +#include +#include #include -#include "instrumentation.h" -#include #include +#include #include -#include -#include class ThreadSafeSingletonTest : public ::testing::Test { @@ -61,10 +61,11 @@ TEST_F(ThreadSafeSingletonTest, MeyersSingletonThreadSafe) for (int i = 0; i < num_threads; ++i) { - threads.emplace_back([&ready_count]() - { + threads.emplace_back([&ready_count]() { ready_count.fetch_add(1); - while (ready_count.load() < num_threads) {} + while (ready_count.load() < num_threads) + { + } MeyersSingleton::instance().do_work(); }); } @@ -148,10 +149,7 @@ TEST_F(ThreadSafeSingletonTest, BrokenDoubleCheckedLocking) for (int i = 0; i < num_threads; ++i) { - threads.emplace_back([]() - { - BrokenDoubleCheckedSingleton::instance()->do_work(); - }); + threads.emplace_back([]() { BrokenDoubleCheckedSingleton::instance()->do_work(); }); } for (auto& t : threads) @@ -220,10 +218,11 @@ TEST_F(ThreadSafeSingletonTest, CorrectDoubleCheckedLocking) for (int i = 0; i < num_threads; ++i) { - threads.emplace_back([&ready_count]() - { + threads.emplace_back([&ready_count]() { ready_count.fetch_add(1); - while (ready_count.load() < num_threads) {} + while (ready_count.load() < num_threads) + { + } CorrectDoubleCheckedSingleton::instance()->do_work(); }); } @@ -256,10 +255,7 @@ class CallOnceSingleton static CallOnceSingleton& instance() { static CallOnceSingleton inst; - std::call_once(init_flag_, []() - { - EventLog::instance().record("CallOnceSingleton::init via call_once"); - }); + std::call_once(init_flag_, []() { EventLog::instance().record("CallOnceSingleton::init via call_once"); }); return inst; } @@ -302,10 +298,11 @@ TEST_F(ThreadSafeSingletonTest, CallOnceSingleton) for (int i = 0; i < num_threads; ++i) { - threads.emplace_back([&ready_count]() - { + threads.emplace_back([&ready_count]() { ready_count.fetch_add(1); - while (ready_count.load() < num_threads) {} + while (ready_count.load() < num_threads) + { + } CallOnceSingleton::instance().do_work(); }); } @@ -374,10 +371,11 @@ TEST_F(ThreadSafeSingletonTest, LazyInitializationRace) for (int i = 0; i < num_threads; ++i) { - threads.emplace_back([&ready_count]() - { + threads.emplace_back([&ready_count]() { ready_count.fetch_add(1); - while (ready_count.load() < num_threads) {} + while (ready_count.load() < num_threads) + { + } LazyInitRace::instance()->do_work(); }); } @@ -450,10 +448,11 @@ TEST_F(ThreadSafeSingletonTest, DISABLED_AtomicSingletonCorrectness) for (int i = 0; i < num_threads; ++i) { - threads.emplace_back([&ready_count]() - { + threads.emplace_back([&ready_count]() { ready_count.fetch_add(1); - while (ready_count.load() < num_threads) {} + while (ready_count.load() < num_threads) + { + } AtomicSingleton::instance()->do_work(); }); } diff --git a/learning_deadlocks/CMakeLists.txt b/learning_deadlocks/CMakeLists.txt index 1e5a466..44a8abb 100644 --- a/learning_deadlocks/CMakeLists.txt +++ b/learning_deadlocks/CMakeLists.txt @@ -1,4 +1,4 @@ -# add_learning_test(test_mutex_ordering_deadlocks tests/test_mutex_ordering_deadlocks.cpp instrumentation Threads::Threads) -# add_learning_test(test_circular_reference_deadlocks tests/test_circular_reference_deadlocks.cpp instrumentation Threads::Threads) -# add_learning_test(test_condition_variable_deadlocks tests/test_condition_variable_deadlocks.cpp instrumentation Threads::Threads) -# add_learning_test(test_ownership_transfer_deadlocks tests/test_ownership_transfer_deadlocks.cpp instrumentation Threads::Threads) +add_learning_test(test_mutex_ordering_deadlocks tests/test_mutex_ordering_deadlocks.cpp instrumentation Threads::Threads) +add_learning_test(test_circular_reference_deadlocks tests/test_circular_reference_deadlocks.cpp instrumentation Threads::Threads) +add_learning_test(test_condition_variable_deadlocks tests/test_condition_variable_deadlocks.cpp instrumentation Threads::Threads) +add_learning_test(test_ownership_transfer_deadlocks tests/test_ownership_transfer_deadlocks.cpp instrumentation Threads::Threads) diff --git a/learning_deadlocks/tests/test_circular_reference_deadlocks.cpp b/learning_deadlocks/tests/test_circular_reference_deadlocks.cpp index a828f81..f925d1c 100644 --- a/learning_deadlocks/tests/test_circular_reference_deadlocks.cpp +++ b/learning_deadlocks/tests/test_circular_reference_deadlocks.cpp @@ -1,10 +1,11 @@ #include "instrumentation.h" + +#include +#include #include #include -#include #include -#include -#include +#include // Test Suite 2: Circular Reference + Mutex Deadlocks // Focus: Combining shared_ptr cycles with mutex contention @@ -27,28 +28,27 @@ class Child; class Parent { public: - explicit Parent(const std::string& name) - : data_(std::make_shared(name)) + explicit Parent(const std::string& name) : data_(std::make_shared(name)) { } - + void set_child(std::shared_ptr child) { std::lock_guard lock(mutex_); child_ = child; } - + std::shared_ptr get_child() { std::lock_guard lock(mutex_); return child_; } - + std::mutex& get_mutex() { return mutex_; } - + private: std::shared_ptr data_; std::shared_ptr child_; @@ -58,28 +58,27 @@ class Parent class Child { public: - explicit Child(const std::string& name) - : data_(std::make_shared(name)) + explicit Child(const std::string& name) : data_(std::make_shared(name)) { } - + void set_parent(std::shared_ptr parent) { std::lock_guard lock(mutex_); parent_ = parent; } - + std::shared_ptr get_parent() { std::lock_guard lock(mutex_); return parent_; } - + std::mutex& get_mutex() { return mutex_; } - + private: std::shared_ptr data_; std::shared_ptr parent_; @@ -91,7 +90,7 @@ void setup_parent_child_deadlock(std::shared_ptr parent, std::shared_ptr { std::lock_guard lock1(parent->get_mutex()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); - + parent->set_child(child); } @@ -99,7 +98,7 @@ void setup_child_parent_deadlock(std::shared_ptr child, std::shared_ptr

lock1(child->get_mutex()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); - + child->set_parent(parent); } @@ -107,11 +106,10 @@ TEST_F(CircularReferenceDeadlocksTest, DISABLED_Scenario1_ParentChildCircularWit { auto parent = std::make_shared("Parent"); auto child = std::make_shared("Child"); - + std::atomic deadlock_detected(false); - - std::thread t1([&]() - { + + std::thread t1([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -120,9 +118,8 @@ TEST_F(CircularReferenceDeadlocksTest, DISABLED_Scenario1_ParentChildCircularWit } setup_parent_child_deadlock(parent, child); }); - - std::thread t2([&]() - { + + std::thread t2([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -131,17 +128,17 @@ TEST_F(CircularReferenceDeadlocksTest, DISABLED_Scenario1_ParentChildCircularWit } setup_child_parent_deadlock(child, parent); }); - + t1.join(); t2.join(); - + // Q: What are the TWO problems in this scenario (ownership + synchronization)? - // A: - // R: - + // A: + // R: + // Q: Does the circular shared_ptr reference cause the deadlock, or the mutex ordering? - // A: - // R: + // A: + // R: } // FIX VERSION: Break cycle with weak_ptr AND fix lock ordering @@ -159,13 +156,13 @@ TEST_F(CircularReferenceDeadlocksTest, Scenario1_ParentChildCircularWithMutex_Fi { auto parent = std::make_shared("Parent"); auto child = std::make_shared("Child"); - + std::thread t1([&]() { setup_parent_child_fixed(parent, child); }); std::thread t2([&]() { setup_child_parent_fixed(child, parent); }); - + t1.join(); t2.join(); - + EXPECT_TRUE(parent->get_child() != nullptr); // Note: If using weak_ptr in Child, check differently } @@ -177,40 +174,39 @@ TEST_F(CircularReferenceDeadlocksTest, Scenario1_ParentChildCircularWithMutex_Fi class Node { public: - explicit Node(const std::string& name) - : data_(std::make_shared(name)) + explicit Node(const std::string& name) : data_(std::make_shared(name)) { } - + void set_next(std::shared_ptr next) { std::lock_guard lock(mutex_); next_ = next; } - + void set_prev(std::shared_ptr prev) { std::lock_guard lock(mutex_); prev_ = prev; } - + std::shared_ptr get_next() { std::lock_guard lock(mutex_); return next_; } - + std::shared_ptr get_prev() { std::lock_guard lock(mutex_); return prev_; } - + std::mutex& get_mutex() { return mutex_; } - + private: std::shared_ptr data_; std::shared_ptr next_; @@ -225,7 +221,7 @@ void link_nodes_deadlock(std::shared_ptr n1, std::shared_ptr n2) std::lock_guard lock1(n1->get_mutex()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::lock_guard lock2(n2->get_mutex()); - + n1->set_next(n2); n2->set_prev(n1); } @@ -235,11 +231,10 @@ TEST_F(CircularReferenceDeadlocksTest, DISABLED_Scenario2_DoublyLinkedListDeadlo auto n1 = std::make_shared("N1"); auto n2 = std::make_shared("N2"); auto n3 = std::make_shared("N3"); - + std::atomic deadlock_count(0); - - std::thread t1([&]() - { + + std::thread t1([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -248,9 +243,8 @@ TEST_F(CircularReferenceDeadlocksTest, DISABLED_Scenario2_DoublyLinkedListDeadlo } link_nodes_deadlock(n1, n2); }); - - std::thread t2([&]() - { + + std::thread t2([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -259,9 +253,8 @@ TEST_F(CircularReferenceDeadlocksTest, DISABLED_Scenario2_DoublyLinkedListDeadlo } link_nodes_deadlock(n2, n3); }); - - std::thread t3([&]() - { + + std::thread t3([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -270,19 +263,19 @@ TEST_F(CircularReferenceDeadlocksTest, DISABLED_Scenario2_DoublyLinkedListDeadlo } link_nodes_deadlock(n3, n1); }); - + t1.join(); t2.join(); t3.join(); - + // Q: Why is a doubly-linked list particularly prone to deadlocks? - // A: - // R: - + // A: + // R: + // Q: What happens to the shared_ptr reference count when we create a cycle? - // A: - // R: - + // A: + // R: + EXPECT_GT(deadlock_count.load(), 0); } @@ -291,24 +284,24 @@ void link_nodes_fixed(std::shared_ptr n1, std::shared_ptr n2) { // TODO: Implement deadlock-free bidirectional linking // Hint: Use weak_ptr for prev pointers AND std::lock() for multiple mutexes - + // YOUR CODE HERE } -TEST_F(CircularReferenceDeadlocksTest, Scenario2_DoublyLinkedListDeadlock_Fixed) +TEST_F(CircularReferenceDeadlocksTest, DISABLED_Scenario2_DoublyLinkedListDeadlock_Fixed) { auto n1 = std::make_shared("N1"); auto n2 = std::make_shared("N2"); auto n3 = std::make_shared("N3"); - + std::thread t1([&]() { link_nodes_fixed(n1, n2); }); std::thread t2([&]() { link_nodes_fixed(n2, n3); }); std::thread t3([&]() { link_nodes_fixed(n3, n1); }); - + t1.join(); t2.join(); t3.join(); - + EXPECT_TRUE(n1->get_next() != nullptr); } @@ -321,24 +314,23 @@ class Observer; class Subject { public: - explicit Subject(const std::string& name) - : data_(std::make_shared(name)) + explicit Subject(const std::string& name) : data_(std::make_shared(name)) { } - + void attach(std::shared_ptr obs) { std::lock_guard lock(mutex_); observers_.push_back(obs); } - + void notify(); - + std::mutex& get_mutex() { return mutex_; } - + private: std::shared_ptr data_; std::vector> observers_; @@ -348,22 +340,21 @@ class Subject class Observer { public: - explicit Observer(const std::string& name) - : data_(std::make_shared(name)) + explicit Observer(const std::string& name) : data_(std::make_shared(name)) { } - + void update(std::shared_ptr subj) { std::lock_guard lock(mutex_); subject_ = subj; } - + std::mutex& get_mutex() { return mutex_; } - + private: std::shared_ptr data_; std::shared_ptr subject_; @@ -375,7 +366,7 @@ void Subject::notify() { std::lock_guard lock(mutex_); std::this_thread::sleep_for(std::chrono::milliseconds(10)); - + for (auto& obs : observers_) { obs->update(std::make_shared("temp")); @@ -387,14 +378,13 @@ TEST_F(CircularReferenceDeadlocksTest, DISABLED_Scenario3_ObserverPatternCircula auto subject = std::make_shared("Subject"); auto obs1 = std::make_shared("Obs1"); auto obs2 = std::make_shared("Obs2"); - + subject->attach(obs1); subject->attach(obs2); - + std::atomic deadlock_count(0); - - std::thread t1([&]() - { + + std::thread t1([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -403,9 +393,8 @@ TEST_F(CircularReferenceDeadlocksTest, DISABLED_Scenario3_ObserverPatternCircula } subject->notify(); }); - - std::thread t2([&]() - { + + std::thread t2([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -415,9 +404,8 @@ TEST_F(CircularReferenceDeadlocksTest, DISABLED_Scenario3_ObserverPatternCircula std::lock_guard lock(obs1->get_mutex()); std::this_thread::sleep_for(std::chrono::milliseconds(20)); }); - - std::thread t3([&]() - { + + std::thread t3([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -427,18 +415,18 @@ TEST_F(CircularReferenceDeadlocksTest, DISABLED_Scenario3_ObserverPatternCircula std::lock_guard lock(obs2->get_mutex()); std::this_thread::sleep_for(std::chrono::milliseconds(20)); }); - + t1.join(); t2.join(); t3.join(); - + // Q: Why is calling observer methods while holding subject lock dangerous? - // A: - // R: - + // A: + // R: + // Q: Should observers hold shared_ptr to subject? What's the alternative? - // A: - // R: + // A: + // R: } // FIX VERSION: Use weak_ptr for observer back-reference AND release lock before callback @@ -447,7 +435,7 @@ void notify_fixed(Subject& subject) { // TODO: Implement deadlock-free notification // Hint: Copy observers, release lock, then notify - + // YOUR CODE HERE } @@ -456,22 +444,20 @@ TEST_F(CircularReferenceDeadlocksTest, Scenario3_ObserverPatternCircular_Fixed) auto subject = std::make_shared("Subject"); auto obs1 = std::make_shared("Obs1"); auto obs2 = std::make_shared("Obs2"); - + subject->attach(obs1); subject->attach(obs2); - + std::thread t1([&]() { notify_fixed(*subject); }); - std::thread t2([&]() - { + std::thread t2([&]() { std::lock_guard lock(obs1->get_mutex()); std::this_thread::sleep_for(std::chrono::milliseconds(20)); }); - std::thread t3([&]() - { + std::thread t3([&]() { std::lock_guard lock(obs2->get_mutex()); std::this_thread::sleep_for(std::chrono::milliseconds(20)); }); - + t1.join(); t2.join(); t3.join(); @@ -484,28 +470,27 @@ TEST_F(CircularReferenceDeadlocksTest, Scenario3_ObserverPatternCircular_Fixed) class GraphNode { public: - explicit GraphNode(const std::string& name) - : data_(std::make_shared(name)) + explicit GraphNode(const std::string& name) : data_(std::make_shared(name)) { } - + void add_edge(std::shared_ptr neighbor) { std::lock_guard lock(mutex_); neighbors_.push_back(neighbor); } - + std::vector> get_neighbors() { std::lock_guard lock(mutex_); return neighbors_; } - + std::mutex& get_mutex() { return mutex_; } - + private: std::shared_ptr data_; std::vector> neighbors_; @@ -518,7 +503,7 @@ void create_bidirectional_edge_deadlock(std::shared_ptr n1, std::shar std::lock_guard lock1(n1->get_mutex()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::lock_guard lock2(n2->get_mutex()); - + n1->add_edge(n2); n2->add_edge(n1); } @@ -528,12 +513,11 @@ TEST_F(CircularReferenceDeadlocksTest, DISABLED_Scenario4_GraphCircularDependenc auto n1 = std::make_shared("N1"); auto n2 = std::make_shared("N2"); auto n3 = std::make_shared("N3"); - + std::atomic deadlock_count(0); - + // Create triangle: N1<->N2, N2<->N3, N3<->N1 - std::thread t1([&]() - { + std::thread t1([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -542,9 +526,8 @@ TEST_F(CircularReferenceDeadlocksTest, DISABLED_Scenario4_GraphCircularDependenc } create_bidirectional_edge_deadlock(n1, n2); }); - - std::thread t2([&]() - { + + std::thread t2([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -553,9 +536,8 @@ TEST_F(CircularReferenceDeadlocksTest, DISABLED_Scenario4_GraphCircularDependenc } create_bidirectional_edge_deadlock(n2, n3); }); - - std::thread t3([&]() - { + + std::thread t3([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -564,19 +546,19 @@ TEST_F(CircularReferenceDeadlocksTest, DISABLED_Scenario4_GraphCircularDependenc } create_bidirectional_edge_deadlock(n3, n1); }); - + t1.join(); t2.join(); t3.join(); - + // Q: In a graph structure, why can't we simply "always lock in order"? - // A: - // R: - + // A: + // R: + // Q: What's the memory leak risk with circular shared_ptr references in a graph? - // A: - // R: - + // A: + // R: + EXPECT_GT(deadlock_count.load(), 0); } @@ -585,23 +567,23 @@ void create_bidirectional_edge_fixed(std::shared_ptr n1, std::shared_ { // TODO: Implement deadlock-free bidirectional edge creation // Hint: Consider using weak_ptr for back-edges OR std::lock() for multiple mutexes - + // YOUR CODE HERE } -TEST_F(CircularReferenceDeadlocksTest, Scenario4_GraphCircularDependencies_Fixed) +TEST_F(CircularReferenceDeadlocksTest, DISABLED_Scenario4_GraphCircularDependencies_Fixed) { auto n1 = std::make_shared("N1"); auto n2 = std::make_shared("N2"); auto n3 = std::make_shared("N3"); - + std::thread t1([&]() { create_bidirectional_edge_fixed(n1, n2); }); std::thread t2([&]() { create_bidirectional_edge_fixed(n2, n3); }); std::thread t3([&]() { create_bidirectional_edge_fixed(n3, n1); }); - + t1.join(); t2.join(); t3.join(); - + EXPECT_TRUE(n1->get_neighbors().size() > 0); } diff --git a/learning_deadlocks/tests/test_condition_variable_deadlocks.cpp b/learning_deadlocks/tests/test_condition_variable_deadlocks.cpp index 59fb899..514cd20 100644 --- a/learning_deadlocks/tests/test_condition_variable_deadlocks.cpp +++ b/learning_deadlocks/tests/test_condition_variable_deadlocks.cpp @@ -1,12 +1,13 @@ #include "instrumentation.h" + +#include +#include +#include #include #include -#include #include -#include -#include -#include #include +#include // Test Suite 3: Condition Variable + shared_ptr Deadlocks // Focus: Synchronization primitives with ownership transfer @@ -27,56 +28,55 @@ class ConditionVariableDeadlocksTest : public ::testing::Test class Queue { public: - Queue() - : shutdown_(false) + Queue() : shutdown_(false) { } - + void push(std::shared_ptr item) { std::lock_guard lock(mutex_); queue_.push(item); cv_.notify_one(); } - + // DEADLOCK VERSION: Wrong wait condition std::shared_ptr pop_deadlock() { std::unique_lock lock(mutex_); - + // BUG: Waits forever if queue is empty and no more items coming while (queue_.empty()) { cv_.wait(lock); } - + auto item = queue_.front(); queue_.pop(); return item; } - + void shutdown() { std::lock_guard lock(mutex_); shutdown_ = true; cv_.notify_all(); } - + bool is_shutdown() const { return shutdown_; } - + std::mutex& get_mutex() { return mutex_; } - + std::condition_variable& get_cv() { return cv_; } - + private: std::queue> queue_; std::mutex mutex_; @@ -88,19 +88,17 @@ TEST_F(ConditionVariableDeadlocksTest, DISABLED_Scenario1_ProducerConsumerWrongW { Queue queue; std::atomic consumer_stuck(false); - + // Producer: pushes 2 items - std::thread producer([&]() - { + std::thread producer([&]() { queue.push(std::make_shared("Item1")); queue.push(std::make_shared("Item2")); std::this_thread::sleep_for(std::chrono::milliseconds(50)); queue.shutdown(); }); - + // Consumer: tries to pop 3 items (will block on 3rd) - std::thread consumer([&]() - { + std::thread consumer([&]() { for (int i = 0; i < 3; ++i) { std::timed_mutex timeout; @@ -109,7 +107,7 @@ TEST_F(ConditionVariableDeadlocksTest, DISABLED_Scenario1_ProducerConsumerWrongW consumer_stuck = true; return; } - + auto item = queue.pop_deadlock(); if (!item) { @@ -117,17 +115,17 @@ TEST_F(ConditionVariableDeadlocksTest, DISABLED_Scenario1_ProducerConsumerWrongW } } }); - + producer.join(); consumer.join(); - + // Q: Why does the consumer thread block forever waiting for the 3rd item? - // A: - // R: - + // A: + // R: + // Q: What should the wait condition check besides queue.empty()? - // A: - // R: + // A: + // R: } // FIX VERSION: Implement correct wait condition @@ -135,27 +133,25 @@ std::shared_ptr pop_fixed(Queue& queue) { // TODO: Implement pop with correct wait condition // Hint: Check both queue.empty() AND shutdown flag - + // YOUR CODE HERE - + return nullptr; } -TEST_F(ConditionVariableDeadlocksTest, Scenario1_ProducerConsumerWrongWait_Fixed) +TEST_F(ConditionVariableDeadlocksTest, DISABLED_Scenario1_ProducerConsumerWrongWait_Fixed) { Queue queue; std::atomic items_consumed(0); - - std::thread producer([&]() - { + + std::thread producer([&]() { queue.push(std::make_shared("Item1")); queue.push(std::make_shared("Item2")); std::this_thread::sleep_for(std::chrono::milliseconds(50)); queue.shutdown(); }); - - std::thread consumer([&]() - { + + std::thread consumer([&]() { while (true) { auto item = pop_fixed(queue); @@ -166,10 +162,10 @@ TEST_F(ConditionVariableDeadlocksTest, Scenario1_ProducerConsumerWrongWait_Fixed items_consumed++; } }); - + producer.join(); consumer.join(); - + EXPECT_EQ(items_consumed.load(), 2); } @@ -180,37 +176,36 @@ TEST_F(ConditionVariableDeadlocksTest, Scenario1_ProducerConsumerWrongWait_Fixed class Signal { public: - Signal() - : ready_(false) + Signal() : ready_(false) { } - + // DEADLOCK VERSION: Notify can happen before wait void notify_deadlock() { ready_ = true; cv_.notify_one(); } - + void wait_deadlock() { std::this_thread::sleep_for(std::chrono::milliseconds(20)); - + std::unique_lock lock(mutex_); // BUG: If notify happened before wait, we wait forever cv_.wait(lock); } - + std::mutex& get_mutex() { return mutex_; } - + bool is_ready() const { return ready_; } - + private: std::mutex mutex_; std::condition_variable cv_; @@ -221,16 +216,12 @@ TEST_F(ConditionVariableDeadlocksTest, DISABLED_Scenario2_NotifyBeforeWait_Broke { Signal signal; std::atomic waiter_stuck(false); - + // Thread 1: Notifies immediately - std::thread notifier([&]() - { - signal.notify_deadlock(); - }); - + std::thread notifier([&]() { signal.notify_deadlock(); }); + // Thread 2: Waits (but notify already happened) - std::thread waiter([&]() - { + std::thread waiter([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -239,17 +230,17 @@ TEST_F(ConditionVariableDeadlocksTest, DISABLED_Scenario2_NotifyBeforeWait_Broke } signal.wait_deadlock(); }); - + notifier.join(); waiter.join(); - + // Q: Why does the waiter miss the notification? - // A: - // R: - + // A: + // R: + // Q: What pattern prevents lost wakeups? - // A: - // R: + // A: + // R: } // FIX VERSION: Implement proper wait with predicate @@ -257,27 +248,21 @@ void wait_fixed(Signal& signal) { // TODO: Implement wait that doesn't miss notifications // Hint: Always check condition in a loop with predicate - + // YOUR CODE HERE } TEST_F(ConditionVariableDeadlocksTest, Scenario2_NotifyBeforeWait_Fixed) { Signal signal; - - std::thread notifier([&]() - { - signal.notify_deadlock(); - }); - - std::thread waiter([&]() - { - wait_fixed(signal); - }); - + + std::thread notifier([&]() { signal.notify_deadlock(); }); + + std::thread waiter([&]() { wait_fixed(signal); }); + notifier.join(); waiter.join(); - + EXPECT_TRUE(signal.is_ready()); } @@ -288,61 +273,59 @@ TEST_F(ConditionVariableDeadlocksTest, Scenario2_NotifyBeforeWait_Fixed) class TwoStageQueue { public: - TwoStageQueue() - : stage1_ready_(false) - , stage2_ready_(false) + TwoStageQueue() : stage1_ready_(false), stage2_ready_(false) { } - + // DEADLOCK VERSION: Holds lock while waiting on another CV void process_deadlock() { // Stage 1: Wait for data std::unique_lock lock1(mutex1_); cv1_.wait(lock1, [this]() { return stage1_ready_; }); - + // Stage 2: Hold lock1, try to signal stage2 std::lock_guard lock2(mutex2_); stage2_ready_ = true; cv2_.notify_one(); } - + void reverse_process_deadlock() { // Stage 2: Wait for stage2 std::unique_lock lock2(mutex2_); cv2_.wait(lock2, [this]() { return stage2_ready_; }); - + // Stage 1: Hold lock2, try to signal stage1 std::lock_guard lock1(mutex1_); stage1_ready_ = true; cv1_.notify_one(); } - + void trigger_stage1() { std::lock_guard lock(mutex1_); stage1_ready_ = true; cv1_.notify_one(); } - + void trigger_stage2() { std::lock_guard lock(mutex2_); stage2_ready_ = true; cv2_.notify_one(); } - + std::mutex& get_mutex1() { return mutex1_; } - + std::mutex& get_mutex2() { return mutex2_; } - + private: std::mutex mutex1_; std::mutex mutex2_; @@ -356,9 +339,8 @@ TEST_F(ConditionVariableDeadlocksTest, DISABLED_Scenario3_NestedConditionVariabl { TwoStageQueue queue; std::atomic deadlock_count(0); - - std::thread t1([&]() - { + + std::thread t1([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(200))) { @@ -367,9 +349,8 @@ TEST_F(ConditionVariableDeadlocksTest, DISABLED_Scenario3_NestedConditionVariabl } queue.process_deadlock(); }); - - std::thread t2([&]() - { + + std::thread t2([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(200))) { @@ -378,26 +359,25 @@ TEST_F(ConditionVariableDeadlocksTest, DISABLED_Scenario3_NestedConditionVariabl } queue.reverse_process_deadlock(); }); - - std::thread t3([&]() - { + + std::thread t3([&]() { std::this_thread::sleep_for(std::chrono::milliseconds(10)); queue.trigger_stage1(); queue.trigger_stage2(); }); - + t1.join(); t2.join(); t3.join(); - + // Q: Why does holding one lock while acquiring another cause deadlock here? - // A: - // R: - + // A: + // R: + // Q: What's the difference between this and simple mutex deadlock? - // A: - // R: - + // A: + // R: + EXPECT_GT(deadlock_count.load(), 0); } @@ -406,30 +386,29 @@ void process_fixed(TwoStageQueue& queue) { // TODO: Implement deadlock-free two-stage processing // Hint: Release lock1 before acquiring lock2 - + // YOUR CODE HERE } void reverse_process_fixed(TwoStageQueue& queue) { // TODO: Implement deadlock-free reverse processing - + // YOUR CODE HERE } TEST_F(ConditionVariableDeadlocksTest, Scenario3_NestedConditionVariables_Fixed) { TwoStageQueue queue; - + std::thread t1([&]() { process_fixed(queue); }); std::thread t2([&]() { reverse_process_fixed(queue); }); - std::thread t3([&]() - { + std::thread t3([&]() { std::this_thread::sleep_for(std::chrono::milliseconds(10)); queue.trigger_stage1(); queue.trigger_stage2(); }); - + t1.join(); t2.join(); t3.join(); @@ -442,12 +421,10 @@ TEST_F(ConditionVariableDeadlocksTest, Scenario3_NestedConditionVariables_Fixed) class WorkQueue { public: - WorkQueue() - : work_available_(false) - , shutdown_(false) + WorkQueue() : work_available_(false), shutdown_(false) { } - + void add_work(std::shared_ptr work) { std::lock_guard lock(mutex_); @@ -455,44 +432,44 @@ class WorkQueue work_available_ = true; cv_.notify_one(); } - + // DEADLOCK VERSION: Doesn't handle spurious wakeups correctly std::shared_ptr get_work_deadlock() { std::unique_lock lock(mutex_); - + // BUG: Single if instead of while - spurious wakeup causes early return if (!work_available_ && !shutdown_) { cv_.wait(lock); } - + if (shutdown_) { return nullptr; } - + work_available_ = false; return work_; } - + void shutdown() { std::lock_guard lock(mutex_); shutdown_ = true; cv_.notify_all(); } - + std::mutex& get_mutex() { return mutex_; } - + bool is_shutdown() const { return shutdown_; } - + private: std::mutex mutex_; std::condition_variable cv_; @@ -505,19 +482,17 @@ TEST_F(ConditionVariableDeadlocksTest, DISABLED_Scenario4_SpuriousWakeupHandling { WorkQueue queue; std::atomic null_work_count(0); - + // Producer: adds work slowly - std::thread producer([&]() - { + std::thread producer([&]() { std::this_thread::sleep_for(std::chrono::milliseconds(50)); queue.add_work(std::make_shared("Work1")); std::this_thread::sleep_for(std::chrono::milliseconds(50)); queue.shutdown(); }); - + // Consumers: may get spurious wakeups - auto consumer_func = [&]() - { + auto consumer_func = [&]() { for (int i = 0; i < 3; ++i) { auto work = queue.get_work_deadlock(); @@ -525,31 +500,31 @@ TEST_F(ConditionVariableDeadlocksTest, DISABLED_Scenario4_SpuriousWakeupHandling { null_work_count++; } - + if (queue.is_shutdown()) { break; } } }; - + std::thread c1(consumer_func); std::thread c2(consumer_func); std::thread c3(consumer_func); - + producer.join(); c1.join(); c2.join(); c3.join(); - + // Q: What is a spurious wakeup and why does it happen? - // A: - // R: - + // A: + // R: + // Q: Why must condition variable waits always use a while loop? - // A: - // R: - + // A: + // R: + // Spurious wakeups may cause unexpected null returns EXPECT_GT(null_work_count.load(), 1); } @@ -559,27 +534,25 @@ std::shared_ptr get_work_fixed(WorkQueue& queue) { // TODO: Implement get_work that handles spurious wakeups correctly // Hint: Use while loop and check condition after every wakeup - + // YOUR CODE HERE - + return nullptr; } -TEST_F(ConditionVariableDeadlocksTest, Scenario4_SpuriousWakeupHandling_Fixed) +TEST_F(ConditionVariableDeadlocksTest, DISABLED_Scenario4_SpuriousWakeupHandling_Fixed) { WorkQueue queue; std::atomic work_count(0); - - std::thread producer([&]() - { + + std::thread producer([&]() { std::this_thread::sleep_for(std::chrono::milliseconds(50)); queue.add_work(std::make_shared("Work1")); std::this_thread::sleep_for(std::chrono::milliseconds(50)); queue.shutdown(); }); - - auto consumer_func = [&]() - { + + auto consumer_func = [&]() { while (true) { auto work = get_work_fixed(queue); @@ -590,15 +563,15 @@ TEST_F(ConditionVariableDeadlocksTest, Scenario4_SpuriousWakeupHandling_Fixed) work_count++; } }; - + std::thread c1(consumer_func); std::thread c2(consumer_func); std::thread c3(consumer_func); - + producer.join(); c1.join(); c2.join(); c3.join(); - + EXPECT_EQ(work_count.load(), 1); } diff --git a/learning_deadlocks/tests/test_mutex_ordering_deadlocks.cpp b/learning_deadlocks/tests/test_mutex_ordering_deadlocks.cpp index a01ff04..8c47ae7 100644 --- a/learning_deadlocks/tests/test_mutex_ordering_deadlocks.cpp +++ b/learning_deadlocks/tests/test_mutex_ordering_deadlocks.cpp @@ -1,10 +1,11 @@ #include "instrumentation.h" + +#include +#include #include #include -#include #include -#include -#include +#include #include // Test Suite 1: Mutex Ordering Deadlocks @@ -26,32 +27,30 @@ class MutexOrderingDeadlocksTest : public ::testing::Test class Account { public: - explicit Account(const std::string& name, int balance) - : data_(std::make_shared(name)) - , balance_(balance) + explicit Account(const std::string& name, int balance) : data_(std::make_shared(name)), balance_(balance) { } - + std::mutex& get_mutex() { return mutex_; } - + int balance() const { return balance_; } - + void add(int amount) { balance_ += amount; } - + void subtract(int amount) { balance_ -= amount; } - + private: std::shared_ptr data_; std::mutex mutex_; @@ -67,7 +66,7 @@ void transfer_deadlock(Account& from, Account& to, int amount) std::lock_guard lock1(from.get_mutex()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::lock_guard lock2(to.get_mutex()); - + from.subtract(amount); to.add(amount); } @@ -76,12 +75,11 @@ TEST_F(MutexOrderingDeadlocksTest, DISABLED_Scenario1_ClassicTwoResourceDeadlock { Account acc1("Account1", 1000); Account acc2("Account2", 1000); - + std::atomic thread1_stuck(false); std::atomic thread2_stuck(false); - - std::thread t1([&]() - { + + std::thread t1([&]() { std::timed_mutex timeout_mutex; if (!timeout_mutex.try_lock_for(std::chrono::milliseconds(100))) { @@ -90,9 +88,8 @@ TEST_F(MutexOrderingDeadlocksTest, DISABLED_Scenario1_ClassicTwoResourceDeadlock } transfer_deadlock(acc1, acc2, 100); }); - - std::thread t2([&]() - { + + std::thread t2([&]() { std::timed_mutex timeout_mutex; if (!timeout_mutex.try_lock_for(std::chrono::milliseconds(100))) { @@ -101,17 +98,17 @@ TEST_F(MutexOrderingDeadlocksTest, DISABLED_Scenario1_ClassicTwoResourceDeadlock } transfer_deadlock(acc2, acc1, 50); }); - + t1.join(); t2.join(); - + // Q: Why does this deadlock occur even though each thread locks in "from->to" order? - // A: - // R: - + // A: + // R: + // Q: What is the lock acquisition sequence that causes deadlock? - // A: - // R: + // A: + // R: } // FIX VERSION: Implement correct lock ordering @@ -120,7 +117,7 @@ void transfer_fixed(Account& from, Account& to, int amount) std::lock(from.get_mutex(), to.get_mutex()); std::lock_guard lock1(from.get_mutex(), std::adopt_lock); std::lock_guard lock2(to.get_mutex(), std::adopt_lock); - + from.subtract(amount); to.add(amount); } @@ -129,13 +126,13 @@ TEST_F(MutexOrderingDeadlocksTest, Scenario1_ClassicTwoResourceDeadlock_Fixed) { Account acc1("Account1", 1000); Account acc2("Account2", 1000); - + std::thread t1([&]() { transfer_fixed(acc1, acc2, 100); }); std::thread t2([&]() { transfer_fixed(acc2, acc1, 50); }); - + t1.join(); t2.join(); - + EXPECT_EQ(acc1.balance(), 950); EXPECT_EQ(acc2.balance(), 1050); } @@ -147,21 +144,20 @@ TEST_F(MutexOrderingDeadlocksTest, Scenario1_ClassicTwoResourceDeadlock_Fixed) class Resource { public: - explicit Resource(const std::string& name) - : data_(std::make_shared(name)) + explicit Resource(const std::string& name) : data_(std::make_shared(name)) { } - + std::timed_mutex& get_mutex() { return mutex_; } - + std::shared_ptr get_data() { return data_; } - + private: std::shared_ptr data_; std::timed_mutex mutex_; @@ -175,19 +171,19 @@ bool process_two_resources_deadlock(Resource& r1, Resource& r2) return false; } std::lock_guard lock1(r1.get_mutex(), std::adopt_lock); - + std::this_thread::sleep_for(std::chrono::milliseconds(10)); - + if (!r2.get_mutex().try_lock_for(std::chrono::milliseconds(50))) { return false; } std::lock_guard lock2(r2.get_mutex(), std::adopt_lock); - + // Process resources std::shared_ptr d1 = r1.get_data(); std::shared_ptr d2 = r2.get_data(); - + return true; } @@ -196,48 +192,45 @@ TEST_F(MutexOrderingDeadlocksTest, DISABLED_Scenario2_ThreeThreadCircularDeadloc Resource r1("R1"); Resource r2("R2"); Resource r3("R3"); - + std::atomic deadlock_count(0); - + // Thread 1: R1 -> R2 - std::thread t1([&]() - { + std::thread t1([&]() { if (!process_two_resources_deadlock(r1, r2)) { deadlock_count++; } }); - + // Thread 2: R2 -> R3 - std::thread t2([&]() - { + std::thread t2([&]() { if (!process_two_resources_deadlock(r2, r3)) { deadlock_count++; } }); - + // Thread 3: R3 -> R1 - std::thread t3([&]() - { + std::thread t3([&]() { if (!process_two_resources_deadlock(r3, r1)) { deadlock_count++; } }); - + t1.join(); t2.join(); t3.join(); - + // Q: What circular wait condition exists in this scenario? - // A: - // R: - + // A: + // R: + // Q: Why does adding a third thread make deadlock more likely than with two threads? - // A: - // R: - + // A: + // R: + EXPECT_GT(deadlock_count.load(), 0); } @@ -246,27 +239,27 @@ bool process_two_resources_fixed(Resource& r1, Resource& r2) { Resource* first = &r1; Resource* second = &r2; - + if (first > second) { std::swap(first, second); } - + std::unique_lock lock1(first->get_mutex(), std::chrono::milliseconds(100)); if (!lock1.owns_lock()) { return false; } - + std::unique_lock lock2(second->get_mutex(), std::chrono::milliseconds(100)); if (!lock2.owns_lock()) { return false; } - + auto data1 = first->get_data(); auto data2 = second->get_data(); - + return true; } @@ -275,37 +268,34 @@ TEST_F(MutexOrderingDeadlocksTest, Scenario2_ThreeThreadCircularDeadlock_Fixed) Resource r1("R1"); Resource r2("R2"); Resource r3("R3"); - + std::atomic success_count(0); - - std::thread t1([&]() - { + + std::thread t1([&]() { if (process_two_resources_fixed(r1, r2)) { success_count++; } }); - - std::thread t2([&]() - { + + std::thread t2([&]() { if (process_two_resources_fixed(r2, r3)) { success_count++; } }); - - std::thread t3([&]() - { + + std::thread t3([&]() { if (process_two_resources_fixed(r3, r1)) { success_count++; } }); - + t1.join(); t2.join(); t3.join(); - + EXPECT_EQ(success_count.load(), 3); } @@ -316,28 +306,27 @@ TEST_F(MutexOrderingDeadlocksTest, Scenario2_ThreeThreadCircularDeadlock_Fixed) class Container { public: - explicit Container(const std::string& name) - : data_(std::make_shared(name)) + explicit Container(const std::string& name) : data_(std::make_shared(name)) { } - + void set_next(std::shared_ptr next) { std::lock_guard lock(mutex_); next_ = next; } - + std::shared_ptr get_next() { std::lock_guard lock(mutex_); return next_; } - + std::mutex& get_mutex() { return mutex_; } - + private: std::shared_ptr data_; std::shared_ptr next_; @@ -349,7 +338,7 @@ void link_containers_deadlock(std::shared_ptr c1, std::shared_ptr lock1(c1->get_mutex()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); - + // Acquiring c2's lock while holding c1's lock c1->set_next(c2); } @@ -358,11 +347,10 @@ TEST_F(MutexOrderingDeadlocksTest, DISABLED_Scenario3_NestedLockAcquisition_Brok { auto c1 = std::make_shared("C1"); auto c2 = std::make_shared("C2"); - + std::atomic deadlock_detected(false); - - std::thread t1([&]() - { + + std::thread t1([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -371,9 +359,8 @@ TEST_F(MutexOrderingDeadlocksTest, DISABLED_Scenario3_NestedLockAcquisition_Brok } link_containers_deadlock(c1, c2); }); - - std::thread t2([&]() - { + + std::thread t2([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -382,17 +369,17 @@ TEST_F(MutexOrderingDeadlocksTest, DISABLED_Scenario3_NestedLockAcquisition_Brok } link_containers_deadlock(c2, c1); }); - + t1.join(); t2.join(); - + // Q: How does nested locking (lock within lock) create deadlock risk? - // A: - // R: - + // A: + // R: + // Q: What role does shared_ptr play in this deadlock scenario? - // A: - // R: + // A: + // R: } // FIX VERSION: Implement safe nested locking @@ -402,7 +389,7 @@ void link_containers_fixed(std::shared_ptr c1, std::shared_ptrset_next(c2); } @@ -410,13 +397,13 @@ TEST_F(MutexOrderingDeadlocksTest, Scenario3_NestedLockAcquisition_Fixed) { auto c1 = std::make_shared("C1"); auto c2 = std::make_shared("C2"); - + std::thread t1([&]() { link_containers_fixed(c1, c2); }); std::thread t2([&]() { link_containers_fixed(c2, c1); }); - + t1.join(); t2.join(); - + EXPECT_TRUE(c1->get_next() != nullptr || c2->get_next() != nullptr); } @@ -427,28 +414,26 @@ TEST_F(MutexOrderingDeadlocksTest, Scenario3_NestedLockAcquisition_Fixed) class HierarchicalResource { public: - HierarchicalResource(const std::string& name, int level) - : data_(std::make_shared(name)) - , level_(level) + HierarchicalResource(const std::string& name, int level) : data_(std::make_shared(name)), level_(level) { } - + int level() const { return level_; } - + std::timed_mutex& get_mutex() { return mutex_; } - + void use_with(HierarchicalResource& other) { // Simulated work std::this_thread::sleep_for(std::chrono::milliseconds(5)); } - + private: std::shared_ptr data_; int level_; @@ -464,15 +449,15 @@ bool acquire_hierarchical_deadlock(HierarchicalResource& r1, HierarchicalResourc return false; } std::lock_guard lock1(r1.get_mutex(), std::adopt_lock); - + std::this_thread::sleep_for(std::chrono::milliseconds(10)); - + if (!r2.get_mutex().try_lock_for(std::chrono::milliseconds(50))) { return false; } std::lock_guard lock2(r2.get_mutex(), std::adopt_lock); - + r1.use_with(r2); return true; } @@ -481,48 +466,45 @@ TEST_F(MutexOrderingDeadlocksTest, DISABLED_Scenario4_LockHierarchyViolation_Bro { HierarchicalResource high("High", 10); HierarchicalResource low("Low", 1); - + std::atomic failure_count(0); - + // Thread 1: high -> low (correct hierarchy) - std::thread t1([&]() - { + std::thread t1([&]() { if (!acquire_hierarchical_deadlock(high, low)) { failure_count++; } }); - + // Thread 2: low -> high (violates hierarchy) - std::thread t2([&]() - { + std::thread t2([&]() { if (!acquire_hierarchical_deadlock(low, high)) { failure_count++; } }); - + // Thread 3: high -> low (correct hierarchy) - std::thread t3([&]() - { + std::thread t3([&]() { if (!acquire_hierarchical_deadlock(high, low)) { failure_count++; } }); - + t1.join(); t2.join(); t3.join(); - + // Q: What is a lock hierarchy and why does it prevent deadlocks? - // A: - // R: - + // A: + // R: + // Q: Which thread(s) violate the hierarchy in this scenario? - // A: - // R: - + // A: + // R: + EXPECT_GT(failure_count.load(), 0); } @@ -531,24 +513,24 @@ bool acquire_hierarchical_fixed(HierarchicalResource& r1, HierarchicalResource& { HierarchicalResource* first = &r1; HierarchicalResource* second = &r2; - + if (r1.level() < r2.level()) { std::swap(first, second); } - + if (!first->get_mutex().try_lock_for(std::chrono::milliseconds(50))) { return false; } std::lock_guard lock1(first->get_mutex(), std::adopt_lock); - + if (!second->get_mutex().try_lock_for(std::chrono::milliseconds(50))) { return false; } std::lock_guard lock2(second->get_mutex(), std::adopt_lock); - + r1.use_with(r2); return true; } @@ -557,36 +539,33 @@ TEST_F(MutexOrderingDeadlocksTest, Scenario4_LockHierarchyViolation_Fixed) { HierarchicalResource high("High", 10); HierarchicalResource low("Low", 1); - + std::atomic success_count(0); - - std::thread t1([&]() - { + + std::thread t1([&]() { if (acquire_hierarchical_fixed(high, low)) { success_count++; } }); - - std::thread t2([&]() - { + + std::thread t2([&]() { if (acquire_hierarchical_fixed(low, high)) { success_count++; } }); - - std::thread t3([&]() - { + + std::thread t3([&]() { if (acquire_hierarchical_fixed(high, low)) { success_count++; } }); - + t1.join(); t2.join(); t3.join(); - + EXPECT_EQ(success_count.load(), 3); } diff --git a/learning_deadlocks/tests/test_ownership_transfer_deadlocks.cpp b/learning_deadlocks/tests/test_ownership_transfer_deadlocks.cpp index f09547a..2a7983e 100644 --- a/learning_deadlocks/tests/test_ownership_transfer_deadlocks.cpp +++ b/learning_deadlocks/tests/test_ownership_transfer_deadlocks.cpp @@ -1,11 +1,12 @@ #include "instrumentation.h" + +#include +#include +#include #include #include -#include #include -#include -#include -#include +#include // Test Suite 4: Ownership Transfer + Lock Inversion Deadlocks // Focus: shared_ptr transfer between threads with synchronization @@ -26,22 +27,21 @@ class OwnershipTransferDeadlocksTest : public ::testing::Test class Mailbox { public: - explicit Mailbox(const std::string& name) - : name_(name) + explicit Mailbox(const std::string& name) : name_(name) { } - + void send(std::shared_ptr msg, Mailbox& recipient) { // Lock sender first std::lock_guard lock1(mutex_); std::this_thread::sleep_for(std::chrono::milliseconds(10)); - + // Then lock recipient std::lock_guard lock2(recipient.mutex_); recipient.messages_.push_back(msg); } - + std::shared_ptr receive() { std::lock_guard lock(mutex_); @@ -53,12 +53,12 @@ class Mailbox messages_.pop_back(); return msg; } - + std::mutex& get_mutex() { return mutex_; } - + private: std::string name_; std::vector> messages_; @@ -70,12 +70,11 @@ TEST_F(OwnershipTransferDeadlocksTest, DISABLED_Scenario1_LockInversionDuringTra Mailbox mb1("MB1"); Mailbox mb2("MB2"); Mailbox mb3("MB3"); - + std::atomic deadlock_count(0); - + // Thread 1: MB1 -> MB2 - std::thread t1([&]() - { + std::thread t1([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -84,10 +83,9 @@ TEST_F(OwnershipTransferDeadlocksTest, DISABLED_Scenario1_LockInversionDuringTra } mb1.send(std::make_shared("Msg1"), mb2); }); - + // Thread 2: MB2 -> MB3 - std::thread t2([&]() - { + std::thread t2([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -96,10 +94,9 @@ TEST_F(OwnershipTransferDeadlocksTest, DISABLED_Scenario1_LockInversionDuringTra } mb2.send(std::make_shared("Msg2"), mb3); }); - + // Thread 3: MB3 -> MB1 - std::thread t3([&]() - { + std::thread t3([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -108,19 +105,19 @@ TEST_F(OwnershipTransferDeadlocksTest, DISABLED_Scenario1_LockInversionDuringTra } mb3.send(std::make_shared("Msg3"), mb1); }); - + t1.join(); t2.join(); t3.join(); - + // Q: How does ownership transfer (shared_ptr) interact with lock ordering? - // A: - // R: - + // A: + // R: + // Q: What makes this different from simple data transfer without shared_ptr? - // A: - // R: - + // A: + // R: + EXPECT_GT(deadlock_count.load(), 0); } @@ -129,35 +126,26 @@ void send_fixed(Mailbox& sender, std::shared_ptr msg, Mailbox& recipien { // TODO: Implement deadlock-free send operation // Hint: Order locks by address or use std::lock() - + // YOUR CODE HERE } -TEST_F(OwnershipTransferDeadlocksTest, Scenario1_LockInversionDuringTransfer_Fixed) +TEST_F(OwnershipTransferDeadlocksTest, DISABLED_Scenario1_LockInversionDuringTransfer_Fixed) { Mailbox mb1("MB1"); Mailbox mb2("MB2"); Mailbox mb3("MB3"); - - std::thread t1([&]() - { - send_fixed(mb1, std::make_shared("Msg1"), mb2); - }); - - std::thread t2([&]() - { - send_fixed(mb2, std::make_shared("Msg2"), mb3); - }); - - std::thread t3([&]() - { - send_fixed(mb3, std::make_shared("Msg3"), mb1); - }); - + + std::thread t1([&]() { send_fixed(mb1, std::make_shared("Msg1"), mb2); }); + + std::thread t2([&]() { send_fixed(mb2, std::make_shared("Msg2"), mb3); }); + + std::thread t3([&]() { send_fixed(mb3, std::make_shared("Msg3"), mb1); }); + t1.join(); t2.join(); t3.join(); - + EXPECT_TRUE(mb1.receive() != nullptr || mb2.receive() != nullptr || mb3.receive() != nullptr); } @@ -168,34 +156,33 @@ TEST_F(OwnershipTransferDeadlocksTest, Scenario1_LockInversionDuringTransfer_Fix class EventHandler { public: - explicit EventHandler(const std::string& name) - : data_(std::make_shared(name)) + explicit EventHandler(const std::string& name) : data_(std::make_shared(name)) { } - + void set_callback(std::function)> cb) { std::lock_guard lock(mutex_); callback_ = cb; } - + // DEADLOCK VERSION: Calls callback while holding lock void trigger_deadlock(std::shared_ptr event) { std::lock_guard lock(mutex_); std::this_thread::sleep_for(std::chrono::milliseconds(10)); - + if (callback_) { callback_(event); } } - + std::mutex& get_mutex() { return mutex_; } - + private: std::shared_ptr data_; std::function)> callback_; @@ -206,23 +193,16 @@ TEST_F(OwnershipTransferDeadlocksTest, DISABLED_Scenario2_CallbackWithLockHeld_B { EventHandler h1("H1"); EventHandler h2("H2"); - + // H1's callback triggers H2 - h1.set_callback([&](std::shared_ptr event) - { - h2.trigger_deadlock(event); - }); - + h1.set_callback([&](std::shared_ptr event) { h2.trigger_deadlock(event); }); + // H2's callback triggers H1 - h2.set_callback([&](std::shared_ptr event) - { - h1.trigger_deadlock(event); - }); - + h2.set_callback([&](std::shared_ptr event) { h1.trigger_deadlock(event); }); + std::atomic deadlock_count(0); - - std::thread t1([&]() - { + + std::thread t1([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -231,9 +211,8 @@ TEST_F(OwnershipTransferDeadlocksTest, DISABLED_Scenario2_CallbackWithLockHeld_B } h1.trigger_deadlock(std::make_shared("Event1")); }); - - std::thread t2([&]() - { + + std::thread t2([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -242,9 +221,8 @@ TEST_F(OwnershipTransferDeadlocksTest, DISABLED_Scenario2_CallbackWithLockHeld_B } h2.trigger_deadlock(std::make_shared("Event2")); }); - - std::thread t3([&]() - { + + std::thread t3([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -253,19 +231,19 @@ TEST_F(OwnershipTransferDeadlocksTest, DISABLED_Scenario2_CallbackWithLockHeld_B } h1.trigger_deadlock(std::make_shared("Event3")); }); - + t1.join(); t2.join(); t3.join(); - + // Q: Why is calling unknown code (callbacks) while holding a lock dangerous? - // A: - // R: - + // A: + // R: + // Q: How does shared_ptr ownership transfer in callbacks affect deadlock risk? - // A: - // R: - + // A: + // R: + EXPECT_GT(deadlock_count.load(), 0); } @@ -274,7 +252,7 @@ void trigger_fixed(EventHandler& handler, std::shared_ptr event) { // TODO: Implement deadlock-free callback triggering // Hint: Copy callback, release lock, then invoke - + // YOUR CODE HERE } @@ -282,32 +260,17 @@ TEST_F(OwnershipTransferDeadlocksTest, Scenario2_CallbackWithLockHeld_Fixed) { EventHandler h1("H1"); EventHandler h2("H2"); - - h1.set_callback([&](std::shared_ptr event) - { - trigger_fixed(h2, event); - }); - - h2.set_callback([&](std::shared_ptr event) - { - trigger_fixed(h1, event); - }); - - std::thread t1([&]() - { - trigger_fixed(h1, std::make_shared("Event1")); - }); - - std::thread t2([&]() - { - trigger_fixed(h2, std::make_shared("Event2")); - }); - - std::thread t3([&]() - { - trigger_fixed(h1, std::make_shared("Event3")); - }); - + + h1.set_callback([&](std::shared_ptr event) { trigger_fixed(h2, event); }); + + h2.set_callback([&](std::shared_ptr event) { trigger_fixed(h1, event); }); + + std::thread t1([&]() { trigger_fixed(h1, std::make_shared("Event1")); }); + + std::thread t2([&]() { trigger_fixed(h2, std::make_shared("Event2")); }); + + std::thread t3([&]() { trigger_fixed(h1, std::make_shared("Event3")); }); + t1.join(); t2.join(); t3.join(); @@ -320,28 +283,27 @@ TEST_F(OwnershipTransferDeadlocksTest, Scenario2_CallbackWithLockHeld_Fixed) class SharedState { public: - explicit SharedState(const std::string& name) - : data_(std::make_shared(name)) + explicit SharedState(const std::string& name) : data_(std::make_shared(name)) { } - + std::shared_ptr get_data() { std::lock_guard lock(mutex_); return data_; } - + void set_data(std::shared_ptr new_data) { std::lock_guard lock(mutex_); data_ = new_data; } - + std::mutex& get_mutex() { return mutex_; } - + private: std::shared_ptr data_; std::mutex mutex_; @@ -353,7 +315,7 @@ void swap_state_deadlock(SharedState& s1, SharedState& s2) std::lock_guard lock1(s1.get_mutex()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::lock_guard lock2(s2.get_mutex()); - + auto temp = s1.get_data(); s1.set_data(s2.get_data()); s2.set_data(temp); @@ -364,11 +326,10 @@ TEST_F(OwnershipTransferDeadlocksTest, DISABLED_Scenario3_SharedStateSwap_Broken SharedState s1("State1"); SharedState s2("State2"); SharedState s3("State3"); - + std::atomic deadlock_count(0); - - std::thread t1([&]() - { + + std::thread t1([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -377,9 +338,8 @@ TEST_F(OwnershipTransferDeadlocksTest, DISABLED_Scenario3_SharedStateSwap_Broken } swap_state_deadlock(s1, s2); }); - - std::thread t2([&]() - { + + std::thread t2([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -388,9 +348,8 @@ TEST_F(OwnershipTransferDeadlocksTest, DISABLED_Scenario3_SharedStateSwap_Broken } swap_state_deadlock(s2, s3); }); - - std::thread t3([&]() - { + + std::thread t3([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(100))) { @@ -399,19 +358,19 @@ TEST_F(OwnershipTransferDeadlocksTest, DISABLED_Scenario3_SharedStateSwap_Broken } swap_state_deadlock(s3, s1); }); - + t1.join(); t2.join(); t3.join(); - + // Q: Why is swapping shared_ptr ownership particularly deadlock-prone? - // A: - // R: - + // A: + // R: + // Q: What happens to reference counts during a swap operation? - // A: - // R: - + // A: + // R: + EXPECT_GT(deadlock_count.load(), 0); } @@ -420,7 +379,7 @@ void swap_state_fixed(SharedState& s1, SharedState& s2) { // TODO: Implement deadlock-free state swap // Hint: Use std::lock() or consistent lock ordering - + // YOUR CODE HERE } @@ -429,11 +388,11 @@ TEST_F(OwnershipTransferDeadlocksTest, Scenario3_SharedStateSwap_Fixed) SharedState s1("State1"); SharedState s2("State2"); SharedState s3("State3"); - + std::thread t1([&]() { swap_state_fixed(s1, s2); }); std::thread t2([&]() { swap_state_fixed(s2, s3); }); std::thread t3([&]() { swap_state_fixed(s3, s1); }); - + t1.join(); t2.join(); t3.join(); @@ -449,7 +408,7 @@ class LazyResource LazyResource() { } - + // DEADLOCK VERSION: Double-checked locking done wrong std::shared_ptr get_deadlock(LazyResource& other) { @@ -457,7 +416,7 @@ class LazyResource { std::lock_guard lock(mutex_); std::this_thread::sleep_for(std::chrono::milliseconds(10)); - + if (!resource_) { // Initialization depends on other resource (creates lock ordering issue) @@ -467,17 +426,17 @@ class LazyResource } return resource_; } - + std::mutex& get_mutex() { return mutex_; } - + std::shared_ptr get_resource() const { return resource_; } - + private: std::shared_ptr resource_; std::mutex mutex_; @@ -488,11 +447,10 @@ TEST_F(OwnershipTransferDeadlocksTest, DISABLED_Scenario4_LazyInitializationRace LazyResource r1; LazyResource r2; LazyResource r3; - + std::atomic deadlock_count(0); - - std::thread t1([&]() - { + + std::thread t1([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(200))) { @@ -501,9 +459,8 @@ TEST_F(OwnershipTransferDeadlocksTest, DISABLED_Scenario4_LazyInitializationRace } r1.get_deadlock(r2); }); - - std::thread t2([&]() - { + + std::thread t2([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(200))) { @@ -512,9 +469,8 @@ TEST_F(OwnershipTransferDeadlocksTest, DISABLED_Scenario4_LazyInitializationRace } r2.get_deadlock(r3); }); - - std::thread t3([&]() - { + + std::thread t3([&]() { std::timed_mutex timeout; if (!timeout.try_lock_for(std::chrono::milliseconds(200))) { @@ -523,19 +479,19 @@ TEST_F(OwnershipTransferDeadlocksTest, DISABLED_Scenario4_LazyInitializationRace } r3.get_deadlock(r1); }); - + t1.join(); t2.join(); t3.join(); - + // Q: Why is lazy initialization with dependencies particularly deadlock-prone? - // A: - // R: - + // A: + // R: + // Q: How does shared_ptr's atomic reference counting help or hurt here? - // A: - // R: - + // A: + // R: + EXPECT_GT(deadlock_count.load(), 0); } @@ -544,25 +500,25 @@ std::shared_ptr get_fixed(LazyResource& resource) { // TODO: Implement deadlock-free lazy initialization // Hint: Either remove circular dependency or initialize in predetermined order - + // YOUR CODE HERE - + return nullptr; } -TEST_F(OwnershipTransferDeadlocksTest, Scenario4_LazyInitializationRace_Fixed) +TEST_F(OwnershipTransferDeadlocksTest, DISABLED_Scenario4_LazyInitializationRace_Fixed) { LazyResource r1; LazyResource r2; LazyResource r3; - + std::thread t1([&]() { get_fixed(r1); }); std::thread t2([&]() { get_fixed(r2); }); std::thread t3([&]() { get_fixed(r3); }); - + t1.join(); t2.join(); t3.join(); - + EXPECT_TRUE(r1.get_resource() != nullptr); } diff --git a/learning_debugging/tests/test_benchmark.cpp b/learning_debugging/tests/test_benchmark.cpp index 67bc171..f348bcc 100644 --- a/learning_debugging/tests/test_benchmark.cpp +++ b/learning_debugging/tests/test_benchmark.cpp @@ -2,14 +2,14 @@ // Estimated Time: 3 hours // Difficulty: Easy - -#include #include "instrumentation.h" -#include -#include + #include +#include +#include #include #include +#include class BenchmarkTest : public ::testing::Test { @@ -156,25 +156,25 @@ TEST_F(BenchmarkTest, CacheEffectsOnPerformance) class LargeObject { public: - explicit LargeObject(size_t size) - : data_(size, 0) + explicit LargeObject(size_t size) : data_(size, 0) { EventLog::instance().record("LargeObject::ctor size=" + std::to_string(size)); } - LargeObject(const LargeObject& other) - : data_(other.data_) + LargeObject(const LargeObject& other) : data_(other.data_) { EventLog::instance().record("LargeObject::copy_ctor size=" + std::to_string(data_.size())); } - LargeObject(LargeObject&& other) noexcept - : data_(std::move(other.data_)) + LargeObject(LargeObject&& other) noexcept : data_(std::move(other.data_)) { EventLog::instance().record("LargeObject::move_ctor"); } - size_t size() const { return data_.size(); } + size_t size() const + { + return data_.size(); + } private: std::vector data_; @@ -237,7 +237,7 @@ TEST_F(BenchmarkTest, MoveVsCopyPerformance) // R: EXPECT_EQ(EventLog::instance().count_events("move_ctor"), iterations * 2); - + if (copy_duration > 0 && move_duration > 0) { double ratio = static_cast(copy_duration) / move_duration; @@ -258,8 +258,7 @@ TEST_F(BenchmarkTest, MoveVsCopyPerformance) class Microbenchmark { public: - template - void run(const std::string& name, Func func, int warmup_iters, int bench_iters) + template void run(const std::string& name, Func func, int warmup_iters, int bench_iters) { // TODO: Run warmup iterations // TODO: Collect timing data for bench_iters @@ -272,14 +271,16 @@ TEST_F(BenchmarkTest, DISABLED_MicrobenchmarkWithWarmup) { Microbenchmark bench; - bench.run("vector_push_back", []() - { - std::vector v; - for (int i = 0; i < 1000; ++i) - { - v.push_back(i); - } - }, 10, 100); + bench.run( + "vector_push_back", + []() { + std::vector v; + for (int i = 0; i < 1000; ++i) + { + v.push_back(i); + } + }, + 10, 100); // Q: Why are warmup iterations necessary? What CPU/cache effects do they mitigate? // A: @@ -301,8 +302,8 @@ TEST_F(BenchmarkTest, MemoryAllocationPerformance) delete ptr; } auto end_individual = std::chrono::high_resolution_clock::now(); - auto individual_duration = std::chrono::duration_cast( - end_individual - start_individual).count(); + auto individual_duration = + std::chrono::duration_cast(end_individual - start_individual).count(); auto start_batch = std::chrono::high_resolution_clock::now(); int* batch = new int[iterations]; @@ -312,8 +313,7 @@ TEST_F(BenchmarkTest, MemoryAllocationPerformance) } delete[] batch; auto end_batch = std::chrono::high_resolution_clock::now(); - auto batch_duration = std::chrono::duration_cast( - end_batch - start_batch).count(); + auto batch_duration = std::chrono::duration_cast(end_batch - start_batch).count(); EventLog::instance().record("Individual allocations: " + std::to_string(individual_duration) + " us"); EventLog::instance().record("Batch allocation: " + std::to_string(batch_duration) + " us"); diff --git a/learning_debugging/tests/test_debugging_techniques.cpp b/learning_debugging/tests/test_debugging_techniques.cpp index caa97ce..74de294 100644 --- a/learning_debugging/tests/test_debugging_techniques.cpp +++ b/learning_debugging/tests/test_debugging_techniques.cpp @@ -2,14 +2,14 @@ // Estimated Time: 4 hours // Difficulty: Moderate +#include "instrumentation.h" +#include #include -#include "instrumentation.h" #include +#include #include #include -#include -#include class DebuggingTechniquesTest : public ::testing::Test { @@ -36,22 +36,26 @@ class DebugCounter { ++value_; ++operation_count_; - EventLog::instance().record("DebugCounter::increment() value=" + - std::to_string(value_) + - " ops=" + std::to_string(operation_count_)); + EventLog::instance().record("DebugCounter::increment() value=" + std::to_string(value_) + + " ops=" + std::to_string(operation_count_)); } void decrement() { --value_; ++operation_count_; - EventLog::instance().record("DebugCounter::decrement() value=" + - std::to_string(value_) + - " ops=" + std::to_string(operation_count_)); + EventLog::instance().record("DebugCounter::decrement() value=" + std::to_string(value_) + + " ops=" + std::to_string(operation_count_)); } - int value() const { return value_; } - int operation_count() const { return operation_count_; } + int value() const + { + return value_; + } + int operation_count() const + { + return operation_count_; + } private: int value_; @@ -90,11 +94,9 @@ TEST_F(DebuggingTechniquesTest, ObservableStateDebugging) class BoundedBuffer { public: - explicit BoundedBuffer(size_t capacity) - : capacity_(capacity) + explicit BoundedBuffer(size_t capacity) : capacity_(capacity) { - EventLog::instance().record("BoundedBuffer::ctor capacity=" + - std::to_string(capacity)); + EventLog::instance().record("BoundedBuffer::ctor capacity=" + std::to_string(capacity)); } void push(int value) @@ -118,7 +120,10 @@ class BoundedBuffer return value; } - size_t size() const { return buffer_.size(); } + size_t size() const + { + return buffer_.size(); + } private: std::vector buffer_; @@ -155,9 +160,7 @@ TEST_F(DebuggingTechniquesTest, AssertionVsException) class DebugResource { public: - explicit DebugResource(int id) - : id_(id) - , valid_(true) + explicit DebugResource(int id) : id_(id), valid_(true) { EventLog::instance().record("DebugResource::ctor id=" + std::to_string(id)); } @@ -165,13 +168,10 @@ class DebugResource DebugResource(const DebugResource&) = delete; DebugResource& operator=(const DebugResource&) = delete; - DebugResource(DebugResource&& other) noexcept - : id_(other.id_) - , valid_(other.valid_) + DebugResource(DebugResource&& other) noexcept : id_(other.id_), valid_(other.valid_) { other.valid_ = false; - EventLog::instance().record("DebugResource::move_ctor id=" + std::to_string(id_) + - " (source now invalid)"); + EventLog::instance().record("DebugResource::move_ctor id=" + std::to_string(id_) + " (source now invalid)"); } DebugResource& operator=(DebugResource&& other) noexcept @@ -180,32 +180,34 @@ class DebugResource { if (valid_) { - EventLog::instance().record("DebugResource::move_assign releasing id=" + - std::to_string(id_)); + EventLog::instance().record("DebugResource::move_assign releasing id=" + std::to_string(id_)); } id_ = other.id_; valid_ = other.valid_; other.valid_ = false; - EventLog::instance().record("DebugResource::move_assign acquired id=" + - std::to_string(id_)); + EventLog::instance().record("DebugResource::move_assign acquired id=" + std::to_string(id_)); } return *this; } - int id() const { return id_; } - bool is_valid() const { return valid_; } + int id() const + { + return id_; + } + bool is_valid() const + { + return valid_; + } ~DebugResource() { if (valid_) { - EventLog::instance().record("DebugResource::dtor releasing id=" + - std::to_string(id_)); + EventLog::instance().record("DebugResource::dtor releasing id=" + std::to_string(id_)); } else { - EventLog::instance().record("DebugResource::dtor moved-from id=" + - std::to_string(id_)); + EventLog::instance().record("DebugResource::dtor moved-from id=" + std::to_string(id_)); } } @@ -248,8 +250,7 @@ TEST_F(DebuggingTechniquesTest, DebuggingMoveSemantics) class Owner { public: - explicit Owner(const std::string& name) - : name_(name) + explicit Owner(const std::string& name) : name_(name) { EventLog::instance().record("Owner::ctor name=" + name); } @@ -260,7 +261,10 @@ class Owner EventLog::instance().record("Owner(" + name_ + ")::set_child()"); } - const std::string& name() const { return name_; } + const std::string& name() const + { + return name_; + } ~Owner() { @@ -314,9 +318,9 @@ TEST_F(DebuggingTechniquesTest, DebuggingLifetimeIssues) // TODO: 3. Can be disabled at compile time for performance #ifdef NDEBUG - #define DEBUG_LOG(msg) ((void)0) +#define DEBUG_LOG(msg) ((void)0) #else - #define DEBUG_LOG(msg) EventLog::instance().record("DEBUG: " + std::string(msg)) +#define DEBUG_LOG(msg) EventLog::instance().record("DEBUG: " + std::string(msg)) #endif class DebugLogger @@ -338,7 +342,10 @@ class DebugLogger result_ = value * 2; } - int result() const { return result_; } + int result() const + { + return result_; + } private: int result_ = 0; @@ -367,8 +374,7 @@ TEST_F(DebuggingTechniquesTest, DISABLED_ConditionalDebugLogging) class Container { public: - explicit Container(const std::string& name) - : name_(name) + explicit Container(const std::string& name) : name_(name) { EventLog::instance().record("Container::ctor name=" + name); } diff --git a/learning_debugging/tests/test_googlemock.cpp b/learning_debugging/tests/test_googlemock.cpp index ea16116..a2a4b73 100644 --- a/learning_debugging/tests/test_googlemock.cpp +++ b/learning_debugging/tests/test_googlemock.cpp @@ -2,18 +2,18 @@ // Estimated Time: 4 hours // Difficulty: Moderate +#include "instrumentation.h" -#include #include -#include "instrumentation.h" +#include #include #include #include -using ::testing::Return; using ::testing::_; using ::testing::AtLeast; using ::testing::Invoke; +using ::testing::Return; using ::testing::SaveArg; class GoogleMockTest : public ::testing::Test @@ -49,8 +49,7 @@ class MockDatabase : public Database class UserService { public: - explicit UserService(std::shared_ptr db) - : db_(std::move(db)) + explicit UserService(std::shared_ptr db) : db_(std::move(db)) { EventLog::instance().record("UserService::ctor"); } @@ -74,11 +73,9 @@ TEST_F(GoogleMockTest, BasicMockExpectations) { auto mock_db = std::make_shared(); - EXPECT_CALL(*mock_db, connect(_)) - .WillOnce(Return(true)); + EXPECT_CALL(*mock_db, connect(_)).WillOnce(Return(true)); - EXPECT_CALL(*mock_db, query(_)) - .WillOnce(Return(42)); + EXPECT_CALL(*mock_db, query(_)).WillOnce(Return(42)); UserService service(mock_db); @@ -121,8 +118,7 @@ class MockEmailService : public EmailService class NotificationManager { public: - explicit NotificationManager(std::shared_ptr email) - : email_(std::move(email)) + explicit NotificationManager(std::shared_ptr email) : email_(std::move(email)) { } @@ -140,9 +136,8 @@ TEST_F(GoogleMockTest, ArgumentMatchers) { auto mock_email = std::make_shared(); - EXPECT_CALL(*mock_email, send(::testing::StartsWith("user@"), - ::testing::Eq("Notification"), - ::testing::HasSubstr("alert"))) + EXPECT_CALL(*mock_email, + send(::testing::StartsWith("user@"), ::testing::Eq("Notification"), ::testing::HasSubstr("alert"))) .WillOnce(Return(true)); NotificationManager manager(mock_email); @@ -185,8 +180,7 @@ class MockFileSystem : public FileSystem class FileReader { public: - explicit FileReader(std::shared_ptr fs) - : fs_(std::move(fs)) + explicit FileReader(std::shared_ptr fs) : fs_(std::move(fs)) { } @@ -216,18 +210,14 @@ TEST_F(GoogleMockTest, MockCallSequences) { ::testing::InSequence seq; - EXPECT_CALL(*mock_fs, open("test.txt")) - .WillOnce(Return(true)); + EXPECT_CALL(*mock_fs, open("test.txt")).WillOnce(Return(true)); EXPECT_CALL(*mock_fs, read(_, 100)) - .WillOnce(::testing::DoAll( - ::testing::Invoke([](char* buf, int size) - { - std::string data = "hello"; - std::copy(data.begin(), data.end(), buf); - }), - Return(5) - )); + .WillOnce(::testing::DoAll(::testing::Invoke([](char* buf, int size) { + std::string data = "hello"; + std::copy(data.begin(), data.end(), buf); + }), + Return(5))); EXPECT_CALL(*mock_fs, close()); } @@ -274,8 +264,7 @@ TEST_F(GoogleMockTest, MockSideEffects) auto mock_queue = std::make_shared(); std::string captured_message; - EXPECT_CALL(*mock_queue, publish("events", _)) - .WillOnce(::testing::SaveArg<1>(&captured_message)); + EXPECT_CALL(*mock_queue, publish("events", _)).WillOnce(::testing::SaveArg<1>(&captured_message)); mock_queue->publish("events", "test message"); @@ -366,8 +355,7 @@ class MockLogger : public Logger class Application { public: - explicit Application(std::shared_ptr logger) - : logger_(std::move(logger)) + explicit Application(std::shared_ptr logger) : logger_(std::move(logger)) { } @@ -384,8 +372,7 @@ TEST_F(GoogleMockTest, MockOwnershipLifetime) { auto mock_logger = std::make_shared(); - EXPECT_CALL(*mock_logger, log(_)) - .Times(AtLeast(1)); + EXPECT_CALL(*mock_logger, log(_)).Times(AtLeast(1)); { Application app(mock_logger); diff --git a/learning_design_patterns/tests/test_behavioral_patterns.cpp b/learning_design_patterns/tests/test_behavioral_patterns.cpp index b7e28d3..fdb4e49 100644 --- a/learning_design_patterns/tests/test_behavioral_patterns.cpp +++ b/learning_design_patterns/tests/test_behavioral_patterns.cpp @@ -2,14 +2,14 @@ // Estimated Time: 7 hours // Difficulty: Moderate +#include "instrumentation.h" +#include +#include #include -#include "instrumentation.h" #include #include #include -#include -#include class BehavioralPatternsTest : public ::testing::Test { @@ -58,11 +58,9 @@ class Subject void notify(const std::string& message) { - observers_.erase( - std::remove_if(observers_.begin(), observers_.end(), - [](const std::weak_ptr& wp) { return wp.expired(); }), - observers_.end() - ); + observers_.erase(std::remove_if(observers_.begin(), observers_.end(), + [](const std::weak_ptr& wp) { return wp.expired(); }), + observers_.end()); for (auto& weak_obs : observers_) { @@ -138,8 +136,7 @@ TEST_F(BehavioralPatternsTest, StrategyPattern) Context context; std::vector data = {3, 1, 4, 1, 5}; - context.set_strategy([](std::vector& d) - { + context.set_strategy([](std::vector& d) { std::sort(d.begin(), d.end()); EventLog::instance().record("Strategy: sort ascending"); }); @@ -171,7 +168,9 @@ class Command class Receiver { public: - Receiver() : value_(0) {} + Receiver() : value_(0) + { + } void add(int amount) { @@ -185,7 +184,10 @@ class Receiver EventLog::instance().record("Receiver::subtract(" + std::to_string(amount) + ")"); } - int value() const { return value_; } + int value() const + { + return value_; + } private: int value_; @@ -194,14 +196,18 @@ class Receiver class AddCommand : public Command { public: - AddCommand(Receiver& receiver, int amount) - : receiver_(receiver) - , amount_(amount) + AddCommand(Receiver& receiver, int amount) : receiver_(receiver), amount_(amount) { } - void execute() override { receiver_.add(amount_); } - void undo() override { receiver_.subtract(amount_); } + void execute() override + { + receiver_.add(amount_); + } + void undo() override + { + receiver_.subtract(amount_); + } private: Receiver& receiver_; @@ -407,7 +413,9 @@ class ClosedState : public State class Connection { public: - Connection() : state_(std::make_unique()) {} + Connection() : state_(std::make_unique()) + { + } void connect() { diff --git a/learning_design_patterns/tests/test_creational_patterns.cpp b/learning_design_patterns/tests/test_creational_patterns.cpp index 9a52615..3a5ad70 100644 --- a/learning_design_patterns/tests/test_creational_patterns.cpp +++ b/learning_design_patterns/tests/test_creational_patterns.cpp @@ -2,15 +2,15 @@ // Estimated Time: 5 hours // Difficulty: Moderate - // LEAVE THIS FILE ALONE. It makes sense that it is 500+ lines due to the 5 patterns being tested -#include #include "instrumentation.h" + +#include +#include +#include #include #include -#include -#include class CreationalPatternsTest : public ::testing::Test { @@ -162,8 +162,7 @@ class Pizza class PizzaBuilder { public: - PizzaBuilder() - : pizza_(std::make_unique()) + PizzaBuilder() : pizza_(std::make_unique()) { } @@ -197,10 +196,7 @@ class PizzaBuilder TEST_F(CreationalPatternsTest, BuilderPatternFluentInterface) { PizzaBuilder builder; - auto pizza = builder.dough("thin") - .sauce("tomato") - .topping("pepperoni") - .build(); + auto pizza = builder.dough("thin").sauce("tomato").topping("pepperoni").build(); EXPECT_EQ(pizza->describe(), "Pizza with thin, tomato, pepperoni"); @@ -226,9 +222,7 @@ TEST_F(CreationalPatternsTest, BuilderPatternFluentInterface) class PooledObject { public: - explicit PooledObject(int id) - : id_(id) - , in_use_(false) + explicit PooledObject(int id) : id_(id), in_use_(false) { EventLog::instance().record("PooledObject::ctor id=" + std::to_string(id)); } @@ -245,8 +239,14 @@ class PooledObject EventLog::instance().record("PooledObject::reset() id=" + std::to_string(id_)); } - bool in_use() const { return in_use_; } - int id() const { return id_; } + bool in_use() const + { + return in_use_; + } + int id() const + { + return id_; + } ~PooledObject() { @@ -276,10 +276,7 @@ class ObjectPool if (!obj->in_use()) { obj->use(); - return std::shared_ptr(obj.get(), [](PooledObject* p) - { - p->reset(); - }); + return std::shared_ptr(obj.get(), [](PooledObject* p) { p->reset(); }); } } return nullptr; @@ -348,8 +345,7 @@ class Prototype class ConcretePrototype : public Prototype { public: - explicit ConcretePrototype(const std::string& data) - : data_(data) + explicit ConcretePrototype(const std::string& data) : data_(data) { // TODO: Add EventLog recording } diff --git a/learning_design_patterns/tests/test_modern_cpp_patterns.cpp b/learning_design_patterns/tests/test_modern_cpp_patterns.cpp index de471d6..a0a2533 100644 --- a/learning_design_patterns/tests/test_modern_cpp_patterns.cpp +++ b/learning_design_patterns/tests/test_modern_cpp_patterns.cpp @@ -2,16 +2,16 @@ // Estimated Time: 5 hours // Difficulty: Moderate - -#include #include "instrumentation.h" #include "move_instrumentation.h" -#include -#include -#include + #include +#include +#include #include +#include #include +#include class ModernCppPatternsTest : public ::testing::Test { @@ -29,9 +29,7 @@ class ModernCppPatternsTest : public ::testing::Test class ScopeGuard { public: - explicit ScopeGuard(std::function cleanup) - : cleanup_(std::move(cleanup)) - , active_(true) + explicit ScopeGuard(std::function cleanup) : cleanup_(std::move(cleanup)), active_(true) { EventLog::instance().record("ScopeGuard::ctor"); } @@ -64,8 +62,7 @@ TEST_F(ModernCppPatternsTest, ScopeGuardPattern) int resource_state = 0; { - ScopeGuard guard([&resource_state]() - { + ScopeGuard guard([&resource_state]() { resource_state = 0; EventLog::instance().record("Cleanup executed"); }); @@ -75,10 +72,7 @@ TEST_F(ModernCppPatternsTest, ScopeGuardPattern) EXPECT_EQ(resource_state, 0); { - ScopeGuard guard([&resource_state]() - { - resource_state = 0; - }); + ScopeGuard guard([&resource_state]() { resource_state = 0; }); resource_state = 2; guard.dismiss(); } @@ -97,41 +91,55 @@ TEST_F(ModernCppPatternsTest, ScopeGuardPattern) // TEST 2: Type-Safe Visitor with std::variant - Moderate // ============================================================================ -struct Add { int value; }; -struct Multiply { int factor; }; -struct Reset {}; +struct Add +{ + int value; +}; +struct Multiply +{ + int factor; +}; +struct Reset +{ +}; using Operation = std::variant; class Calculator { public: - Calculator() : value_(0) {} + Calculator() : value_(0) + { + } void execute(const Operation& op) { - std::visit([this](auto&& arg) - { - using T = std::decay_t; - if constexpr (std::is_same_v) - { - value_ += arg.value; - EventLog::instance().record("Calculator: Add " + std::to_string(arg.value)); - } - else if constexpr (std::is_same_v) - { - value_ *= arg.factor; - EventLog::instance().record("Calculator: Multiply by " + std::to_string(arg.factor)); - } - else if constexpr (std::is_same_v) - { - value_ = 0; - EventLog::instance().record("Calculator: Reset"); - } - }, op); + std::visit( + [this](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) + { + value_ += arg.value; + EventLog::instance().record("Calculator: Add " + std::to_string(arg.value)); + } + else if constexpr (std::is_same_v) + { + value_ *= arg.factor; + EventLog::instance().record("Calculator: Multiply by " + std::to_string(arg.factor)); + } + else if constexpr (std::is_same_v) + { + value_ = 0; + EventLog::instance().record("Calculator: Reset"); + } + }, + op); } - int value() const { return value_; } + int value() const + { + return value_; + } private: int value_; @@ -166,13 +174,21 @@ TEST_F(ModernCppPatternsTest, VariantVisitorPattern) // TEST 3: Policy-Based Design with Templates - Hard // ============================================================================ -template -class Container +template class Container { public: - void add(int value) { storage_.add(value); } - int get(size_t index) const { return storage_.get(index); } - size_t size() const { return storage_.size(); } + void add(int value) + { + storage_.add(value); + } + int get(size_t index) const + { + return storage_.get(index); + } + size_t size() const + { + return storage_.size(); + } private: StoragePolicy storage_; @@ -186,8 +202,14 @@ class VectorStorage data_.push_back(value); EventLog::instance().record("VectorStorage::add()"); } - int get(size_t index) const { return data_[index]; } - size_t size() const { return data_.size(); } + int get(size_t index) const + { + return data_[index]; + } + size_t size() const + { + return data_.size(); + } private: std::vector data_; @@ -196,7 +218,9 @@ class VectorStorage class ArrayStorage { public: - ArrayStorage() : size_(0) {} + ArrayStorage() : size_(0) + { + } void add(int value) { @@ -206,8 +230,14 @@ class ArrayStorage EventLog::instance().record("ArrayStorage::add()"); } } - int get(size_t index) const { return data_[index]; } - size_t size() const { return size_; } + int get(size_t index) const + { + return data_[index]; + } + size_t size() const + { + return size_; + } private: int data_[10]; @@ -248,8 +278,7 @@ class Logger public: using LogFunction = std::function; - explicit Logger(LogFunction log_func) - : log_func_(std::move(log_func)) + explicit Logger(LogFunction log_func) : log_func_(std::move(log_func)) { EventLog::instance().record("Logger::ctor"); } @@ -269,8 +298,7 @@ class Logger class Service { public: - explicit Service(std::unique_ptr logger) - : logger_(std::move(logger)) + explicit Service(std::unique_ptr logger) : logger_(std::move(logger)) { } @@ -287,8 +315,7 @@ TEST_F(ModernCppPatternsTest, DependencyInjection) { std::vector log_output; - auto logger = std::make_unique([&log_output](const std::string& msg) - { + auto logger = std::make_unique([&log_output](const std::string& msg) { log_output.push_back(msg); EventLog::instance().record("Lambda logger: " + msg); }); @@ -325,8 +352,7 @@ class Base class DerivedA : public Base { public: - explicit DerivedA(int value) - : value_(value) + explicit DerivedA(int value) : value_(value) { EventLog::instance().record("DerivedA::ctor value=" + std::to_string(value)); } @@ -343,9 +369,7 @@ class DerivedA : public Base class DerivedB : public Base { public: - DerivedB(std::string name, int count) - : name_(std::move(name)) - , count_(count) + DerivedB(std::string name, int count) : name_(std::move(name)), count_(count) { EventLog::instance().record("DerivedB::ctor name=" + name_); } @@ -360,8 +384,7 @@ class DerivedB : public Base int count_; }; -template -std::unique_ptr make_derived(Args&&... args) +template std::unique_ptr make_derived(Args&&... args) { EventLog::instance().record("make_derived() called"); return std::make_unique(std::forward(args)...); diff --git a/learning_design_patterns/tests/test_structural_patterns.cpp b/learning_design_patterns/tests/test_structural_patterns.cpp index c903319..589b8a1 100644 --- a/learning_design_patterns/tests/test_structural_patterns.cpp +++ b/learning_design_patterns/tests/test_structural_patterns.cpp @@ -2,16 +2,16 @@ // Estimated Time: 6 hours // Difficulty: Moderate +#include "instrumentation.h" +#include #include -#include "instrumentation.h" -#include -#include -#include #include +#include #include +#include #include -#include +#include class StructuralPatternsTest : public ::testing::Test { @@ -29,11 +29,7 @@ class StructuralPatternsTest : public ::testing::Test class LegacyRectangle { public: - LegacyRectangle(int x, int y, int w, int h) - : x_(x) - , y_(y) - , w_(w) - , h_(h) + LegacyRectangle(int x, int y, int w, int h) : x_(x), y_(y), w_(w), h_(h) { EventLog::instance().record("LegacyRectangle::ctor"); } @@ -43,10 +39,22 @@ class LegacyRectangle EventLog::instance().record("LegacyRectangle::legacy_draw()"); } - int get_x() const { return x_; } - int get_y() const { return y_; } - int get_w() const { return w_; } - int get_h() const { return h_; } + int get_x() const + { + return x_; + } + int get_y() const + { + return y_; + } + int get_w() const + { + return w_; + } + int get_h() const + { + return h_; + } private: int x_, y_, w_, h_; @@ -63,8 +71,7 @@ class Shape class RectangleAdapter : public Shape { public: - explicit RectangleAdapter(std::unique_ptr rect) - : rect_(std::move(rect)) + explicit RectangleAdapter(std::unique_ptr rect) : rect_(std::move(rect)) { EventLog::instance().record("RectangleAdapter::ctor"); } @@ -143,8 +150,7 @@ class ConcreteComponent : public Component class Decorator : public Component { public: - explicit Decorator(std::unique_ptr component) - : component_(std::move(component)) + explicit Decorator(std::unique_ptr component) : component_(std::move(component)) { } @@ -160,8 +166,7 @@ class Decorator : public Component class ConcreteDecoratorA : public Decorator { public: - explicit ConcreteDecoratorA(std::unique_ptr component) - : Decorator(std::move(component)) + explicit ConcreteDecoratorA(std::unique_ptr component) : Decorator(std::move(component)) { EventLog::instance().record("ConcreteDecoratorA::ctor"); } @@ -175,8 +180,7 @@ class ConcreteDecoratorA : public Decorator class ConcreteDecoratorB : public Decorator { public: - explicit ConcreteDecoratorB(std::unique_ptr component) - : Decorator(std::move(component)) + explicit ConcreteDecoratorB(std::unique_ptr component) : Decorator(std::move(component)) { EventLog::instance().record("ConcreteDecoratorB::ctor"); } @@ -248,8 +252,7 @@ class RealSubject : public Subject class LazyProxy : public Subject { public: - LazyProxy() - : real_subject_(nullptr) + LazyProxy() : real_subject_(nullptr) { EventLog::instance().record("LazyProxy::ctor (cheap)"); } @@ -306,16 +309,14 @@ TEST_F(StructuralPatternsTest, ProxyPatternLazyInit) class Flyweight { public: - explicit Flyweight(const std::string& shared_state) - : shared_state_(shared_state) + explicit Flyweight(const std::string& shared_state) : shared_state_(shared_state) { EventLog::instance().record("Flyweight::ctor shared_state=" + shared_state); } void operation(const std::string& unique_state) const { - EventLog::instance().record("Flyweight::operation() shared=" + shared_state_ + - " unique=" + unique_state); + EventLog::instance().record("Flyweight::operation() shared=" + shared_state_ + " unique=" + unique_state); } ~Flyweight() @@ -409,9 +410,7 @@ class FileSystemComponent class File : public FileSystemComponent { public: - explicit File(const std::string& name, int size) - : name_(name) - , size_(size) + explicit File(const std::string& name, int size) : name_(name), size_(size) { // TODO: Add EventLog recording } @@ -430,8 +429,7 @@ class File : public FileSystemComponent class Directory : public FileSystemComponent { public: - explicit Directory(const std::string& name) - : name_(name) + explicit Directory(const std::string& name) : name_(name) { // TODO: Add EventLog recording } diff --git a/learning_error_handling/tests/test_error_codes_vs_exceptions.cpp b/learning_error_handling/tests/test_error_codes_vs_exceptions.cpp index 692c3d2..484ac87 100644 --- a/learning_error_handling/tests/test_error_codes_vs_exceptions.cpp +++ b/learning_error_handling/tests/test_error_codes_vs_exceptions.cpp @@ -2,14 +2,14 @@ // Estimated Time: 2 hours // Difficulty: Easy - #include "instrumentation.h" -#include -#include -#include + #include #include +#include +#include #include +#include class ErrorCodesVsExceptionsTest : public ::testing::Test { @@ -43,17 +43,16 @@ struct FileResult FileResult read_file_error_code(const std::string& path) { EventLog::instance().record("read_file_error_code: called"); - + std::ifstream file(path); if (!file.is_open()) { EventLog::instance().record("read_file_error_code: file not found"); return {"", FileError::NotFound}; } - - std::string content((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - + + std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + EventLog::instance().record("read_file_error_code: success"); return {content, FileError::Success}; } @@ -69,9 +68,9 @@ FileResult read_file_error_code(const std::string& path) TEST_F(ErrorCodesVsExceptionsTest, ErrorCodePattern_BasicUsage) { // Easy: Error codes require explicit checking - + FileResult result = read_file_error_code("/nonexistent/file.txt"); - + EXPECT_EQ(result.error, FileError::NotFound); EXPECT_EQ(result.content, ""); EXPECT_EQ(EventLog::instance().count_events("read_file_error_code: called"), 1); @@ -85,17 +84,16 @@ TEST_F(ErrorCodesVsExceptionsTest, ErrorCodePattern_BasicUsage) std::string read_file_exception(const std::string& path) { EventLog::instance().record("read_file_exception: called"); - + std::ifstream file(path); if (!file.is_open()) { EventLog::instance().record("read_file_exception: throwing exception"); throw std::runtime_error("File not found: " + path); } - - std::string content((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - + + std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + EventLog::instance().record("read_file_exception: success"); return content; } @@ -111,7 +109,7 @@ std::string read_file_exception(const std::string& path) TEST_F(ErrorCodesVsExceptionsTest, ExceptionPattern_BasicUsage) { // Easy: Exceptions automatically propagate up the stack - + try { std::string content = read_file_exception("/nonexistent/file.txt"); @@ -121,7 +119,7 @@ TEST_F(ErrorCodesVsExceptionsTest, ExceptionPattern_BasicUsage) { EXPECT_NE(std::string(e.what()).find("File not found"), std::string::npos); } - + EXPECT_EQ(EventLog::instance().count_events("read_file_exception: called"), 1); EXPECT_EQ(EventLog::instance().count_events("read_file_exception: throwing exception"), 1); EXPECT_EQ(EventLog::instance().count_events("read_file_exception: success"), 0); @@ -135,14 +133,14 @@ TEST_F(ErrorCodesVsExceptionsTest, ExceptionPattern_BasicUsage) FileResult process_with_error_codes(const std::string& path) { EventLog::instance().record("process_with_error_codes: start"); - + FileResult result = read_file_error_code(path); if (result.error != FileError::Success) { EventLog::instance().record("process_with_error_codes: propagating error"); return result; } - + EventLog::instance().record("process_with_error_codes: processing content"); // TODO: Add processing logic here return result; @@ -152,10 +150,10 @@ FileResult process_with_error_codes(const std::string& path) std::string process_with_exceptions(const std::string& path) { EventLog::instance().record("process_with_exceptions: start"); - + // TODO: Call read_file_exception and let exceptions propagate std::string content = read_file_exception(path); - + EventLog::instance().record("process_with_exceptions: processing content"); return content; } @@ -171,9 +169,9 @@ std::string process_with_exceptions(const std::string& path) TEST_F(ErrorCodesVsExceptionsTest, ErrorPropagation_ErrorCodes) { // Moderate: Error codes require explicit propagation at each level - + FileResult result = process_with_error_codes("/nonexistent/file.txt"); - + EXPECT_EQ(result.error, FileError::NotFound); EXPECT_EQ(EventLog::instance().count_events("process_with_error_codes: start"), 1); EXPECT_EQ(EventLog::instance().count_events("process_with_error_codes: propagating error"), 1); @@ -183,7 +181,7 @@ TEST_F(ErrorCodesVsExceptionsTest, ErrorPropagation_ErrorCodes) TEST_F(ErrorCodesVsExceptionsTest, ErrorPropagation_Exceptions) { // Moderate: Exceptions propagate automatically through call stack - + try { std::string content = process_with_exceptions("/nonexistent/file.txt"); @@ -193,7 +191,7 @@ TEST_F(ErrorCodesVsExceptionsTest, ErrorPropagation_Exceptions) { EXPECT_NE(std::string(e.what()).find("File not found"), std::string::npos); } - + EXPECT_EQ(EventLog::instance().count_events("process_with_exceptions: start"), 1); EXPECT_EQ(EventLog::instance().count_events("process_with_exceptions: processing content"), 0); } @@ -205,20 +203,18 @@ TEST_F(ErrorCodesVsExceptionsTest, ErrorPropagation_Exceptions) class ResourceWithErrorCodes { public: - explicit ResourceWithErrorCodes(const std::string& name) - : name_(name) - , acquired_(false) + explicit ResourceWithErrorCodes(const std::string& name) : name_(name), acquired_(false) { EventLog::instance().record("Resource(" + name_ + ")::ctor"); } - + FileError acquire() { EventLog::instance().record("Resource(" + name_ + ")::acquire"); acquired_ = true; return FileError::Success; } - + void release() { if (acquired_) @@ -227,7 +223,7 @@ class ResourceWithErrorCodes acquired_ = false; } } - + ~ResourceWithErrorCodes() { EventLog::instance().record("Resource(" + name_ + ")::dtor"); @@ -245,22 +241,22 @@ class ResourceWithErrorCodes FileResult operation_with_error_codes_manual_cleanup() { EventLog::instance().record("operation: start"); - + ResourceWithErrorCodes r1("R1"); FileError err = r1.acquire(); if (err != FileError::Success) { return {"", err}; } - + ResourceWithErrorCodes r2("R2"); err = r2.acquire(); if (err != FileError::Success) { - r1.release(); // Must manually clean up R1 + r1.release(); // Must manually clean up R1 return {"", err}; } - + // Simulate error after acquiring both resources EventLog::instance().record("operation: simulating error"); r2.release(); @@ -279,13 +275,12 @@ FileResult operation_with_error_codes_manual_cleanup() class ResourceWithExceptions { public: - explicit ResourceWithExceptions(const std::string& name) - : name_(name) + explicit ResourceWithExceptions(const std::string& name) : name_(name) { EventLog::instance().record("Resource(" + name_ + ")::ctor"); EventLog::instance().record("Resource(" + name_ + ")::acquire"); } - + ~ResourceWithExceptions() { EventLog::instance().record("Resource(" + name_ + ")::release"); @@ -299,10 +294,10 @@ class ResourceWithExceptions std::string operation_with_exceptions_automatic_cleanup() { EventLog::instance().record("operation: start"); - + ResourceWithExceptions r1("R1"); ResourceWithExceptions r2("R2"); - + // Simulate error after acquiring both resources EventLog::instance().record("operation: throwing exception"); throw std::runtime_error("Simulated error"); @@ -315,15 +310,15 @@ std::string operation_with_exceptions_automatic_cleanup() TEST_F(ErrorCodesVsExceptionsTest, ResourceCleanup_ErrorCodes) { // Moderate: Error codes require manual cleanup at each error point - + FileResult result = operation_with_error_codes_manual_cleanup(); - + EXPECT_EQ(result.error, FileError::IoError); EXPECT_EQ(EventLog::instance().count_events("Resource(R1)::acquire"), 1); EXPECT_EQ(EventLog::instance().count_events("Resource(R2)::acquire"), 1); EXPECT_EQ(EventLog::instance().count_events("Resource(R1)::release"), 1); EXPECT_EQ(EventLog::instance().count_events("Resource(R2)::release"), 1); - + // Verify no leaks EXPECT_EQ(EventLog::instance().count_events("leaked!"), 0); } @@ -331,7 +326,7 @@ TEST_F(ErrorCodesVsExceptionsTest, ResourceCleanup_ErrorCodes) TEST_F(ErrorCodesVsExceptionsTest, ResourceCleanup_Exceptions) { // Moderate: Exceptions trigger automatic RAII cleanup via stack unwinding - + try { std::string result = operation_with_exceptions_automatic_cleanup(); @@ -341,7 +336,7 @@ TEST_F(ErrorCodesVsExceptionsTest, ResourceCleanup_Exceptions) { // Exception caught } - + // Verify automatic cleanup happened in reverse order (R2 then R1) EXPECT_EQ(EventLog::instance().count_events("Resource(R1)::acquire"), 1); EXPECT_EQ(EventLog::instance().count_events("Resource(R2)::acquire"), 1); @@ -351,7 +346,6 @@ TEST_F(ErrorCodesVsExceptionsTest, ResourceCleanup_Exceptions) EXPECT_EQ(EventLog::instance().count_events("Resource(R2)::dtor"), 1); } - // ============================================================================ // When to Use Which Pattern // ============================================================================ @@ -388,16 +382,21 @@ class NetworkErrorCategory : public std::error_category { return "network"; } - + std::string message(int ev) const override { switch (static_cast(ev)) { - case NetworkError::Success: return "Success"; - case NetworkError::Timeout: return "Connection timeout"; - case NetworkError::ConnectionRefused: return "Connection refused"; - case NetworkError::HostUnreachable: return "Host unreachable"; - default: return "Unknown error"; + case NetworkError::Success: + return "Success"; + case NetworkError::Timeout: + return "Connection timeout"; + case NetworkError::ConnectionRefused: + return "Connection refused"; + case NetworkError::HostUnreachable: + return "Host unreachable"; + default: + return "Unknown error"; } } }; @@ -415,9 +414,10 @@ std::error_code make_error_code(NetworkError e) namespace std { - template<> - struct is_error_code_enum : true_type {}; -} +template <> struct is_error_code_enum : true_type +{ +}; +} // namespace std struct ConnectionResult { @@ -428,19 +428,19 @@ struct ConnectionResult ConnectionResult connect_to_server(const std::string& host) { EventLog::instance().record("connect_to_server: called"); - + if (host == "timeout.example.com") { EventLog::instance().record("connect_to_server: timeout"); return {"", make_error_code(NetworkError::Timeout)}; } - + if (host == "refused.example.com") { EventLog::instance().record("connect_to_server: refused"); return {"", make_error_code(NetworkError::ConnectionRefused)}; } - + EventLog::instance().record("connect_to_server: success"); return {"data", make_error_code(NetworkError::Success)}; } @@ -452,16 +452,16 @@ ConnectionResult connect_to_server(const std::string& host) TEST_F(ErrorCodesVsExceptionsTest, StdErrorCode_TypeSafety) { // Moderate: std::error_code provides type-safe, composable error handling - + ConnectionResult result1 = connect_to_server("timeout.example.com"); - EXPECT_TRUE(result1.error); // Implicit bool conversion + EXPECT_TRUE(result1.error); // Implicit bool conversion EXPECT_EQ(result1.error, NetworkError::Timeout); EXPECT_EQ(result1.error.message(), "Connection timeout"); - + ConnectionResult result2 = connect_to_server("success.example.com"); - EXPECT_FALSE(result2.error); // Success is falsy + EXPECT_FALSE(result2.error); // Success is falsy EXPECT_EQ(result2.data, "data"); - + // Q: How does std::error_code's bool conversion work? // A: // R: @@ -474,29 +474,29 @@ TEST_F(ErrorCodesVsExceptionsTest, StdErrorCode_TypeSafety) std::string fetch_with_retry(const std::string& host, int max_retries) { EventLog::instance().record("fetch_with_retry: start"); - + for (int attempt = 0; attempt < max_retries; ++attempt) { ConnectionResult result = connect_to_server(host); - + if (!result.error) { EventLog::instance().record("fetch_with_retry: success on attempt " + std::to_string(attempt)); return result.data; } - + // Transient errors - retry if (result.error == NetworkError::Timeout) { EventLog::instance().record("fetch_with_retry: retry on timeout"); continue; } - + // Permanent errors - throw exception EventLog::instance().record("fetch_with_retry: permanent error, throwing"); throw std::system_error(result.error, "Permanent network error"); } - + EventLog::instance().record("fetch_with_retry: max retries exceeded, throwing"); throw std::runtime_error("Max retries exceeded"); } @@ -508,11 +508,11 @@ std::string fetch_with_retry(const std::string& host, int max_retries) TEST_F(ErrorCodesVsExceptionsTest, MixedPattern_RetryLogic) { // Hard: Combining error codes (expected/transient) with exceptions (unexpected/permanent) - + // Transient error that succeeds on retry // TODO: Modify connect_to_server to succeed after first timeout // For now, test permanent error - + try { std::string data = fetch_with_retry("refused.example.com", 3); @@ -522,12 +522,11 @@ TEST_F(ErrorCodesVsExceptionsTest, MixedPattern_RetryLogic) { EXPECT_EQ(e.code(), NetworkError::ConnectionRefused); } - + EXPECT_EQ(EventLog::instance().count_events("fetch_with_retry: start"), 1); EXPECT_EQ(EventLog::instance().count_events("fetch_with_retry: permanent error, throwing"), 1); - + // Q: What observable signal confirms that no retry happened for permanent errors? // A: // R: } - diff --git a/learning_error_handling/tests/test_exception_safety.cpp b/learning_error_handling/tests/test_exception_safety.cpp index 2ac23f5..2425117 100644 --- a/learning_error_handling/tests/test_exception_safety.cpp +++ b/learning_error_handling/tests/test_exception_safety.cpp @@ -2,12 +2,12 @@ // Estimated Time: 3 hours // Difficulty: Moderate - #include "instrumentation.h" + +#include #include -#include #include -#include +#include class ExceptionSafetyTest : public ::testing::Test { @@ -46,29 +46,32 @@ class BasicSafetyContainer { EventLog::instance().record("BasicSafetyContainer::ctor"); } - + ~BasicSafetyContainer() { EventLog::instance().record("BasicSafetyContainer::dtor"); } - + void add_item(std::unique_ptr item, bool throw_after_add) { EventLog::instance().record("add_item: start"); - + items_.push_back(std::move(item)); EventLog::instance().record("add_item: item added"); - + if (throw_after_add) { EventLog::instance().record("add_item: throwing exception"); throw std::runtime_error("Simulated error"); } - + EventLog::instance().record("add_item: success"); } - - size_t size() const { return items_.size(); } + + size_t size() const + { + return items_.size(); + } private: std::vector> items_; @@ -85,14 +88,14 @@ class BasicSafetyContainer TEST_F(ExceptionSafetyTest, BasicSafety_NoLeaks) { // Easy: Basic guarantee ensures no resource leaks - + BasicSafetyContainer container; - + try { container.add_item(std::make_unique("Item1"), false); EXPECT_EQ(container.size(), 1); - + container.add_item(std::make_unique("Item2"), true); FAIL() << "Should have thrown exception"; } @@ -100,10 +103,10 @@ TEST_F(ExceptionSafetyTest, BasicSafety_NoLeaks) { // Exception caught } - + // Container state changed (Item2 was added before exception) EXPECT_EQ(container.size(), 2); - + // Verify no leaks - both items constructed and will be destroyed with container EXPECT_EQ(EventLog::instance().count_events("Tracked(Item1)::ctor"), 1); EXPECT_EQ(EventLog::instance().count_events("Tracked(Item2)::ctor"), 1); @@ -120,37 +123,40 @@ class StrongSafetyContainer { EventLog::instance().record("StrongSafetyContainer::ctor"); } - + ~StrongSafetyContainer() { EventLog::instance().record("StrongSafetyContainer::dtor"); } - + void add_item(std::unique_ptr item, bool throw_after_add) { EventLog::instance().record("add_item: start"); - + // TODO: Implement strong guarantee using copy-and-swap or similar technique // Hint: Modify a temporary, then swap only if successful - + std::vector> temp_items; for (auto& existing : items_) { // Can't copy unique_ptr, so this is a simplified example // In real code, you'd use a different strategy } - + if (throw_after_add) { EventLog::instance().record("add_item: throwing before commit"); throw std::runtime_error("Simulated error"); } - + items_.push_back(std::move(item)); EventLog::instance().record("add_item: success"); } - - size_t size() const { return items_.size(); } + + size_t size() const + { + return items_.size(); + } private: std::vector> items_; @@ -167,14 +173,14 @@ class StrongSafetyContainer TEST_F(ExceptionSafetyTest, StrongSafety_NoStateChange) { // Moderate: Strong guarantee ensures either success or no observable change - + StrongSafetyContainer container; - + try { container.add_item(std::make_unique("Item1"), false); EXPECT_EQ(container.size(), 1); - + container.add_item(std::make_unique("Item2"), true); FAIL() << "Should have thrown exception"; } @@ -182,10 +188,10 @@ TEST_F(ExceptionSafetyTest, StrongSafety_NoStateChange) { // Exception caught } - + // Container state unchanged (Item2 was NOT added) EXPECT_EQ(container.size(), 1); - + // Verify Item2 was constructed but not added to container EXPECT_EQ(EventLog::instance().count_events("Tracked(Item2)::ctor"), 1); } @@ -197,27 +203,25 @@ TEST_F(ExceptionSafetyTest, StrongSafety_NoStateChange) class CopyAndSwapAssignment { public: - CopyAndSwapAssignment() - : data_(nullptr) + CopyAndSwapAssignment() : data_(nullptr) { EventLog::instance().record("CopyAndSwapAssignment::ctor"); } - - explicit CopyAndSwapAssignment(const std::string& name) - : data_(std::make_unique(name)) + + explicit CopyAndSwapAssignment(const std::string& name) : data_(std::make_unique(name)) { EventLog::instance().record("CopyAndSwapAssignment::ctor(name)"); } - + ~CopyAndSwapAssignment() { EventLog::instance().record("CopyAndSwapAssignment::dtor"); } - + CopyAndSwapAssignment& operator=(const CopyAndSwapAssignment& other) { EventLog::instance().record("operator=: copy-and-swap start"); - + if (this != &other) { // TODO: Create a temporary copy, then swap @@ -227,15 +231,18 @@ class CopyAndSwapAssignment EventLog::instance().record("operator=: creating temp copy"); temp = std::make_unique(other.data_->name()); } - + EventLog::instance().record("operator=: swapping"); data_.swap(temp); } - + return *this; } - - bool has_data() const { return data_ != nullptr; } + + bool has_data() const + { + return data_ != nullptr; + } private: std::unique_ptr data_; @@ -252,21 +259,20 @@ class CopyAndSwapAssignment TEST_F(ExceptionSafetyTest, CopyAndSwap_StrongGuarantee) { // Hard: Copy-and-swap idiom provides strong guarantee for assignment - + CopyAndSwapAssignment a1("A1"); CopyAndSwapAssignment a2("A2"); - + EventLog::instance().clear(); - + a1 = a2; - + EXPECT_TRUE(a1.has_data()); EXPECT_EQ(EventLog::instance().count_events("operator=: creating temp copy"), 1); EXPECT_EQ(EventLog::instance().count_events("operator=: swapping"), 1); EXPECT_EQ(EventLog::instance().count_events("Tracked(A1)::dtor"), 1); } - // ============================================================================ // Exception Safety in Constructors // ============================================================================ @@ -275,25 +281,23 @@ class PartiallyConstructedObject { public: PartiallyConstructedObject(bool throw_after_first) - : first_(std::make_unique("First")) - , second_(nullptr) - , third_(nullptr) + : first_(std::make_unique("First")), second_(nullptr), third_(nullptr) { EventLog::instance().record("PartiallyConstructedObject::ctor - first initialized"); - + if (throw_after_first) { EventLog::instance().record("PartiallyConstructedObject::ctor - throwing"); throw std::runtime_error("Construction failed"); } - + second_ = std::make_unique("Second"); EventLog::instance().record("PartiallyConstructedObject::ctor - second initialized"); - + third_ = std::make_unique("Third"); EventLog::instance().record("PartiallyConstructedObject::ctor - complete"); } - + ~PartiallyConstructedObject() { EventLog::instance().record("PartiallyConstructedObject::dtor"); @@ -316,7 +320,7 @@ class PartiallyConstructedObject TEST_F(ExceptionSafetyTest, Constructor_BasicSafety) { // Moderate: Constructors provide basic safety - initialized members are cleaned up - + try { PartiallyConstructedObject obj(true); @@ -326,20 +330,19 @@ TEST_F(ExceptionSafetyTest, Constructor_BasicSafety) { // Exception caught } - + // Verify first_ was initialized and cleaned up EXPECT_EQ(EventLog::instance().count_events("Tracked(First)::ctor"), 1); EXPECT_EQ(EventLog::instance().count_events("Tracked(First)::dtor"), 1); - + // Verify second_ and third_ were never initialized EXPECT_EQ(EventLog::instance().count_events("Tracked(Second)::ctor"), 0); EXPECT_EQ(EventLog::instance().count_events("Tracked(Third)::ctor"), 0); - + // Verify destructor was NOT called (object never fully constructed) EXPECT_EQ(EventLog::instance().count_events("PartiallyConstructedObject::dtor"), 0); } - // ============================================================================ // Exception Safety in Assignment Operators // ============================================================================ @@ -347,34 +350,32 @@ TEST_F(ExceptionSafetyTest, Constructor_BasicSafety) class AssignmentSafety { public: - AssignmentSafety() - : data_(nullptr) + AssignmentSafety() : data_(nullptr) { EventLog::instance().record("AssignmentSafety::ctor"); } - - explicit AssignmentSafety(const std::string& name) - : data_(std::make_unique(name)) + + explicit AssignmentSafety(const std::string& name) : data_(std::make_unique(name)) { EventLog::instance().record("AssignmentSafety::ctor(name)"); } - + ~AssignmentSafety() { EventLog::instance().record("AssignmentSafety::dtor"); } - + // Copy assignment - basic safety (may leak on exception) AssignmentSafety& operator=(const AssignmentSafety& other) { EventLog::instance().record("operator=: copy assignment start"); - + if (this != &other) { // Dangerous: delete first, then allocate data_.reset(); EventLog::instance().record("operator=: old data deleted"); - + if (other.data_) { // If this throws, we've already deleted our data! @@ -382,24 +383,27 @@ class AssignmentSafety EventLog::instance().record("operator=: new data created"); } } - + return *this; } - + // Move assignment - no-throw guarantee AssignmentSafety& operator=(AssignmentSafety&& other) noexcept { EventLog::instance().record("operator=: move assignment"); - + if (this != &other) { data_ = std::move(other.data_); } - + return *this; } - - bool has_data() const { return data_ != nullptr; } + + bool has_data() const + { + return data_ != nullptr; + } private: std::unique_ptr data_; @@ -416,26 +420,25 @@ class AssignmentSafety TEST_F(ExceptionSafetyTest, Assignment_SafetyGuarantees) { // Hard: Assignment operators have different safety guarantees - + AssignmentSafety a1("A1"); AssignmentSafety a2("A2"); - + // Move assignment - no-throw static_assert(noexcept(a1 = std::move(a2)), "Move assignment should be noexcept"); - + EventLog::instance().clear(); - + AssignmentSafety a3("A3"); AssignmentSafety a4("A4"); - + // Copy assignment - may throw a3 = a4; - + EXPECT_EQ(EventLog::instance().count_events("operator=: old data deleted"), 1); EXPECT_EQ(EventLog::instance().count_events("operator=: new data created"), 1); } - // ============================================================================ // Transactional Operations - Strong Guarantee Pattern // ============================================================================ @@ -447,7 +450,7 @@ class Transaction { EventLog::instance().record("Transaction::ctor"); } - + ~Transaction() { if (!committed_) @@ -460,40 +463,43 @@ class Transaction EventLog::instance().record("Transaction::dtor - already committed"); } } - + void add_operation(std::unique_ptr item) { EventLog::instance().record("Transaction::add_operation"); pending_.push_back(std::move(item)); } - + void commit(bool throw_on_commit) { EventLog::instance().record("Transaction::commit - start"); - + if (throw_on_commit) { EventLog::instance().record("Transaction::commit - throwing"); throw std::runtime_error("Commit failed"); } - + for (auto& item : pending_) { committed_items_.push_back(std::move(item)); } pending_.clear(); - + committed_ = true; EventLog::instance().record("Transaction::commit - success"); } - + void rollback() { EventLog::instance().record("Transaction::rollback"); pending_.clear(); } - - size_t committed_count() const { return committed_items_.size(); } + + size_t committed_count() const + { + return committed_items_.size(); + } private: std::vector> pending_; @@ -512,14 +518,14 @@ class Transaction TEST_F(ExceptionSafetyTest, Transaction_StrongGuarantee) { // Hard: Transaction pattern provides strong exception safety - + { Transaction txn; txn.add_operation(std::make_unique("Op1")); txn.add_operation(std::make_unique("Op2")); - + EventLog::instance().clear(); - + try { txn.commit(true); @@ -529,11 +535,11 @@ TEST_F(ExceptionSafetyTest, Transaction_StrongGuarantee) { // Exception caught } - + EXPECT_EQ(txn.committed_count(), 0); EXPECT_EQ(EventLog::instance().count_events("Transaction::commit - success"), 0); } - + EXPECT_EQ(EventLog::instance().count_events("Transaction::dtor - rolling back"), 1); EXPECT_EQ(EventLog::instance().count_events("Tracked(Op1)::dtor"), 1); EXPECT_EQ(EventLog::instance().count_events("Tracked(Op2)::dtor"), 1); @@ -554,4 +560,3 @@ TEST_F(ExceptionSafetyTest, Transaction_StrongGuarantee) // Q: What operations must always provide no-throw guarantee? // A: // R: - diff --git a/learning_error_handling/tests/test_noexcept.cpp b/learning_error_handling/tests/test_noexcept.cpp index baecba0..5d319c0 100644 --- a/learning_error_handling/tests/test_noexcept.cpp +++ b/learning_error_handling/tests/test_noexcept.cpp @@ -2,11 +2,11 @@ // Estimated Time: 3 hours // Difficulty: Moderate - #include "instrumentation.h" + #include -#include #include +#include class NoexceptTest : public ::testing::Test { @@ -48,16 +48,16 @@ int unsafe_add(int a, int b) TEST_F(NoexceptTest, Noexcept_BasicSpecification) { // Easy: noexcept is a compile-time specification - + static_assert(noexcept(safe_add(1, 2)), "safe_add should be noexcept"); static_assert(!noexcept(unsafe_add(1, 2)), "unsafe_add should not be noexcept"); - + int result1 = safe_add(10, 20); EXPECT_EQ(result1, 30); - + int result2 = unsafe_add(10, 20); EXPECT_EQ(result2, 30); - + // Q: Can the compiler optimize noexcept functions differently? // A: // R: @@ -70,37 +70,34 @@ TEST_F(NoexceptTest, Noexcept_BasicSpecification) class NonNoexceptMove { public: - explicit NonNoexceptMove(const std::string& name) - : name_(name) + explicit NonNoexceptMove(const std::string& name) : name_(name) { EventLog::instance().record("NonNoexceptMove(" + name_ + ")::ctor"); } - + ~NonNoexceptMove() { EventLog::instance().record("NonNoexceptMove(" + name_ + ")::dtor"); } - + // Move constructor NOT marked noexcept - NonNoexceptMove(NonNoexceptMove&& other) - : name_(std::move(other.name_)) + NonNoexceptMove(NonNoexceptMove&& other) : name_(std::move(other.name_)) { EventLog::instance().record("NonNoexceptMove::move_ctor"); } - + NonNoexceptMove& operator=(NonNoexceptMove&& other) { EventLog::instance().record("NonNoexceptMove::move_assign"); name_ = std::move(other.name_); return *this; } - - NonNoexceptMove(const NonNoexceptMove& other) - : name_(other.name_) + + NonNoexceptMove(const NonNoexceptMove& other) : name_(other.name_) { EventLog::instance().record("NonNoexceptMove::copy_ctor"); } - + NonNoexceptMove& operator=(const NonNoexceptMove& other) { EventLog::instance().record("NonNoexceptMove::copy_assign"); @@ -115,37 +112,34 @@ class NonNoexceptMove class NoexceptMove { public: - explicit NoexceptMove(const std::string& name) - : name_(name) + explicit NoexceptMove(const std::string& name) : name_(name) { EventLog::instance().record("NoexceptMove(" + name_ + ")::ctor"); } - + ~NoexceptMove() { EventLog::instance().record("NoexceptMove(" + name_ + ")::dtor"); } - + // Move constructor marked noexcept - NoexceptMove(NoexceptMove&& other) noexcept - : name_(std::move(other.name_)) + NoexceptMove(NoexceptMove&& other) noexcept : name_(std::move(other.name_)) { EventLog::instance().record("NoexceptMove::move_ctor"); } - + NoexceptMove& operator=(NoexceptMove&& other) noexcept { EventLog::instance().record("NoexceptMove::move_assign"); name_ = std::move(other.name_); return *this; } - - NoexceptMove(const NoexceptMove& other) - : name_(other.name_) + + NoexceptMove(const NoexceptMove& other) : name_(other.name_) { EventLog::instance().record("NoexceptMove::copy_ctor"); } - + NoexceptMove& operator=(const NoexceptMove& other) { EventLog::instance().record("NoexceptMove::copy_assign"); @@ -168,38 +162,38 @@ class NoexceptMove TEST_F(NoexceptTest, VectorReallocation_NoexceptMoveOptimization) { // Hard: std::vector uses move only if move constructor is noexcept - + { std::vector vec; vec.reserve(2); - + vec.emplace_back("A"); vec.emplace_back("B"); - + EventLog::instance().clear(); - + // Force reallocation vec.emplace_back("C"); - + // Without noexcept move, vector uses copy constructor for safety // (In practice, std::string's move is noexcept, so this may still move) // The key point: vector checks noexcept before deciding } - + EventLog::instance().clear(); - + { std::vector vec; vec.reserve(2); - + vec.emplace_back("A"); vec.emplace_back("B"); - + EventLog::instance().clear(); - + // Force reallocation vec.emplace_back("C"); - + // With noexcept move, vector uses move constructor EXPECT_GE(EventLog::instance().count_events("NoexceptMove::move_ctor"), 2); } @@ -209,24 +203,25 @@ TEST_F(NoexceptTest, VectorReallocation_NoexceptMoveOptimization) // Conditional noexcept // ============================================================================ -template -class ConditionalNoexceptWrapper +template class ConditionalNoexceptWrapper { public: - explicit ConditionalNoexceptWrapper(T value) - : value_(std::move(value)) + explicit ConditionalNoexceptWrapper(T value) : value_(std::move(value)) { EventLog::instance().record("ConditionalNoexceptWrapper::ctor"); } - + // Conditionally noexcept based on T's move constructor ConditionalNoexceptWrapper(ConditionalNoexceptWrapper&& other) noexcept(std::is_nothrow_move_constructible_v) - : value_(std::move(other.value_)) + : value_(std::move(other.value_)) { EventLog::instance().record("ConditionalNoexceptWrapper::move_ctor"); } - - T& get() { return value_; } + + T& get() + { + return value_; + } private: T value_; @@ -243,27 +238,27 @@ class ConditionalNoexceptWrapper TEST_F(NoexceptTest, ConditionalNoexcept_TypeDependent) { // Hard: noexcept can be conditional based on template parameters - + using IntWrapper = ConditionalNoexceptWrapper; using NonNoexceptWrapper = ConditionalNoexceptWrapper; using NoexceptWrapper = ConditionalNoexceptWrapper; - + // int is nothrow move constructible static_assert(std::is_nothrow_move_constructible_v, "int should be nothrow movable"); static_assert(noexcept(IntWrapper(std::declval())), "IntWrapper move should be noexcept"); - + // NonNoexceptMove is not nothrow move constructible - static_assert(!std::is_nothrow_move_constructible_v, "NonNoexceptMove should not be nothrow movable"); - static_assert(!noexcept(NonNoexceptWrapper(std::declval())), "NonNoexceptWrapper move should not be noexcept"); - + static_assert(!std::is_nothrow_move_constructible_v, + "NonNoexceptMove should not be nothrow movable"); + static_assert(!noexcept(NonNoexceptWrapper(std::declval())), + "NonNoexceptWrapper move should not be noexcept"); + // NoexceptMove is nothrow move constructible static_assert(std::is_nothrow_move_constructible_v, "NoexceptMove should be nothrow movable"); - static_assert(noexcept(NoexceptWrapper(std::declval())), "NoexceptWrapper move should be noexcept"); + static_assert(noexcept(NoexceptWrapper(std::declval())), + "NoexceptWrapper move should be noexcept"); } - - - // ============================================================================ // noexcept and std::vector Performance // ============================================================================ @@ -271,34 +266,29 @@ TEST_F(NoexceptTest, ConditionalNoexcept_TypeDependent) class ExpensiveCopy { public: - explicit ExpensiveCopy(int id) - : id_(id) - , data_(std::make_unique("Data" + std::to_string(id))) + explicit ExpensiveCopy(int id) : id_(id), data_(std::make_unique("Data" + std::to_string(id))) { EventLog::instance().record("ExpensiveCopy(" + std::to_string(id_) + ")::ctor"); } - + ~ExpensiveCopy() { EventLog::instance().record("ExpensiveCopy(" + std::to_string(id_) + ")::dtor"); } - + // Copy is expensive ExpensiveCopy(const ExpensiveCopy& other) - : id_(other.id_) - , data_(std::make_unique("Data" + std::to_string(id_))) + : id_(other.id_), data_(std::make_unique("Data" + std::to_string(id_))) { EventLog::instance().record("ExpensiveCopy::copy_ctor - expensive!"); } - + // Move is cheap and noexcept - ExpensiveCopy(ExpensiveCopy&& other) noexcept - : id_(other.id_) - , data_(std::move(other.data_)) + ExpensiveCopy(ExpensiveCopy&& other) noexcept : id_(other.id_), data_(std::move(other.data_)) { EventLog::instance().record("ExpensiveCopy::move_ctor - cheap!"); } - + ExpensiveCopy& operator=(const ExpensiveCopy&) = delete; ExpensiveCopy& operator=(ExpensiveCopy&&) = delete; @@ -318,18 +308,18 @@ class ExpensiveCopy TEST_F(NoexceptTest, Vector_NoexceptMoveOptimization) { // Hard: noexcept move enables efficient vector reallocation - + std::vector vec; vec.reserve(2); - + vec.emplace_back(1); vec.emplace_back(2); - + EventLog::instance().clear(); - + // Force reallocation - should use move, not copy vec.emplace_back(3); - + // Verify moves were used (cheap), not copies (expensive) EXPECT_GE(EventLog::instance().count_events("ExpensiveCopy::move_ctor - cheap!"), 2); EXPECT_EQ(EventLog::instance().count_events("ExpensiveCopy::copy_ctor - expensive!"), 0); @@ -339,19 +329,18 @@ TEST_F(NoexceptTest, Vector_NoexceptMoveOptimization) // Conditional noexcept with Member Functions // ============================================================================ -template -class Container +template class Container { public: Container() = default; - + // Conditionally noexcept based on T's move constructor void add(T value) noexcept(std::is_nothrow_move_constructible_v) { EventLog::instance().record("Container::add"); items_.push_back(std::move(value)); } - + // Conditionally noexcept based on T's swap void swap(Container& other) noexcept(std::is_nothrow_swappable_v) { @@ -370,47 +359,39 @@ class Container TEST_F(NoexceptTest, ConditionalNoexcept_MemberFunctions) { // Hard: Member functions can be conditionally noexcept - + // Check type traits directly - static_assert(std::is_nothrow_move_constructible_v, - "int should be nothrow move constructible"); - - static_assert(!std::is_nothrow_move_constructible_v, + static_assert(std::is_nothrow_move_constructible_v, "int should be nothrow move constructible"); + + static_assert(!std::is_nothrow_move_constructible_v, "NonNoexceptMove should not be nothrow move constructible"); - - static_assert(std::is_nothrow_move_constructible_v, + + static_assert(std::is_nothrow_move_constructible_v, "NoexceptMove should be nothrow move constructible"); - + // Container::add is conditionally noexcept based on T // The actual noexcept specification depends on vector's push_back // which may have additional requirements beyond just T's move constructor } - - - - // ============================================================================ // noexcept Propagation in Templates // ============================================================================ -template -class Wrapper +template class Wrapper { public: - explicit Wrapper(T value) - : value_(std::move(value)) + explicit Wrapper(T value) : value_(std::move(value)) { EventLog::instance().record("Wrapper::ctor"); } - + // Propagate noexcept from T - Wrapper(Wrapper&& other) noexcept(std::is_nothrow_move_constructible_v) - : value_(std::move(other.value_)) + Wrapper(Wrapper&& other) noexcept(std::is_nothrow_move_constructible_v) : value_(std::move(other.value_)) { EventLog::instance().record("Wrapper::move_ctor"); } - + Wrapper& operator=(Wrapper&& other) noexcept(std::is_nothrow_move_assignable_v) { EventLog::instance().record("Wrapper::move_assign"); @@ -429,22 +410,20 @@ class Wrapper TEST_F(NoexceptTest, Templates_NoexceptPropagation) { // Hard: Templates should propagate noexcept specifications - + using IntWrapper = Wrapper; using NoexceptWrapper = Wrapper; using NonNoexceptWrapper = Wrapper; - + // int operations are noexcept - static_assert(std::is_nothrow_move_constructible_v, - "Wrapper move should be noexcept"); - + static_assert(std::is_nothrow_move_constructible_v, "Wrapper move should be noexcept"); + // NoexceptMove operations are noexcept // Note: std::string's move is noexcept, so NoexceptMove's move is noexcept - static_assert(std::is_nothrow_move_constructible_v, - "NoexceptMove should be nothrow movable"); - + static_assert(std::is_nothrow_move_constructible_v, "NoexceptMove should be nothrow movable"); + // NonNoexceptMove operations are not noexcept - static_assert(!std::is_nothrow_move_constructible_v, + static_assert(!std::is_nothrow_move_constructible_v, "NonNoexceptMove should not be nothrow movable"); } @@ -463,4 +442,3 @@ TEST_F(NoexceptTest, Templates_NoexceptPropagation) // Q: How does noexcept affect API design and evolution? // A: // R: - diff --git a/learning_error_handling/tests/test_optional_result_types.cpp b/learning_error_handling/tests/test_optional_result_types.cpp index 32206bd..aef26cb 100644 --- a/learning_error_handling/tests/test_optional_result_types.cpp +++ b/learning_error_handling/tests/test_optional_result_types.cpp @@ -2,13 +2,13 @@ // Estimated Time: 3 hours // Difficulty: Moderate - #include "instrumentation.h" + #include #include +#include #include #include -#include class OptionalResultTypesTest : public ::testing::Test { @@ -26,7 +26,7 @@ class OptionalResultTypesTest : public ::testing::Test std::optional parse_int(const std::string& str) { EventLog::instance().record("parse_int: called with '" + str + "'"); - + try { size_t pos; @@ -57,19 +57,19 @@ std::optional parse_int(const std::string& str) TEST_F(OptionalResultTypesTest, Optional_BasicUsage) { // Easy: std::optional represents values that may or may not exist - + auto result1 = parse_int("123"); EXPECT_TRUE(result1.has_value()); EXPECT_EQ(*result1, 123); EXPECT_EQ(result1.value(), 123); - + auto result2 = parse_int("abc"); EXPECT_FALSE(result2.has_value()); - + // Using value_or for default values EXPECT_EQ(result1.value_or(0), 123); EXPECT_EQ(result2.value_or(0), 0); - + EXPECT_EQ(EventLog::instance().count_events("parse_int: success"), 1); EXPECT_EQ(EventLog::instance().count_events("parse_int: exception caught"), 1); } @@ -81,13 +81,13 @@ TEST_F(OptionalResultTypesTest, Optional_BasicUsage) std::optional create_tracked_conditionally(bool should_create) { EventLog::instance().record("create_tracked: called"); - + if (!should_create) { EventLog::instance().record("create_tracked: returning nullopt"); return std::nullopt; } - + EventLog::instance().record("create_tracked: creating Tracked"); // TODO: Return a Tracked object wrapped in optional return Tracked("Optional"); @@ -104,18 +104,18 @@ std::optional create_tracked_conditionally(bool should_create) TEST_F(OptionalResultTypesTest, Optional_ComplexTypes) { // Moderate: std::optional works with any type, including move-only types - + auto result1 = create_tracked_conditionally(true); EXPECT_TRUE(result1.has_value()); - + // Verify Tracked was constructed (may involve moves) EXPECT_GE(EventLog::instance().count_events("::ctor"), 1); - + EventLog::instance().clear(); - + auto result2 = create_tracked_conditionally(false); EXPECT_FALSE(result2.has_value()); - + // Verify no Tracked object was created EXPECT_EQ(EventLog::instance().count_events("::ctor"), 0); } @@ -132,8 +132,7 @@ enum class ParseError OutOfRange }; -template -class Result +template class Result { public: static Result Ok(T value) @@ -141,33 +140,46 @@ class Result EventLog::instance().record("Result::Ok"); return Result(std::move(value), E{}); } - + static Result Err(E error) { EventLog::instance().record("Result::Err"); return Result(std::nullopt, error); } - - bool is_ok() const { return value_.has_value(); } - bool is_err() const { return !value_.has_value(); } - - const T& value() const { return *value_; } - T& value() { return *value_; } - - const E& error() const { return error_; } - + + bool is_ok() const + { + return value_.has_value(); + } + bool is_err() const + { + return !value_.has_value(); + } + + const T& value() const + { + return *value_; + } + T& value() + { + return *value_; + } + + const E& error() const + { + return error_; + } + T value_or(T default_value) const { return value_.value_or(std::move(default_value)); } private: - Result(std::optional value, E error) - : value_(std::move(value)) - , error_(error) + Result(std::optional value, E error) : value_(std::move(value)), error_(error) { } - + std::optional value_; E error_; }; @@ -175,24 +187,24 @@ class Result Result parse_int_with_error(const std::string& str) { EventLog::instance().record("parse_int_with_error: called"); - + if (str.empty()) { EventLog::instance().record("parse_int_with_error: empty string"); return Result::Err(ParseError::EmptyString); } - + try { size_t pos; int value = std::stoi(str, &pos); - + if (pos != str.length()) { EventLog::instance().record("parse_int_with_error: invalid format"); return Result::Err(ParseError::InvalidFormat); } - + EventLog::instance().record("parse_int_with_error: success"); return Result::Ok(value); } @@ -219,19 +231,19 @@ Result parse_int_with_error(const std::string& str) TEST_F(OptionalResultTypesTest, ResultType_ErrorInformation) { // Moderate: Result types carry both success values and error information - + auto result1 = parse_int_with_error("123"); EXPECT_TRUE(result1.is_ok()); EXPECT_EQ(result1.value(), 123); - + auto result2 = parse_int_with_error(""); EXPECT_TRUE(result2.is_err()); EXPECT_EQ(result2.error(), ParseError::EmptyString); - + auto result3 = parse_int_with_error("12abc"); EXPECT_TRUE(result3.is_err()); EXPECT_EQ(result3.error(), ParseError::InvalidFormat); - + auto result4 = parse_int_with_error("999999999999999999999"); EXPECT_TRUE(result4.is_err()); EXPECT_EQ(result4.error(), ParseError::OutOfRange); @@ -244,7 +256,7 @@ TEST_F(OptionalResultTypesTest, ResultType_ErrorInformation) Result parse_and_double(const std::string& str) { EventLog::instance().record("parse_and_double: start"); - + // TODO: Parse the string and return error if parsing fails auto result = parse_int_with_error(str); if (result.is_err()) @@ -252,7 +264,7 @@ Result parse_and_double(const std::string& str) EventLog::instance().record("parse_and_double: propagating error"); return Result::Err(result.error()); } - + EventLog::instance().record("parse_and_double: doubling value"); return Result::Ok(result.value() * 2); } @@ -268,20 +280,19 @@ Result parse_and_double(const std::string& str) TEST_F(OptionalResultTypesTest, ResultType_ErrorPropagation) { // Moderate: Result types require explicit error propagation - + auto result1 = parse_and_double("10"); EXPECT_TRUE(result1.is_ok()); EXPECT_EQ(result1.value(), 20); - + auto result2 = parse_and_double("abc"); EXPECT_TRUE(result2.is_err()); EXPECT_EQ(result2.error(), ParseError::InvalidFormat); - + EXPECT_EQ(EventLog::instance().count_events("parse_and_double: propagating error"), 1); EXPECT_EQ(EventLog::instance().count_events("parse_and_double: doubling value"), 1); } - // ============================================================================ // std::variant for Multiple Error Types // ============================================================================ @@ -304,27 +315,27 @@ using FetchResult = std::variant; FetchResult fetch_and_parse(const std::string& url) { EventLog::instance().record("fetch_and_parse: start"); - + // Simulate network fetch if (url == "timeout.example.com") { EventLog::instance().record("fetch_and_parse: network timeout"); return NetworkError::Timeout; } - + if (url == "refused.example.com") { EventLog::instance().record("fetch_and_parse: connection refused"); return NetworkError::ConnectionRefused; } - + // Simulate parsing if (url == "invalid.example.com") { EventLog::instance().record("fetch_and_parse: parse error"); return ParseError2::InvalidFormat; } - + EventLog::instance().record("fetch_and_parse: success"); return std::string("data from ") + url; } @@ -340,15 +351,15 @@ FetchResult fetch_and_parse(const std::string& url) TEST_F(OptionalResultTypesTest, Variant_MultipleErrorTypes) { // Hard: std::variant can represent success or multiple error types - + auto result1 = fetch_and_parse("success.example.com"); EXPECT_TRUE(std::holds_alternative(result1)); EXPECT_EQ(std::get(result1), "data from success.example.com"); - + auto result2 = fetch_and_parse("timeout.example.com"); EXPECT_TRUE(std::holds_alternative(result2)); EXPECT_EQ(std::get(result2), NetworkError::Timeout); - + auto result3 = fetch_and_parse("invalid.example.com"); EXPECT_TRUE(std::holds_alternative(result3)); EXPECT_EQ(std::get(result3), ParseError2::InvalidFormat); @@ -361,28 +372,29 @@ TEST_F(OptionalResultTypesTest, Variant_MultipleErrorTypes) std::string handle_fetch_result(const FetchResult& result) { EventLog::instance().record("handle_fetch_result: start"); - + // TODO: Use std::visit to handle each variant alternative - return std::visit([](auto&& arg) -> std::string - { - using T = std::decay_t; - - if constexpr (std::is_same_v) - { - EventLog::instance().record("visitor: success case"); - return arg; - } - else if constexpr (std::is_same_v) - { - EventLog::instance().record("visitor: network error case"); - return "Network error"; - } - else if constexpr (std::is_same_v) - { - EventLog::instance().record("visitor: parse error case"); - return "Parse error"; - } - }, result); + return std::visit( + [](auto&& arg) -> std::string { + using T = std::decay_t; + + if constexpr (std::is_same_v) + { + EventLog::instance().record("visitor: success case"); + return arg; + } + else if constexpr (std::is_same_v) + { + EventLog::instance().record("visitor: network error case"); + return "Network error"; + } + else if constexpr (std::is_same_v) + { + EventLog::instance().record("visitor: parse error case"); + return "Parse error"; + } + }, + result); } // Q: How does std::visit ensure all variant alternatives are handled? @@ -392,14 +404,14 @@ std::string handle_fetch_result(const FetchResult& result) TEST_F(OptionalResultTypesTest, Variant_VisitorPattern) { // Hard: std::visit provides type-safe exhaustive handling - + auto result1 = fetch_and_parse("success.example.com"); std::string msg1 = handle_fetch_result(result1); EXPECT_EQ(msg1, "data from success.example.com"); EXPECT_EQ(EventLog::instance().count_events("visitor: success case"), 1); - + EventLog::instance().clear(); - + auto result2 = fetch_and_parse("timeout.example.com"); std::string msg2 = handle_fetch_result(result2); EXPECT_EQ(msg2, "Network error"); @@ -413,23 +425,21 @@ TEST_F(OptionalResultTypesTest, Variant_VisitorPattern) class DatabaseConnection { public: - explicit DatabaseConnection(const std::string& name) - : name_(name) + explicit DatabaseConnection(const std::string& name) : name_(name) { EventLog::instance().record("DatabaseConnection(" + name_ + ")::ctor"); } - + ~DatabaseConnection() { EventLog::instance().record("DatabaseConnection(" + name_ + ")::dtor"); } - - DatabaseConnection(DatabaseConnection&& other) noexcept - : name_(std::move(other.name_)) + + DatabaseConnection(DatabaseConnection&& other) noexcept : name_(std::move(other.name_)) { EventLog::instance().record("DatabaseConnection::move_ctor"); } - + DatabaseConnection(const DatabaseConnection&) = delete; DatabaseConnection& operator=(const DatabaseConnection&) = delete; DatabaseConnection& operator=(DatabaseConnection&&) = delete; @@ -448,19 +458,19 @@ enum class DbError Result connect_to_database(const std::string& host) { EventLog::instance().record("connect_to_database: called"); - + if (host == "timeout.example.com") { EventLog::instance().record("connect_to_database: timeout"); return Result::Err(DbError::Timeout); } - + if (host == "auth.example.com") { EventLog::instance().record("connect_to_database: auth failed"); return Result::Err(DbError::AuthenticationFailed); } - + EventLog::instance().record("connect_to_database: success"); return Result::Ok(DatabaseConnection(host)); } @@ -476,31 +486,30 @@ Result connect_to_database(const std::string& host) TEST_F(OptionalResultTypesTest, ResultType_WithRAII) { // Hard: Result types work seamlessly with RAII resources - + { auto result1 = connect_to_database("success.example.com"); EXPECT_TRUE(result1.is_ok()); EXPECT_EQ(EventLog::instance().count_events("DatabaseConnection(success.example.com)::ctor"), 1); - + // result1 goes out of scope here } - + // Verify DatabaseConnection was destroyed EXPECT_EQ(EventLog::instance().count_events("DatabaseConnection(success.example.com)::dtor"), 1); - + EventLog::instance().clear(); - + { auto result2 = connect_to_database("timeout.example.com"); EXPECT_TRUE(result2.is_err()); EXPECT_EQ(result2.error(), DbError::Timeout); - + // No DatabaseConnection created EXPECT_EQ(EventLog::instance().count_events("DatabaseConnection"), 0); } } - // ============================================================================ // Performance: Optional vs Exceptions // ============================================================================ @@ -516,4 +525,3 @@ TEST_F(OptionalResultTypesTest, ResultType_WithRAII) // Q: For a parser that fails 50% of the time, which approach is more efficient? // A: // R: - diff --git a/learning_error_handling/tests/test_raii_exception_safety.cpp b/learning_error_handling/tests/test_raii_exception_safety.cpp index 6c44ce2..8d85ea4 100644 --- a/learning_error_handling/tests/test_raii_exception_safety.cpp +++ b/learning_error_handling/tests/test_raii_exception_safety.cpp @@ -2,12 +2,12 @@ // Estimated Time: 2 hours // Difficulty: Easy - #include "instrumentation.h" + +#include #include #include #include -#include class RaiiExceptionSafetyTest : public ::testing::Test { @@ -16,7 +16,7 @@ class RaiiExceptionSafetyTest : public ::testing::Test { EventLog::instance().clear(); } - + void TearDown() override { std::remove("test_raii_file.txt"); @@ -30,19 +30,17 @@ class RaiiExceptionSafetyTest : public ::testing::Test class ManualResource { public: - explicit ManualResource(const std::string& name) - : name_(name) - , resource_(new Tracked(name)) + explicit ManualResource(const std::string& name) : name_(name), resource_(new Tracked(name)) { EventLog::instance().record("ManualResource(" + name_ + ")::ctor"); } - + ~ManualResource() { EventLog::instance().record("ManualResource(" + name_ + ")::dtor"); - delete resource_; // RAII ensures cleanup even on exception + delete resource_; // RAII ensures cleanup even on exception } - + ManualResource(const ManualResource&) = delete; ManualResource& operator=(const ManualResource&) = delete; @@ -54,10 +52,10 @@ class ManualResource void operation_with_manual_cleanup() { EventLog::instance().record("operation: start"); - + ManualResource r1("R1"); ManualResource r2("R2"); - + EventLog::instance().record("operation: throwing exception"); throw std::runtime_error("Simulated error"); } @@ -73,7 +71,7 @@ void operation_with_manual_cleanup() TEST_F(RaiiExceptionSafetyTest, BasicRAII_AutomaticCleanup) { // Easy: RAII ensures cleanup even when exceptions are thrown - + try { operation_with_manual_cleanup(); @@ -83,19 +81,18 @@ TEST_F(RaiiExceptionSafetyTest, BasicRAII_AutomaticCleanup) { // Exception caught } - + // Verify both resources were cleaned up EXPECT_EQ(EventLog::instance().count_events("ManualResource(R1)::ctor"), 1); EXPECT_EQ(EventLog::instance().count_events("ManualResource(R2)::ctor"), 1); EXPECT_EQ(EventLog::instance().count_events("ManualResource(R2)::dtor"), 1); EXPECT_EQ(EventLog::instance().count_events("ManualResource(R1)::dtor"), 1); - + // Verify Tracked objects were also destroyed EXPECT_EQ(EventLog::instance().count_events("Tracked(R1)::dtor"), 1); EXPECT_EQ(EventLog::instance().count_events("Tracked(R2)::dtor"), 1); } - // ============================================================================ // Smart Pointers - RAII for Dynamic Memory // ============================================================================ @@ -103,11 +100,11 @@ TEST_F(RaiiExceptionSafetyTest, BasicRAII_AutomaticCleanup) void operation_with_smart_pointers() { EventLog::instance().record("operation: start"); - + auto r1 = std::make_unique("R1"); auto r2 = std::make_unique("R2"); auto r3 = std::make_unique("R3"); - + EventLog::instance().record("operation: throwing exception"); throw std::runtime_error("Simulated error"); } @@ -119,7 +116,7 @@ void operation_with_smart_pointers() TEST_F(RaiiExceptionSafetyTest, SmartPointers_AutomaticCleanup) { // Easy: Smart pointers provide automatic RAII cleanup - + try { operation_with_smart_pointers(); @@ -129,7 +126,7 @@ TEST_F(RaiiExceptionSafetyTest, SmartPointers_AutomaticCleanup) { // Exception caught } - + // Verify all resources were cleaned up automatically EXPECT_EQ(EventLog::instance().count_events("Tracked(R1)::ctor"), 1); EXPECT_EQ(EventLog::instance().count_events("Tracked(R2)::ctor"), 1); @@ -146,26 +143,23 @@ TEST_F(RaiiExceptionSafetyTest, SmartPointers_AutomaticCleanup) class CompositeResource { public: - CompositeResource(bool fail_on_second) - : r1_(std::make_unique("R1")) - , r2_(nullptr) - , r3_(nullptr) + CompositeResource(bool fail_on_second) : r1_(std::make_unique("R1")), r2_(nullptr), r3_(nullptr) { EventLog::instance().record("CompositeResource::ctor - r1 initialized"); - + if (fail_on_second) { EventLog::instance().record("CompositeResource::ctor - throwing before r2"); throw std::runtime_error("Failed during construction"); } - + r2_ = std::make_unique("R2"); EventLog::instance().record("CompositeResource::ctor - r2 initialized"); - + r3_ = std::make_unique("R3"); EventLog::instance().record("CompositeResource::ctor - r3 initialized"); } - + ~CompositeResource() { EventLog::instance().record("CompositeResource::dtor"); @@ -188,34 +182,33 @@ class CompositeResource TEST_F(RaiiExceptionSafetyTest, PartialConstruction_MemberCleanup) { // Moderate: Members initialized before exception are automatically cleaned up - + try { - CompositeResource resource(true); // Throws after r1 but before r2 + CompositeResource resource(true); // Throws after r1 but before r2 FAIL() << "Should have thrown exception"; } catch (const std::runtime_error&) { // Exception caught } - + // Verify r1 was constructed and destroyed EXPECT_EQ(EventLog::instance().count_events("Tracked(R1)::ctor"), 1); EXPECT_EQ(EventLog::instance().count_events("Tracked(R1)::dtor"), 1); - + // Verify r2 and r3 were never constructed EXPECT_EQ(EventLog::instance().count_events("Tracked(R2)::ctor"), 0); EXPECT_EQ(EventLog::instance().count_events("Tracked(R3)::ctor"), 0); - + // Verify CompositeResource destructor was NOT called EXPECT_EQ(EventLog::instance().count_events("CompositeResource::dtor"), 0); - + // Q: Why is the CompositeResource destructor not called? // A: // R: } - // ============================================================================ // Multiple Resources - Order of Cleanup // ============================================================================ @@ -223,12 +216,12 @@ TEST_F(RaiiExceptionSafetyTest, PartialConstruction_MemberCleanup) void operation_with_ordered_resources() { EventLog::instance().record("operation: start"); - + // TODO: Create resources in order: R1, R2, R3 using std::make_unique auto r1 = std::make_unique("R1"); auto r2 = std::make_unique("R2"); auto r3 = std::make_unique("R3"); - + EventLog::instance().record("operation: all resources acquired"); EventLog::instance().record("operation: throwing exception"); throw std::runtime_error("Simulated error"); @@ -245,7 +238,7 @@ void operation_with_ordered_resources() TEST_F(RaiiExceptionSafetyTest, MultipleResources_DestructionOrder) { // Moderate: Stack unwinding destroys objects in reverse construction order - + try { operation_with_ordered_resources(); @@ -255,26 +248,32 @@ TEST_F(RaiiExceptionSafetyTest, MultipleResources_DestructionOrder) { // Exception caught } - + // Verify construction order std::vector events = EventLog::instance().events(); size_t r1_ctor_idx = 0, r2_ctor_idx = 0, r3_ctor_idx = 0; size_t r1_dtor_idx = 0, r2_dtor_idx = 0, r3_dtor_idx = 0; - + for (size_t i = 0; i < events.size(); ++i) { - if (events[i].find("Tracked(R1)::ctor") != std::string::npos) r1_ctor_idx = i; - if (events[i].find("Tracked(R2)::ctor") != std::string::npos) r2_ctor_idx = i; - if (events[i].find("Tracked(R3)::ctor") != std::string::npos) r3_ctor_idx = i; - if (events[i].find("Tracked(R1)::dtor") != std::string::npos) r1_dtor_idx = i; - if (events[i].find("Tracked(R2)::dtor") != std::string::npos) r2_dtor_idx = i; - if (events[i].find("Tracked(R3)::dtor") != std::string::npos) r3_dtor_idx = i; + if (events[i].find("Tracked(R1)::ctor") != std::string::npos) + r1_ctor_idx = i; + if (events[i].find("Tracked(R2)::ctor") != std::string::npos) + r2_ctor_idx = i; + if (events[i].find("Tracked(R3)::ctor") != std::string::npos) + r3_ctor_idx = i; + if (events[i].find("Tracked(R1)::dtor") != std::string::npos) + r1_dtor_idx = i; + if (events[i].find("Tracked(R2)::dtor") != std::string::npos) + r2_dtor_idx = i; + if (events[i].find("Tracked(R3)::dtor") != std::string::npos) + r3_dtor_idx = i; } - + // Construction order: R1 -> R2 -> R3 EXPECT_LT(r1_ctor_idx, r2_ctor_idx); EXPECT_LT(r2_ctor_idx, r3_ctor_idx); - + // Destruction order: R3 -> R2 -> R1 (reverse) EXPECT_GT(r1_dtor_idx, r2_dtor_idx); EXPECT_GT(r2_dtor_idx, r3_dtor_idx); @@ -284,17 +283,14 @@ TEST_F(RaiiExceptionSafetyTest, MultipleResources_DestructionOrder) // Scope Guards - Generic RAII for Cleanup // ============================================================================ -template -class ScopeGuard +template class ScopeGuard { public: - explicit ScopeGuard(F&& cleanup) - : cleanup_(std::forward(cleanup)) - , active_(true) + explicit ScopeGuard(F&& cleanup) : cleanup_(std::forward(cleanup)), active_(true) { EventLog::instance().record("ScopeGuard::ctor"); } - + ~ScopeGuard() { if (active_) @@ -303,13 +299,13 @@ class ScopeGuard cleanup_(); } } - + void dismiss() { EventLog::instance().record("ScopeGuard::dismiss"); active_ = false; } - + ScopeGuard(const ScopeGuard&) = delete; ScopeGuard& operator=(const ScopeGuard&) = delete; @@ -318,8 +314,7 @@ class ScopeGuard bool active_; }; -template -ScopeGuard make_scope_guard(F&& cleanup) +template ScopeGuard make_scope_guard(F&& cleanup) { return ScopeGuard(std::forward(cleanup)); } @@ -335,16 +330,13 @@ ScopeGuard make_scope_guard(F&& cleanup) TEST_F(RaiiExceptionSafetyTest, ScopeGuard_CleanupOnException) { // Moderate: Scope guards execute cleanup even on exception - + try { EventLog::instance().record("operation: start"); - - auto guard = make_scope_guard([]() - { - EventLog::instance().record("cleanup: executed"); - }); - + + auto guard = make_scope_guard([]() { EventLog::instance().record("cleanup: executed"); }); + EventLog::instance().record("operation: throwing exception"); throw std::runtime_error("Simulated error"); } @@ -352,12 +344,11 @@ TEST_F(RaiiExceptionSafetyTest, ScopeGuard_CleanupOnException) { // Exception caught } - + EXPECT_EQ(EventLog::instance().count_events("ScopeGuard::dtor - executing cleanup"), 1); EXPECT_EQ(EventLog::instance().count_events("cleanup: executed"), 1); } - // ============================================================================ // Nested RAII - Multiple Levels of Cleanup // ============================================================================ @@ -366,20 +357,19 @@ class OuterResource { public: explicit OuterResource(const std::string& name, bool throw_after_inner) - : name_(name) - , inner_(std::make_unique("Inner_" + name)) + : name_(name), inner_(std::make_unique("Inner_" + name)) { EventLog::instance().record("OuterResource(" + name_ + ")::ctor - inner initialized"); - + if (throw_after_inner) { EventLog::instance().record("OuterResource(" + name_ + ")::ctor - throwing"); throw std::runtime_error("Outer construction failed"); } - + EventLog::instance().record("OuterResource(" + name_ + ")::ctor - complete"); } - + ~OuterResource() { EventLog::instance().record("OuterResource(" + name_ + ")::dtor"); @@ -397,27 +387,25 @@ class OuterResource TEST_F(RaiiExceptionSafetyTest, NestedRAII_InnerCleanup) { // Hard: Nested RAII objects are cleaned up even on partial construction - + try { - OuterResource outer("Outer", true); // Throws after inner_ initialized + OuterResource outer("Outer", true); // Throws after inner_ initialized FAIL() << "Should have thrown exception"; } catch (const std::runtime_error&) { // Exception caught } - + // Verify inner was constructed and destroyed EXPECT_EQ(EventLog::instance().count_events("Tracked(Inner_Outer)::ctor"), 1); EXPECT_EQ(EventLog::instance().count_events("Tracked(Inner_Outer)::dtor"), 1); - + // Verify outer destructor was NOT called (object never fully constructed) EXPECT_EQ(EventLog::instance().count_events("OuterResource(Outer)::dtor"), 0); - + // Q: What observable signal confirms that inner_ was cleaned up? // A: // R: } - - diff --git a/learning_memory/tests/test_alignment_cache_friendly.cpp b/learning_memory/tests/test_alignment_cache_friendly.cpp index cf8f74d..9ed4199 100644 --- a/learning_memory/tests/test_alignment_cache_friendly.cpp +++ b/learning_memory/tests/test_alignment_cache_friendly.cpp @@ -3,12 +3,13 @@ // Difficulty: Hard #include "instrumentation.h" -#include -#include + +#include #include #include +#include +#include #include -#include class AlignmentCacheFriendlyTest : public ::testing::Test { @@ -42,22 +43,22 @@ TEST_F(AlignmentCacheFriendlyTest, AlignmentAndPadding) // Q: What is the sizeof(UnalignedData)? // A: // R: - + // Q: What is the sizeof(AlignedData)? // A: // R: - + EXPECT_EQ(sizeof(UnalignedData), 12); EXPECT_EQ(sizeof(AlignedData), 8); - + // Q: Why is UnalignedData larger despite having the same members? // A: // R: - + // Q: What is the alignment requirement of int on most platforms? // A: // R: - + EXPECT_EQ(alignof(int), 4); EXPECT_EQ(alignof(UnalignedData), 4); EXPECT_EQ(alignof(AlignedData), 4); @@ -70,7 +71,7 @@ TEST_F(AlignmentCacheFriendlyTest, AlignmentAndPadding) struct CacheLineAligned { alignas(64) int data; - + // Q: What does alignas(64) guarantee? // A: // R: @@ -85,25 +86,25 @@ TEST_F(AlignmentCacheFriendlyTest, CustomAlignment) { CacheLineAligned obj1; CacheLineAligned obj2; - + // Q: What is the sizeof(CacheLineAligned)? // A: // R: - + EXPECT_EQ(sizeof(CacheLineAligned), 64); EXPECT_EQ(alignof(CacheLineAligned), 64); - + // Q: Why might we align data to 64 bytes? // A: // R: - + // Q: What is the sizeof(OverAligned)? // A: // R: - + EXPECT_EQ(sizeof(OverAligned), 128); EXPECT_EQ(alignof(OverAligned), 128); - + // Q: What is the memory cost of over-alignment? // A: // R: @@ -125,7 +126,7 @@ class Points_AoS { points_.push_back({x, y, z}); } - + float sum_x() const { float sum = 0.0f; @@ -135,12 +136,12 @@ class Points_AoS } return sum; } - + size_t size() const { return points_.size(); } - + private: std::vector points_; }; @@ -154,7 +155,7 @@ class Points_SoA y_.push_back(y); z_.push_back(z); } - + float sum_x() const { float sum = 0.0f; @@ -164,12 +165,12 @@ class Points_SoA } return sum; } - + size_t size() const { return x_.size(); } - + private: std::vector x_; std::vector y_; @@ -180,33 +181,33 @@ TEST_F(AlignmentCacheFriendlyTest, AoS_vs_SoA_Structure) { Points_AoS aos; Points_SoA soa; - + // Q: What is the memory layout difference between AoS and SoA? // A: // R: - + for (int i = 0; i < 100; ++i) { aos.add(i * 1.0f, i * 2.0f, i * 3.0f); soa.add(i * 1.0f, i * 2.0f, i * 3.0f); } - + EXPECT_EQ(aos.size(), 100); EXPECT_EQ(soa.size(), 100); - + // Q: When iterating to sum only x values, which layout is more cache-friendly? // A: // R: - + // Q: What is the cache miss pattern for AoS when accessing only x values? // A: // R: - + float aos_sum = aos.sum_x(); float soa_sum = soa.sum_x(); - + EXPECT_FLOAT_EQ(aos_sum, soa_sum); - + // Q: In what scenario would AoS be more cache-friendly than SoA? // A: // R: @@ -233,33 +234,31 @@ TEST_F(AlignmentCacheFriendlyTest, FalseSharing) // Q: What is false sharing? // A: // R: - + SharedCacheLine shared; shared.counter1 = 0; shared.counter2 = 0; - + // Q: What is the memory offset between counter1 and counter2 in SharedCacheLine? // A: // R: - - EXPECT_EQ(offsetof(SharedCacheLine, counter2) - offsetof(SharedCacheLine, counter1), - sizeof(int)); - + + EXPECT_EQ(offsetof(SharedCacheLine, counter2) - offsetof(SharedCacheLine, counter1), sizeof(int)); + SeparateCacheLines separate; separate.counter1 = 0; separate.counter2 = 0; - + // Q: What is the memory offset between counter1 and counter2 in SeparateCacheLines? // A: // R: - - EXPECT_EQ(offsetof(SeparateCacheLines, counter2) - offsetof(SeparateCacheLines, counter1), - 64); - + + EXPECT_EQ(offsetof(SeparateCacheLines, counter2) - offsetof(SeparateCacheLines, counter1), 64); + // Q: Why would separate cache lines improve performance in multi-threaded scenarios? // A: // R: - + // Q: What is the trade-off of using alignas(64) to prevent false sharing? // A: // R: @@ -274,24 +273,24 @@ TEST_F(AlignmentCacheFriendlyTest, AlignedAllocation) // Q: Does operator new guarantee alignment for over-aligned types? // A: // R: - + struct OverAligned { alignas(64) int data; }; - + // TODO: Allocate an OverAligned object OverAligned* obj = new OverAligned(); - + // Q: How can we verify that obj is properly aligned to 64 bytes? // A: // R: - + uintptr_t addr = reinterpret_cast(obj); EXPECT_EQ(addr % 64, 0); - + delete obj; - + // Q: In C++11/14, what happens if we use operator new for over-aligned types? // A: // R: diff --git a/learning_memory/tests/test_custom_allocators.cpp b/learning_memory/tests/test_custom_allocators.cpp index 8d0a76f..01c3331 100644 --- a/learning_memory/tests/test_custom_allocators.cpp +++ b/learning_memory/tests/test_custom_allocators.cpp @@ -3,10 +3,11 @@ // Difficulty: Hard #include "instrumentation.h" + #include #include -#include #include +#include class CustomAllocatorsTest : public ::testing::Test { @@ -24,42 +25,42 @@ class CustomAllocatorsTest : public ::testing::Test TEST_F(CustomAllocatorsTest, StdAllocatorBasics) { std::allocator alloc; - + // Q: What does std::allocator::allocate(n) return? // A: // R: - + // TODO: Allocate memory for 1 Tracked object Tracked* ptr = alloc.allocate(1); - + // Q: Is the object constructed at this point? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("::ctor"), 0); - + // Q: What is the difference between allocate() and construct()? // A: // R: - + // TODO: Construct the object using std::allocator_traits std::allocator_traits>::construct(alloc, ptr, "AllocatedObj"); - + EXPECT_EQ(EventLog::instance().count_events("::ctor"), 1); EXPECT_EQ(ptr->name(), "AllocatedObj"); - + // Q: In what order must we call destroy() and deallocate()? // A: // R: - + // TODO: Destroy the object std::allocator_traits>::destroy(alloc, ptr); - + EXPECT_EQ(EventLog::instance().count_events("::dtor"), 1); - + // TODO: Deallocate the memory alloc.deallocate(ptr, 1); - + // Q: What would happen if we deallocated before destroying? // A: // R: @@ -69,52 +70,46 @@ TEST_F(CustomAllocatorsTest, StdAllocatorBasics) // Scenario 2: Custom Logging Allocator (Moderate) // ============================================================================ -template -class LoggingAllocator +template class LoggingAllocator { public: using value_type = T; - + LoggingAllocator() noexcept = default; - - template - LoggingAllocator(const LoggingAllocator&) noexcept + + template LoggingAllocator(const LoggingAllocator&) noexcept { } - + T* allocate(std::size_t n) { // Q: Why do we multiply n by sizeof(T)? // A: // R: - + std::ostringstream oss; - oss << "LoggingAllocator::allocate(" << n << " objects, " - << (n * sizeof(T)) << " bytes)"; + oss << "LoggingAllocator::allocate(" << n << " objects, " << (n * sizeof(T)) << " bytes)"; EventLog::instance().record(oss.str()); - + return static_cast(::operator new(n * sizeof(T))); } - + void deallocate(T* ptr, std::size_t n) noexcept { std::ostringstream oss; - oss << "LoggingAllocator::deallocate(" << n << " objects, " - << (n * sizeof(T)) << " bytes)"; + oss << "LoggingAllocator::deallocate(" << n << " objects, " << (n * sizeof(T)) << " bytes)"; EventLog::instance().record(oss.str()); - + ::operator delete(ptr); } }; -template -bool operator==(const LoggingAllocator&, const LoggingAllocator&) +template bool operator==(const LoggingAllocator&, const LoggingAllocator&) { return true; } -template -bool operator!=(const LoggingAllocator&, const LoggingAllocator&) +template bool operator!=(const LoggingAllocator&, const LoggingAllocator&) { return false; } @@ -124,33 +119,33 @@ TEST_F(CustomAllocatorsTest, LoggingAllocatorWithVector) // Q: When does std::vector call allocate()? // A: // R: - + std::vector> vec; - + // Q: Has any allocation occurred yet? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("LoggingAllocator::allocate"), 0); - + // TODO: Add one element to trigger allocation vec.emplace_back("Vec1"); - + // Q: How many allocate() calls occurred, and for how many objects? // A: // R: - + EXPECT_GE(EventLog::instance().count_events("LoggingAllocator::allocate"), 1); EXPECT_EQ(EventLog::instance().count_events("::ctor"), 1); - + // TODO: Add more elements to trigger reallocation vec.emplace_back("Vec2"); vec.emplace_back("Vec3"); - + // Q: What happens to the old allocation when std::vector reallocates? // A: // R: - + // Q: Walk through the observable signals: how many allocate() and deallocate() calls? // A: // R: @@ -165,24 +160,24 @@ TEST_F(CustomAllocatorsTest, AllocatorRebinding) // Q: What is allocator rebinding, and why is it necessary? // A: // R: - + using NodeAllocator = typename std::allocator_traits>::rebind_alloc; - + // Q: What type is NodeAllocator? // A: // R: - + NodeAllocator node_alloc; Tracked* ptr = node_alloc.allocate(1); - + std::allocator_traits::construct(node_alloc, ptr, "ReboundObj"); - + EXPECT_EQ(ptr->name(), "ReboundObj"); - + // Q: Why would std::list> need to rebind to allocate list nodes? // A: // R: - + std::allocator_traits::destroy(node_alloc, ptr); node_alloc.deallocate(ptr, 1); } @@ -191,63 +186,57 @@ TEST_F(CustomAllocatorsTest, AllocatorRebinding) // Scenario 4: Stateful Allocator (Hard) // ============================================================================ -template -class CountingAllocator +template class CountingAllocator { public: using value_type = T; - + CountingAllocator() - : allocation_count_(std::make_shared(0)) - , deallocation_count_(std::make_shared(0)) + : allocation_count_(std::make_shared(0)), deallocation_count_(std::make_shared(0)) { } - - template + + template CountingAllocator(const CountingAllocator& other) noexcept - : allocation_count_(other.allocation_count_) - , deallocation_count_(other.deallocation_count_) + : allocation_count_(other.allocation_count_), deallocation_count_(other.deallocation_count_) { } - + T* allocate(std::size_t n) { (*allocation_count_)++; return static_cast(::operator new(n * sizeof(T))); } - + void deallocate(T* ptr, std::size_t n) noexcept { (*deallocation_count_)++; ::operator delete(ptr); } - + size_t allocation_count() const { return *allocation_count_; } - + size_t deallocation_count() const { return *deallocation_count_; } - - template - friend class CountingAllocator; - + + template friend class CountingAllocator; + private: std::shared_ptr allocation_count_; std::shared_ptr deallocation_count_; }; -template -bool operator==(const CountingAllocator& lhs, const CountingAllocator& rhs) +template bool operator==(const CountingAllocator& lhs, const CountingAllocator& rhs) { return lhs.allocation_count_ == rhs.allocation_count_; } -template -bool operator!=(const CountingAllocator& lhs, const CountingAllocator& rhs) +template bool operator!=(const CountingAllocator& lhs, const CountingAllocator& rhs) { return !(lhs == rhs); } @@ -255,35 +244,35 @@ bool operator!=(const CountingAllocator& lhs, const CountingAllocator& rhs TEST_F(CustomAllocatorsTest, StatefulAllocator) { CountingAllocator alloc; - + // Q: Why does CountingAllocator use shared_ptr for its counters? // A: // R: - + std::vector> vec(alloc); - + vec.push_back(1); vec.push_back(2); vec.push_back(3); vec.push_back(4); vec.push_back(5); - + // Q: How many allocations occurred during these 5 push_back calls? // A: // R: - + EXPECT_GE(alloc.allocation_count(), 1); - + // Q: Why might allocation_count be greater than 1? // A: // R: - + size_t alloc_count = alloc.allocation_count(); size_t dealloc_count = alloc.deallocation_count(); - + // Q: What is the relationship between allocation_count and deallocation_count before vec is destroyed? // A: // R: - + EXPECT_EQ(alloc_count - dealloc_count, 1); } diff --git a/learning_memory/tests/test_placement_new.cpp b/learning_memory/tests/test_placement_new.cpp index a35a58b..799a195 100644 --- a/learning_memory/tests/test_placement_new.cpp +++ b/learning_memory/tests/test_placement_new.cpp @@ -3,11 +3,12 @@ // Difficulty: Moderate #include "instrumentation.h" + +#include #include #include #include #include -#include class PlacementNewTest : public ::testing::Test { @@ -29,29 +30,29 @@ TEST_F(PlacementNewTest, BasicPlacementNew) // R: alignas(Tracked) char buffer[sizeof(Tracked)]; - + // Q: What does alignas(Tracked) ensure about the buffer? // A: // R: - + // Construct object in pre-allocated buffer Tracked* obj = new (buffer) Tracked("PlacementObj"); - + // Q: Where is the Tracked object's memory allocated? // A: // R: - + EXPECT_EQ(obj->name(), "PlacementObj"); EXPECT_EQ(EventLog::instance().count_events("::ctor"), 1); - + // Q: Why must we manually call the destructor here? // A: // R: - + obj->~Tracked(); - + EXPECT_EQ(EventLog::instance().count_events("::dtor"), 1); - + // Q: What happens if we forget to call the destructor manually? // A: // R: @@ -65,31 +66,31 @@ TEST_F(PlacementNewTest, MultipleObjectsInBuffer) { constexpr size_t buffer_size = sizeof(Tracked) * 3; alignas(Tracked) char buffer[buffer_size]; - + // TODO: Use placement new to construct 3 Tracked objects in the buffer // Hint: Calculate offset for each object Tracked* obj1 = new (buffer) Tracked("Obj1"); Tracked* obj2 = new (buffer + sizeof(Tracked)) Tracked("Obj2"); Tracked* obj3 = new (buffer + 2 * sizeof(Tracked)) Tracked("Obj3"); - + // Q: What is the relationship between the pointer arithmetic and object alignment? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("::ctor"), 3); EXPECT_EQ(obj1->name(), "Obj1"); EXPECT_EQ(obj2->name(), "Obj2"); EXPECT_EQ(obj3->name(), "Obj3"); - + // Q: In what order should we destroy these objects, and why does it matter? // A: // R: - + // TODO: Destroy the objects in reverse order of construction obj3->~Tracked(); obj2->~Tracked(); obj1->~Tracked(); - + EXPECT_EQ(EventLog::instance().count_events("::dtor"), 3); } @@ -102,30 +103,30 @@ TEST_F(PlacementNewTest, AlignedStorage) // Q: What is std::aligned_storage, and why would you use it instead of a char buffer? // A: // R: - + using Storage = typename std::aligned_storage::type; Storage storage; - + // Q: What guarantees does std::aligned_storage provide that char[] does not? // A: // R: - + Tracked* obj = new (&storage) Tracked("AlignedObj"); - + EXPECT_EQ(obj->name(), "AlignedObj"); EXPECT_EQ(EventLog::instance().count_events("::ctor"), 1); - + // Q: Can we reuse this storage after destroying the object? // A: // R: - + obj->~Tracked(); - + // Reuse the storage Tracked* obj2 = new (&storage) Tracked("ReusedObj"); EXPECT_EQ(obj2->name(), "ReusedObj"); EXPECT_EQ(EventLog::instance().count_events("::ctor"), 2); - + obj2->~Tracked(); } @@ -136,17 +137,16 @@ TEST_F(PlacementNewTest, AlignedStorage) class ManualLifetimeManager { public: - ManualLifetimeManager() - : obj_(nullptr) + ManualLifetimeManager() : obj_(nullptr) { } - + void construct(const std::string& name) { // TODO: Use placement new to construct a Tracked object in storage_ obj_ = new (&storage_) Tracked(name); } - + void destroy() { // TODO: Manually call destructor and set obj_ to nullptr @@ -156,29 +156,29 @@ class ManualLifetimeManager obj_ = nullptr; } } - + Tracked* get() { return obj_; } - + bool is_constructed() const { return obj_ != nullptr; } - + ~ManualLifetimeManager() { // Q: Why is it critical to check is_constructed() in the destructor? // A: // R: - + if (is_constructed()) { destroy(); } } - + private: using Storage = typename std::aligned_storage::type; Storage storage_; @@ -188,28 +188,28 @@ class ManualLifetimeManager TEST_F(PlacementNewTest, ManualLifetimeManagement) { ManualLifetimeManager manager; - + EXPECT_FALSE(manager.is_constructed()); - + // Q: What is the state of storage_ before construct() is called? // A: // R: - + manager.construct("ManagedObj"); - + EXPECT_TRUE(manager.is_constructed()); EXPECT_EQ(manager.get()->name(), "ManagedObj"); EXPECT_EQ(EventLog::instance().count_events("::ctor"), 1); - + // Q: What happens if we call construct() twice without calling destroy()? // A: // R: - + manager.destroy(); - + EXPECT_FALSE(manager.is_constructed()); EXPECT_EQ(EventLog::instance().count_events("::dtor"), 1); - + // Reuse the manager manager.construct("SecondObj"); EXPECT_EQ(manager.get()->name(), "SecondObj"); @@ -222,25 +222,24 @@ TEST_F(PlacementNewTest, ManualLifetimeManagement) class ThrowingTracked { public: - explicit ThrowingTracked(const std::string& name, bool should_throw) - : tracked_(name) + explicit ThrowingTracked(const std::string& name, bool should_throw) : tracked_(name) { if (should_throw) { throw std::runtime_error("Construction failed"); } } - + ~ThrowingTracked() { EventLog::instance().record("ThrowingTracked::dtor " + tracked_.name()); } - + std::string name() const { return tracked_.name(); } - + private: Tracked tracked_; }; @@ -248,14 +247,14 @@ class ThrowingTracked TEST_F(PlacementNewTest, PlacementNewWithExceptions) { alignas(ThrowingTracked) char buffer[sizeof(ThrowingTracked)]; - + // Q: What happens to the buffer if the constructor throws? // A: // R: - + ThrowingTracked* obj = nullptr; bool exception_caught = false; - + try { obj = new (buffer) ThrowingTracked("WillThrow", true); @@ -264,22 +263,22 @@ TEST_F(PlacementNewTest, PlacementNewWithExceptions) { exception_caught = true; } - + EXPECT_TRUE(exception_caught); EXPECT_EQ(obj, nullptr); - + // Q: Do we need to call the destructor if the constructor threw? // A: // R: - + // Q: Is the buffer still usable after the failed construction? // A: // R: - + // Reuse buffer after failed construction obj = new (buffer) ThrowingTracked("WillSucceed", false); EXPECT_EQ(obj->name(), "WillSucceed"); - + obj->~ThrowingTracked(); } @@ -290,27 +289,27 @@ TEST_F(PlacementNewTest, PlacementNewWithExceptions) TEST_F(PlacementNewTest, PlacementNewVsHeapAllocation) { EventLog::instance().clear(); - + // Heap allocation Tracked* heap_obj = new Tracked("HeapObj"); - + // Stack buffer with placement new alignas(Tracked) char buffer[sizeof(Tracked)]; Tracked* placement_obj = new (buffer) Tracked("PlacementObj"); - + // Q: What observable signal in EventLog distinguishes heap vs placement allocation? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("::ctor"), 2); - + // Q: Which object requires delete, and which requires manual destructor call? // A: // R: - + delete heap_obj; placement_obj->~Tracked(); - + // Q: What would happen if we called delete on placement_obj? // A: // R: diff --git a/learning_memory/tests/test_pool_allocators.cpp b/learning_memory/tests/test_pool_allocators.cpp index 2ecd24f..8282f1e 100644 --- a/learning_memory/tests/test_pool_allocators.cpp +++ b/learning_memory/tests/test_pool_allocators.cpp @@ -3,10 +3,11 @@ // Difficulty: Hard #include "instrumentation.h" + #include #include -#include #include +#include class PoolAllocatorsTest : public ::testing::Test { @@ -21,76 +22,71 @@ class PoolAllocatorsTest : public ::testing::Test // Fixed-Size Pool Allocator Implementation // ============================================================================ -template -class FixedSizePool +template class FixedSizePool { public: - FixedSizePool() - : next_free_(0) + FixedSizePool() : next_free_(0) { for (size_t i = 0; i < PoolSize; ++i) { free_list_[i] = true; } - - EventLog::instance().record("FixedSizePool::ctor (pool_size=" + - std::to_string(PoolSize) + ")"); + + EventLog::instance().record("FixedSizePool::ctor (pool_size=" + std::to_string(PoolSize) + ")"); } - + ~FixedSizePool() { EventLog::instance().record("FixedSizePool::dtor"); } - + T* allocate() { // Q: What happens if the pool is exhausted? // A: // R: - + for (size_t i = 0; i < PoolSize; ++i) { if (free_list_[i]) { free_list_[i] = false; - - EventLog::instance().record("FixedSizePool::allocate slot " + - std::to_string(i)); - + + EventLog::instance().record("FixedSizePool::allocate slot " + std::to_string(i)); + return reinterpret_cast(&storage_[i]); } } - + throw std::bad_alloc(); } - + void deallocate(T* ptr) { // Q: How do we determine which slot in the pool this pointer belongs to? // A: // R: - + char* pool_start = reinterpret_cast(&storage_[0]); char* ptr_addr = reinterpret_cast(ptr); - + ptrdiff_t offset = ptr_addr - pool_start; size_t slot = offset / sizeof(Storage); - + // Q: What would happen if ptr doesn't belong to this pool? // A: // R: - + if (slot >= PoolSize) { throw std::invalid_argument("Pointer not from this pool"); } - + free_list_[slot] = true; - - EventLog::instance().record("FixedSizePool::deallocate slot " + - std::to_string(slot)); + + EventLog::instance().record("FixedSizePool::deallocate slot " + std::to_string(slot)); } - + size_t available() const { size_t count = 0; @@ -103,7 +99,7 @@ class FixedSizePool } return count; } - + private: using Storage = typename std::aligned_storage::type; Storage storage_[PoolSize]; @@ -118,50 +114,50 @@ class FixedSizePool TEST_F(PoolAllocatorsTest, FixedSizePoolBasics) { FixedSizePool pool; - + EXPECT_EQ(pool.available(), 3); - + // Q: What is the advantage of a fixed-size pool over calling new/delete repeatedly? // A: // R: - + EventLog::instance().clear(); - + // TODO: Allocate memory for first object Tracked* obj1 = pool.allocate(); - + EXPECT_EQ(pool.available(), 2); EXPECT_EQ(EventLog::instance().count_events("FixedSizePool::allocate"), 1); - + // Q: Is obj1 constructed at this point? // A: // R: - + // TODO: Construct obj1 using placement new new (obj1) Tracked("PoolObj1"); - + EXPECT_EQ(EventLog::instance().count_events("::ctor"), 1); - + // Allocate and construct second object Tracked* obj2 = pool.allocate(); new (obj2) Tracked("PoolObj2"); - + EXPECT_EQ(pool.available(), 1); - + // Q: What happens when we destroy and deallocate obj1? // A: // R: - + obj1->~Tracked(); pool.deallocate(obj1); - + EXPECT_EQ(pool.available(), 2); EXPECT_EQ(EventLog::instance().count_events("FixedSizePool::deallocate"), 1); - + // Q: Can we reuse the slot that obj1 occupied? // A: // R: - + // Cleanup obj2->~Tracked(); pool.deallocate(obj2); @@ -174,19 +170,19 @@ TEST_F(PoolAllocatorsTest, FixedSizePoolBasics) TEST_F(PoolAllocatorsTest, PoolExhaustion) { FixedSizePool pool; - + Tracked* obj1 = pool.allocate(); new (obj1) Tracked("Obj1"); - + Tracked* obj2 = pool.allocate(); new (obj2) Tracked("Obj2"); - + EXPECT_EQ(pool.available(), 0); - + // Q: What exception is thrown when the pool is exhausted? // A: // R: - + // TODO: Try to allocate when pool is full bool exception_thrown = false; try @@ -198,23 +194,23 @@ TEST_F(PoolAllocatorsTest, PoolExhaustion) { exception_thrown = true; } - + EXPECT_TRUE(exception_thrown); - + // Q: After deallocating one object, can we allocate again? // A: // R: - + obj1->~Tracked(); pool.deallocate(obj1); - + EXPECT_EQ(pool.available(), 1); - + Tracked* obj3 = pool.allocate(); new (obj3) Tracked("Obj3"); - + EXPECT_EQ(pool.available(), 0); - + // Cleanup obj2->~Tracked(); pool.deallocate(obj2); @@ -229,49 +225,49 @@ TEST_F(PoolAllocatorsTest, PoolExhaustion) TEST_F(PoolAllocatorsTest, PoolReuseAndFragmentation) { FixedSizePool pool; - + // Allocate all slots Tracked* obj1 = pool.allocate(); new (obj1) Tracked("Obj1"); - + Tracked* obj2 = pool.allocate(); new (obj2) Tracked("Obj2"); - + Tracked* obj3 = pool.allocate(); new (obj3) Tracked("Obj3"); - + Tracked* obj4 = pool.allocate(); new (obj4) Tracked("Obj4"); - + EXPECT_EQ(pool.available(), 0); - + // Q: What is the memory layout of these 4 objects in the pool? // A: // R: - + // Deallocate obj2 (middle slot) obj2->~Tracked(); pool.deallocate(obj2); - + EXPECT_EQ(pool.available(), 1); - + // Q: Can we allocate a new object in the freed slot? // A: // R: - + Tracked* obj5 = pool.allocate(); new (obj5) Tracked("Obj5"); - + // Q: What is the relationship between obj2's address and obj5's address? // A: // R: - + EXPECT_EQ(obj2, obj5); - + // Q: What would happen if we tried to deallocate obj2 again? // A: // R: - + // Cleanup obj1->~Tracked(); pool.deallocate(obj1); diff --git a/learning_modern_cpp/tests/test_auto_type_deduction.cpp b/learning_modern_cpp/tests/test_auto_type_deduction.cpp index 8a86552..4410e10 100644 --- a/learning_modern_cpp/tests/test_auto_type_deduction.cpp +++ b/learning_modern_cpp/tests/test_auto_type_deduction.cpp @@ -4,11 +4,12 @@ // C++11/14 #include "instrumentation.h" + #include -#include #include #include #include +#include class AutoTypeDeductionTest : public ::testing::Test { @@ -28,25 +29,24 @@ TEST_F(AutoTypeDeductionTest, BasicAutoDeduction) // Q: What type is x? // A: // R: - + auto x = 42; static_assert(std::is_same::value, "x should be int"); - + // Q: What type is y? // A: // R: - + auto y = 3.14; static_assert(std::is_same::value, "y should be double"); - + // Q: What type is ptr? // A: // R: - + auto ptr = std::make_shared("Auto"); - static_assert(std::is_same>::value, - "ptr should be shared_ptr"); - + static_assert(std::is_same>::value, "ptr should be shared_ptr"); + EXPECT_EQ(ptr->name(), "Auto"); } @@ -58,32 +58,32 @@ TEST_F(AutoTypeDeductionTest, AutoWithReferences) { int value = 10; int& ref = value; - + // Q: What type is x (auto from reference)? // A: // R: - + auto x = ref; static_assert(std::is_same::value, "x should be int, not int&"); - + x = 20; - + // Q: What is the value of 'value' after x = 20? // A: // R: - + EXPECT_EQ(value, 10); EXPECT_EQ(x, 20); - + // Q: How do we preserve the reference with auto? // A: // R: - + auto& y = ref; static_assert(std::is_same::value, "y should be int&"); - + y = 30; - + EXPECT_EQ(value, 30); } @@ -94,33 +94,33 @@ TEST_F(AutoTypeDeductionTest, AutoWithReferences) TEST_F(AutoTypeDeductionTest, AutoWithConst) { const int const_value = 42; - + // Q: What type is x? // A: // R: - + auto x = const_value; static_assert(std::is_same::value, "x should be int, not const int"); - - x = 100; // This compiles - + + x = 100; // This compiles + // Q: Why does auto drop const? // A: // R: - + // Q: How do we preserve const with auto? // A: // R: - + const auto y = const_value; static_assert(std::is_same::value, "y should be const int"); - + // y = 100; // This would fail to compile - + // Q: What type is z? // A: // R: - + auto& z = const_value; static_assert(std::is_same::value, "z should be const int&"); } @@ -137,29 +137,29 @@ int global_func() TEST_F(AutoTypeDeductionTest, DecltypeBasics) { int x = 10; - + // Q: What is the difference between auto and decltype? // A: // R: - + decltype(x) y = 20; static_assert(std::is_same::value, "y should be int"); - + // Q: What type is result? // A: // R: - + decltype(global_func()) result = global_func(); static_assert(std::is_same::value, "result should be int"); - + // Q: What type is ref_type? // A: // R: - + int& ref = x; decltype(ref) ref_type = x; static_assert(std::is_same::value, "ref_type should be int&"); - + ref_type = 30; EXPECT_EQ(x, 30); } @@ -171,27 +171,27 @@ TEST_F(AutoTypeDeductionTest, DecltypeBasics) TEST_F(AutoTypeDeductionTest, DecltypeWithExpressions) { int x = 10; - + // Q: What type is decltype(x)? // A: // R: - + static_assert(std::is_same::value, "decltype(x) is int"); - + // Q: What type is decltype((x))? // A: // R: - + static_assert(std::is_same::value, "decltype((x)) is int&"); - + // Q: Why does adding parentheses change the deduced type? // A: // R: - + // Q: What type is decltype(x + 1)? // A: // R: - + static_assert(std::is_same::value, "decltype(x + 1) is int"); } @@ -205,49 +205,49 @@ TEST_F(AutoTypeDeductionTest, AutoWithIterators) vec.emplace_back("V1"); vec.emplace_back("V2"); vec.emplace_back("V3"); - + EventLog::instance().clear(); - + // Q: What is the advantage of auto for iterator types? // A: // R: - + // TODO: Use auto to iterate through vec for (auto it = vec.begin(); it != vec.end(); ++it) { EventLog::instance().record("Iterator: " + it->name()); } - + EXPECT_EQ(EventLog::instance().count_events("Iterator:"), 3); - + // Q: What type is elem in this range-based for loop? // A: // R: - + EventLog::instance().clear(); - + for (auto elem : vec) { EventLog::instance().record("Element: " + elem.name()); } - + // Q: How many copy constructions occurred in the range-based for loop? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("copy_ctor"), 3); - + // Q: How can we avoid the copies? // A: // R: - + EventLog::instance().clear(); - + for (const auto& elem : vec) { EventLog::instance().record("Ref: " + elem.name()); } - + EXPECT_EQ(EventLog::instance().count_events("copy_ctor"), 0); } @@ -265,14 +265,13 @@ TEST_F(AutoTypeDeductionTest, AutoReturnTypeDeduction) // Q: What is the return type of make_tracked? // A: // R: - + auto ptr = make_tracked("AutoReturn"); - - static_assert(std::is_same>::value, - "Should be shared_ptr"); - + + static_assert(std::is_same>::value, "Should be shared_ptr"); + EXPECT_EQ(ptr->name(), "AutoReturn"); - + // Q: What limitation does auto return type have with multiple return statements? // A: // R: diff --git a/learning_modern_cpp/tests/test_delegating_constructors.cpp b/learning_modern_cpp/tests/test_delegating_constructors.cpp index 11d146a..c81cf4a 100644 --- a/learning_modern_cpp/tests/test_delegating_constructors.cpp +++ b/learning_modern_cpp/tests/test_delegating_constructors.cpp @@ -4,6 +4,7 @@ // C++11 #include "instrumentation.h" + #include #include @@ -23,44 +24,39 @@ class DelegatingConstructorsTest : public ::testing::Test class Resource { public: - Resource(const std::string& name, int value) - : name_(name) - , value_(value) + Resource(const std::string& name, int value) : name_(name), value_(value) { - EventLog::instance().record("Resource::ctor(name=" + name_ + ", value=" + - std::to_string(value_) + ")"); + EventLog::instance().record("Resource::ctor(name=" + name_ + ", value=" + std::to_string(value_) + ")"); } - + // Q: What does this constructor delegate to? // A: // R: - - Resource(const std::string& name) - : Resource(name, 0) + + Resource(const std::string& name) : Resource(name, 0) { EventLog::instance().record("Resource::delegating_ctor(name=" + name_ + ")"); } - + // Q: What is the order of execution: delegated constructor first or delegating constructor first? // A: // R: - - Resource() - : Resource("default") + + Resource() : Resource("default") { EventLog::instance().record("Resource::default_ctor"); } - + std::string name() const { return name_; } - + int value() const { return value_; } - + private: std::string name_; int value_; @@ -69,35 +65,35 @@ class Resource TEST_F(DelegatingConstructorsTest, BasicDelegation) { Resource r1("test", 42); - + EXPECT_EQ(r1.name(), "test"); EXPECT_EQ(r1.value(), 42); EXPECT_EQ(EventLog::instance().count_events("Resource::ctor(name=test, value=42)"), 1); - + EventLog::instance().clear(); - + Resource r2("partial"); - + // Q: How many constructor calls appear in EventLog? // A: // R: - + EXPECT_EQ(r2.name(), "partial"); EXPECT_EQ(r2.value(), 0); EXPECT_EQ(EventLog::instance().count_events("Resource::ctor"), 1); EXPECT_EQ(EventLog::instance().count_events("delegating_ctor"), 1); - + EventLog::instance().clear(); - + Resource r3; - + EXPECT_EQ(r3.name(), "default"); EXPECT_EQ(r3.value(), 0); - + // Q: What is the delegation chain for the default constructor? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("default_ctor"), 1); } @@ -109,28 +105,26 @@ class TrackedContainer { public: explicit TrackedContainer(const std::string& name, int capacity) - : data_(std::make_shared(name)) - , capacity_(capacity) + : data_(std::make_shared(name)), capacity_(capacity) { EventLog::instance().record("TrackedContainer::main_ctor"); } - - explicit TrackedContainer(const std::string& name) - : TrackedContainer(name, 10) + + explicit TrackedContainer(const std::string& name) : TrackedContainer(name, 10) { EventLog::instance().record("TrackedContainer::delegating_ctor"); } - + std::string name() const { return data_->name(); } - + int capacity() const { return capacity_; } - + private: std::shared_ptr data_; int capacity_; @@ -139,22 +133,22 @@ class TrackedContainer TEST_F(DelegatingConstructorsTest, DelegationWithTracked) { TrackedContainer tc("Container"); - + // Q: How many Tracked objects were constructed? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("Tracked(Container)::ctor"), 1); - + // Q: In what order do the EventLog entries appear? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("main_ctor"), 1); EXPECT_EQ(EventLog::instance().count_events("delegating_ctor"), 1); - + auto events = EventLog::instance().events(); - + // Q: Which event comes first in the log? // A: // R: @@ -167,30 +161,26 @@ TEST_F(DelegatingConstructorsTest, DelegationWithTracked) class Base { public: - explicit Base(const std::string& name) - : name_(name) + explicit Base(const std::string& name) : name_(name) { EventLog::instance().record("Base::ctor(name=" + name_ + ")"); } - - Base(const std::string& name, int value) - : name_(name) - , value_(value) + + Base(const std::string& name, int value) : name_(name), value_(value) { - EventLog::instance().record("Base::ctor(name=" + name_ + ", value=" + - std::to_string(value_) + ")"); + EventLog::instance().record("Base::ctor(name=" + name_ + ", value=" + std::to_string(value_) + ")"); } - + std::string name() const { return name_; } - + int value() const { return value_; } - + protected: std::string name_; int value_ = 0; @@ -202,9 +192,9 @@ class Derived : public Base // Q: What does 'using Base::Base' do? // A: // R: - + using Base::Base; - + void extra_method() { EventLog::instance().record("Derived::extra_method"); @@ -216,21 +206,21 @@ TEST_F(DelegatingConstructorsTest, InheritingConstructors) // Q: Which constructor is called? // A: // R: - + Derived d1("inherited"); - + EXPECT_EQ(d1.name(), "inherited"); EXPECT_EQ(d1.value(), 0); EXPECT_EQ(EventLog::instance().count_events("Base::ctor(name=inherited)"), 1); - + EventLog::instance().clear(); - + Derived d2("inherited", 42); - + EXPECT_EQ(d2.name(), "inherited"); EXPECT_EQ(d2.value(), 42); EXPECT_EQ(EventLog::instance().count_events("Base::ctor(name=inherited, value=42)"), 1); - + // Q: What constructors does Derived have after 'using Base::Base'? // A: // R: diff --git a/learning_modern_cpp/tests/test_if_constexpr.cpp b/learning_modern_cpp/tests/test_if_constexpr.cpp index b7bd0cb..0a96653 100644 --- a/learning_modern_cpp/tests/test_if_constexpr.cpp +++ b/learning_modern_cpp/tests/test_if_constexpr.cpp @@ -2,11 +2,11 @@ // Estimated Time: 3 hours // Difficulty: Moderate - #include "instrumentation.h" + #include -#include #include +#include #include class IfConstexprTest : public ::testing::Test @@ -22,13 +22,12 @@ class IfConstexprTest : public ::testing::Test // Scenario 1: if constexpr Basics (Moderate) // ============================================================================ -template -std::string describe_type(T value) +template std::string describe_type(T value) { // Q: What is the difference between if constexpr and regular if? // A: // R: - + if constexpr (std::is_integral_v) { return "integral: " + std::to_string(value); @@ -48,16 +47,16 @@ TEST_F(IfConstexprTest, BasicIfConstexpr) // Q: When is the condition in if constexpr evaluated? // A: // R: - + auto result_int = describe_type(42); EXPECT_EQ(result_int, "integral: 42"); - + auto result_double = describe_type(3.14); EXPECT_EQ(result_double, "floating: 3.140000"); - + auto result_string = describe_type(std::string("test")); EXPECT_EQ(result_string, "other type"); - + // Q: What code is generated for describe_type? // A: // R: @@ -67,13 +66,12 @@ TEST_F(IfConstexprTest, BasicIfConstexpr) // Scenario 2: if constexpr for Type-Specific Behavior (Hard) // ============================================================================ -template -void process_value(const T& value) +template void process_value(const T& value) { // Q: Why does this compile even though not all branches are valid for all types? // A: // R: - + if constexpr (std::is_pointer_v) { EventLog::instance().record("Processing pointer"); @@ -96,16 +94,16 @@ TEST_F(IfConstexprTest, IfConstexprTypeSpecific) { int* ptr = nullptr; process_value(ptr); - + EXPECT_EQ(EventLog::instance().count_events("Processing pointer"), 1); - + EventLog::instance().clear(); - + int value = 42; process_value(value); - + EXPECT_EQ(EventLog::instance().count_events("Processing integral"), 1); - + // Q: What advantage does if constexpr have over function overloading? // A: // R: @@ -115,23 +113,21 @@ TEST_F(IfConstexprTest, IfConstexprTypeSpecific) // Scenario 3: Fold Expressions - Unary Folds (Moderate) // ============================================================================ -template -auto sum(Args... args) +template auto sum(Args... args) { // Q: What does (args + ...) do? // A: // R: - + return (args + ...); } -template -void log_all(Args... args) +template void log_all(Args... args) { // Q: What does (EventLog::instance().record(args), ...) do? // A: // R: - + (EventLog::instance().record(std::to_string(args)), ...); } @@ -140,15 +136,15 @@ TEST_F(IfConstexprTest, UnaryFoldExpressions) // Q: How does the fold expression expand? // A: // R: - + auto result = sum(1, 2, 3, 4, 5); EXPECT_EQ(result, 15); - + auto result2 = sum(10, 20); EXPECT_EQ(result2, 30); - + log_all(1, 2, 3); - + EXPECT_EQ(EventLog::instance().count_events("1"), 1); EXPECT_EQ(EventLog::instance().count_events("2"), 1); EXPECT_EQ(EventLog::instance().count_events("3"), 1); @@ -158,23 +154,21 @@ TEST_F(IfConstexprTest, UnaryFoldExpressions) // Scenario 4: Fold Expressions - Binary Folds (Hard) // ============================================================================ -template -auto sum_with_init(Args... args) +template auto sum_with_init(Args... args) { // Q: What does (0 + ... + args) do? // A: // R: - + return (0 + ... + args); } -template -bool all_positive(Args... args) +template bool all_positive(Args... args) { // Q: What does (... && (args > 0)) do? // A: // R: - + return (... && (args > 0)); } @@ -182,14 +176,14 @@ TEST_F(IfConstexprTest, BinaryFoldExpressions) { auto result = sum_with_init(1, 2, 3); EXPECT_EQ(result, 6); - + auto empty = sum_with_init(); EXPECT_EQ(empty, 0); - + // Q: Why is the initial value important for empty parameter packs? // A: // R: - + EXPECT_TRUE(all_positive(1, 2, 3, 4)); EXPECT_FALSE(all_positive(1, -2, 3)); EXPECT_TRUE(all_positive(5)); @@ -199,8 +193,7 @@ TEST_F(IfConstexprTest, BinaryFoldExpressions) // Scenario 5: Compile-Time Branching with Tracked (Hard) // ============================================================================ -template -auto make_container(const std::string& name) +template auto make_container(const std::string& name) { if constexpr (std::is_same_v) { @@ -224,17 +217,17 @@ TEST_F(IfConstexprTest, CompileTimeBranchingWithTracked) // Q: How many instantiations of make_container exist? // A: // R: - + auto tracked = make_container("Direct"); EXPECT_EQ(tracked.name(), "Direct"); EXPECT_EQ(EventLog::instance().count_events("creating Tracked"), 1); - + EventLog::instance().clear(); - + auto ptr = make_container>("Shared"); EXPECT_EQ(ptr->name(), "Shared"); EXPECT_EQ(EventLog::instance().count_events("creating shared_ptr"), 1); - + // Q: What branches are compiled for make_container? // A: // R: @@ -244,27 +237,26 @@ TEST_F(IfConstexprTest, CompileTimeBranchingWithTracked) // Scenario 6: Variadic Fold with Tracked (Hard) // ============================================================================ -template -void construct_all(std::vector& vec, Args&&... args) +template void construct_all(std::vector& vec, Args&&... args) { // Q: What does (vec.emplace_back(std::forward(args)), ...) do? // A: // R: - + (vec.emplace_back(std::forward(args)), ...); } TEST_F(IfConstexprTest, VariadicFoldWithTracked) { std::vector vec; - + construct_all(vec, "First", "Second", "Third"); - + EXPECT_EQ(vec.size(), 3); EXPECT_EQ(vec[0].name(), "First"); EXPECT_EQ(vec[1].name(), "Second"); EXPECT_EQ(vec[2].name(), "Third"); - + // Q: How many Tracked constructions occurred? // A: // R: diff --git a/learning_modern_cpp/tests/test_lambdas.cpp b/learning_modern_cpp/tests/test_lambdas.cpp index bef1ee0..62913cf 100644 --- a/learning_modern_cpp/tests/test_lambdas.cpp +++ b/learning_modern_cpp/tests/test_lambdas.cpp @@ -4,11 +4,12 @@ // C++11/14 #include "instrumentation.h" -#include -#include + #include #include +#include #include +#include class LambdasTest : public ::testing::Test { @@ -28,24 +29,24 @@ TEST_F(LambdasTest, BasicLambdaSyntax) // Q: What is a lambda expression in C++? // A: // R: - + auto simple_lambda = []() { return 42; }; - + // Q: What is the type of simple_lambda? // A: // R: - + EXPECT_EQ(simple_lambda(), 42); - + // Lambda with parameters auto add = [](int a, int b) { return a + b; }; - + EXPECT_EQ(add(3, 4), 7); - + // Q: Can two lambdas with identical signatures be assigned to the same variable? // A: // R: - + // auto another_add = [](int a, int b) { return a + b; }; // add = another_add; // This would fail to compile } @@ -58,25 +59,25 @@ TEST_F(LambdasTest, CaptureByValue) { int x = 10; int y = 20; - + // Q: What does [x, y] mean in the capture list? // A: // R: - + auto lambda = [x, y]() { return x + y; }; - + EXPECT_EQ(lambda(), 30); - + // Modify original variables x = 100; y = 200; - + // Q: What does lambda() return now? // A: // R: - + EXPECT_EQ(lambda(), 30); - + // Q: Why didn't the lambda see the modified values? // A: // R: @@ -89,36 +90,36 @@ TEST_F(LambdasTest, CaptureByValue) TEST_F(LambdasTest, CaptureByReference) { int counter = 0; - + // Q: What does [&counter] mean? // A: // R: - + auto increment = [&counter]() { counter++; }; - + increment(); increment(); increment(); - + EXPECT_EQ(counter, 3); - + // Q: What lifetime hazard exists with capture by reference? // A: // R: - + std::function dangerous_lambda; - + { int local_var = 42; - dangerous_lambda = [&local_var]() { + dangerous_lambda = [&local_var]() { EventLog::instance().record("Accessing local_var: " + std::to_string(local_var)); }; } - + // Q: What happens if we call dangerous_lambda() here? // A: // R: - + // DANGER: Uncommenting this would be undefined behavior // dangerous_lambda(); } @@ -132,36 +133,40 @@ TEST_F(LambdasTest, CaptureAll) int a = 1; int b = 2; int c = 3; - + // Q: What does [=] capture? // A: // R: - + auto lambda_value = [=]() { return a + b + c; }; - + EXPECT_EQ(lambda_value(), 6); - + // Q: What does [&] capture? // A: // R: - - auto lambda_ref = [&]() { a++; b++; c++; }; - + + auto lambda_ref = [&]() { + a++; + b++; + c++; + }; + lambda_ref(); - + EXPECT_EQ(a, 2); EXPECT_EQ(b, 3); EXPECT_EQ(c, 4); - + // Q: What does [=, &c] mean? // A: // R: - + int d = 10; auto mixed = [=, &c]() { c = a + b + d; }; - + mixed(); - + // a=2, b=3, d=10 -> c should be 15 EXPECT_EQ(c, 15); } @@ -173,31 +178,31 @@ TEST_F(LambdasTest, CaptureAll) TEST_F(LambdasTest, MutableLambdas) { int x = 5; - - auto immutable = [x]() { + + auto immutable = [x]() { // x++; // This would fail to compile - return x; + return x; }; - + // Q: Why can't we modify x inside a non-mutable lambda? // A: // R: - - auto mutable_lambda = [x]() mutable { - x++; - return x; + + auto mutable_lambda = [x]() mutable { + x++; + return x; }; - + EXPECT_EQ(mutable_lambda(), 6); EXPECT_EQ(mutable_lambda(), 7); EXPECT_EQ(mutable_lambda(), 8); - + // Q: What is the value of the original x? // A: // R: - + EXPECT_EQ(x, 5); - + // Q: Where is the lambda's copy of x stored? // A: // R: @@ -210,28 +215,28 @@ TEST_F(LambdasTest, MutableLambdas) TEST_F(LambdasTest, LambdasWithTrackedObjects) { std::shared_ptr ptr = std::make_shared("Lambda"); - + EventLog::instance().clear(); - + // Q: What happens to ptr's use_count when captured by value? // A: // R: - + auto lambda_by_value = [ptr]() { return ptr->name(); }; - + EXPECT_EQ(ptr.use_count(), 2); - + // Q: What observable signal in EventLog shows the capture? // A: // R: - + // Capture by reference auto lambda_by_ref = [&ptr]() { return ptr->name(); }; - + // Q: What is ptr's use_count with reference capture? // A: // R: - + EXPECT_EQ(ptr.use_count(), 2); } @@ -244,23 +249,22 @@ TEST_F(LambdasTest, GenericLambdas) // Q: What does auto in the parameter list enable? // A: // R: - + auto generic_add = [](auto a, auto b) { return a + b; }; - + EXPECT_EQ(generic_add(1, 2), 3); EXPECT_EQ(generic_add(1.5, 2.5), 4.0); - + // Q: How many different instantiations of this lambda exist? // A: // R: - + std::vector vec = {1, 2, 3, 4, 5}; - + // TODO: Use a generic lambda with std::transform to double each element std::vector doubled(vec.size()); - std::transform(vec.begin(), vec.end(), doubled.begin(), - [](auto x) { return x * 2; }); - + std::transform(vec.begin(), vec.end(), doubled.begin(), [](auto x) { return x * 2; }); + EXPECT_EQ(doubled[0], 2); EXPECT_EQ(doubled[4], 10); } @@ -269,8 +273,7 @@ TEST_F(LambdasTest, GenericLambdas) // Scenario 8: Lambda as Function Parameter (Hard) // ============================================================================ -template -void apply_to_tracked(Func&& func) +template void apply_to_tracked(Func&& func) { Tracked obj("FuncParam"); func(obj); @@ -279,33 +282,31 @@ void apply_to_tracked(Func&& func) TEST_F(LambdasTest, LambdaAsFunctionParameter) { int call_count = 0; - + // Q: What is the advantage of template parameter over std::function? // A: // R: - + apply_to_tracked([&call_count](Tracked& obj) { EventLog::instance().record("Lambda called with " + obj.name()); call_count++; }); - + EXPECT_EQ(call_count, 1); - + // Q: How many Tracked objects were constructed? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("::ctor"), 1); - + // Using std::function - std::function func = [&call_count](Tracked& obj) { - call_count++; - }; - + std::function func = [&call_count](Tracked& obj) { call_count++; }; + apply_to_tracked(func); - + EXPECT_EQ(call_count, 2); - + // Q: What is the performance cost of std::function compared to template parameter? // A: // R: diff --git a/learning_modern_cpp/tests/test_optional_variant_any.cpp b/learning_modern_cpp/tests/test_optional_variant_any.cpp index 59f5b19..a7119ae 100644 --- a/learning_modern_cpp/tests/test_optional_variant_any.cpp +++ b/learning_modern_cpp/tests/test_optional_variant_any.cpp @@ -2,14 +2,14 @@ // Estimated Time: 3 hours // Difficulty: Moderate - #include "instrumentation.h" + +#include #include +#include #include -#include -#include #include -#include +#include class OptionalVariantAnyTest : public ::testing::Test { @@ -38,29 +38,29 @@ TEST_F(OptionalVariantAnyTest, OptionalBasics) // Q: What does std::optional represent? // A: // R: - + auto result = find_value(true); - + // Q: How do we check if an optional has a value? // A: // R: - + EXPECT_TRUE(result.has_value()); EXPECT_EQ(*result, 42); EXPECT_EQ(result.value(), 42); - + auto empty = find_value(false); - + EXPECT_FALSE(empty.has_value()); - + // Q: What happens if we call value() on an empty optional? // A: // R: - + // Q: What is the difference between *opt and opt.value()? // A: // R: - + int default_val = empty.value_or(100); EXPECT_EQ(default_val, 100); } @@ -83,25 +83,25 @@ TEST_F(OptionalVariantAnyTest, OptionalWithTracked) // Q: How many Tracked objects are constructed when returning from make_optional_tracked? // A: // R: - + auto opt = make_optional_tracked(true); - + EXPECT_TRUE(opt.has_value()); EXPECT_EQ(opt->name(), "Optional"); - + // Q: What observable signal shows copy vs move construction? // A: // R: - + EventLog::instance().clear(); - + // TODO: Reset the optional opt.reset(); - + // Q: What happens to the Tracked object when reset() is called? // A: // R: - + EXPECT_FALSE(opt.has_value()); EXPECT_EQ(EventLog::instance().count_events("::dtor"), 1); } @@ -115,30 +115,30 @@ TEST_F(OptionalVariantAnyTest, VariantBasics) // Q: What is std::variant? // A: // R: - + std::variant var; - + // Q: What is the default value of var? // A: // R: - + EXPECT_EQ(var.index(), 0); EXPECT_EQ(std::get(var), 0); - + var = 3.14; - + // Q: What is var.index() now? // A: // R: - + EXPECT_EQ(var.index(), 1); EXPECT_EQ(std::get(var), 3.14); - + var = "hello"; - + EXPECT_EQ(var.index(), 2); EXPECT_EQ(std::get(var), "hello"); - + // Q: What happens if we call std::get(var) when var holds a string? // A: // R: @@ -151,11 +151,11 @@ TEST_F(OptionalVariantAnyTest, VariantBasics) TEST_F(OptionalVariantAnyTest, VariantWithVisit) { std::variant var = 42; - + // Q: What is std::visit? // A: // R: - + auto visitor = [](auto&& arg) { using T = std::decay_t; if constexpr (std::is_same_v) @@ -171,16 +171,16 @@ TEST_F(OptionalVariantAnyTest, VariantWithVisit) EventLog::instance().record("Visited string: " + arg); } }; - + std::visit(visitor, var); - + EXPECT_EQ(EventLog::instance().count_events("Visited int"), 1); - + var = 3.14; std::visit(visitor, var); - + EXPECT_EQ(EventLog::instance().count_events("Visited double"), 1); - + // Q: What advantage does std::visit have over std::get? // A: // R: @@ -195,27 +195,27 @@ TEST_F(OptionalVariantAnyTest, AnyBasics) // Q: What is the difference between std::variant and std::any? // A: // R: - + std::any a = 42; - + EXPECT_TRUE(a.has_value()); - + // Q: How do we extract the value from std::any? // A: // R: - + int value = std::any_cast(a); EXPECT_EQ(value, 42); - + a = std::string("hello"); - + // Q: What happens if we call std::any_cast(a) when a holds a string? // A: // R: - + std::string str = std::any_cast(a); EXPECT_EQ(str, "hello"); - + // Q: What is the performance cost of std::any compared to std::variant? // A: // R: @@ -228,30 +228,30 @@ TEST_F(OptionalVariantAnyTest, AnyBasics) TEST_F(OptionalVariantAnyTest, AnyWithTracked) { std::any a = Tracked("AnyTracked"); - + // Q: How many Tracked objects exist at this point? // A: // R: - + EXPECT_TRUE(a.has_value()); - + // TODO: Extract the Tracked object Tracked extracted = std::any_cast(a); - + // Q: How many copy operations occurred during any_cast? // A: // R: - + EXPECT_EQ(extracted.name(), "AnyTracked"); - + EventLog::instance().clear(); - + a.reset(); - + // Q: What happens to the Tracked object when any is reset? // A: // R: - + EXPECT_FALSE(a.has_value()); EXPECT_EQ(EventLog::instance().count_events("::dtor"), 1); } diff --git a/learning_modern_cpp/tests/test_string_view.cpp b/learning_modern_cpp/tests/test_string_view.cpp index 7b228fd..630f86c 100644 --- a/learning_modern_cpp/tests/test_string_view.cpp +++ b/learning_modern_cpp/tests/test_string_view.cpp @@ -2,11 +2,11 @@ // Estimated Time: 2 hours // Difficulty: Easy - #include "instrumentation.h" + #include -#include #include +#include #include class StringViewTest : public ::testing::Test @@ -27,23 +27,23 @@ TEST_F(StringViewTest, BasicStringView) // Q: What is std::string_view? // A: // R: - + std::string str = "Hello, World!"; std::string_view sv = str; - + EXPECT_EQ(sv.size(), 13); EXPECT_EQ(sv, "Hello, World!"); - + // Q: Does string_view own the string data? // A: // R: - + // Q: What happens if we modify str? // A: // R: - + str[0] = 'h'; - + EXPECT_EQ(sv[0], 'h'); } @@ -56,18 +56,18 @@ TEST_F(StringViewTest, StringViewFromLiterals) // Q: What is the advantage of string_view over const string& for string literals? // A: // R: - + std::string_view sv = "literal"; - + EXPECT_EQ(sv.size(), 7); - + // Q: Does creating string_view from a literal allocate memory? // A: // R: - + const char* literal = "another"; std::string_view sv2 = literal; - + EXPECT_EQ(sv2, "another"); } @@ -86,18 +86,18 @@ TEST_F(StringViewTest, LifetimeIssues) // Q: What is wrong with get_dangerous_view()? // A: // R: - + // DANGER: Uncommenting this creates undefined behavior // auto view = get_dangerous_view(); // std::string copy(view); // Undefined behavior - + // Q: What lifetime guarantee does string_view require? // A: // R: - + std::string safe_str = "safe"; std::string_view safe_view = safe_str; - + EXPECT_EQ(safe_view, "safe"); } @@ -109,24 +109,24 @@ TEST_F(StringViewTest, StringViewSubstrings) { std::string str = "Hello, World!"; std::string_view sv = str; - + // Q: Does substr() on string_view allocate memory? // A: // R: - + std::string_view sub = sv.substr(0, 5); - + EXPECT_EQ(sub, "Hello"); - + // Q: What does sub point to? // A: // R: - + EXPECT_EQ(sub.data(), str.data()); - + // TODO: Get substring "World" std::string_view world = sv.substr(7, 5); - + EXPECT_EQ(world, "World"); } @@ -147,21 +147,21 @@ void process_string_view(std::string_view sv) TEST_F(StringViewTest, StringViewPerformance) { std::string str = "performance test"; - + EventLog::instance().clear(); - + // Q: How many string copies occur when calling process_string_copy? // A: // R: - + process_string_copy(str); - + // Q: How many string copies occur when calling process_string_view? // A: // R: - + process_string_view(str); - + // Q: When should you prefer string_view over const string&? // A: // R: @@ -175,17 +175,17 @@ TEST_F(StringViewTest, StringViewModificationRestrictions) { std::string str = "mutable"; std::string_view sv = str; - + // Q: Can we modify the underlying string through string_view? // A: // R: - + // sv[0] = 'M'; // This would fail to compile - + str[0] = 'M'; - + EXPECT_EQ(sv, "Mutable"); - + // Q: What operations can string_view perform? // A: // R: diff --git a/learning_modern_cpp/tests/test_structured_bindings.cpp b/learning_modern_cpp/tests/test_structured_bindings.cpp index afc7fd3..eb24ca7 100644 --- a/learning_modern_cpp/tests/test_structured_bindings.cpp +++ b/learning_modern_cpp/tests/test_structured_bindings.cpp @@ -2,13 +2,13 @@ // Estimated Time: 2 hours // Difficulty: Easy - #include "instrumentation.h" + #include -#include #include -#include #include +#include +#include class StructuredBindingsTest : public ::testing::Test { @@ -33,23 +33,23 @@ TEST_F(StructuredBindingsTest, BindingsWithPairs) // Q: What does auto [a, b] do? // A: // R: - + auto [number, text] = get_pair(); - + EXPECT_EQ(number, 42); EXPECT_EQ(text, "answer"); - + // Q: What are the types of number and text? // A: // R: - + static_assert(std::is_same::value, "number is int"); static_assert(std::is_same::value, "text is string"); - + // Q: Are number and text references or copies? // A: // R: - + number = 100; auto [n2, t2] = get_pair(); EXPECT_EQ(n2, 42); @@ -62,23 +62,23 @@ TEST_F(StructuredBindingsTest, BindingsWithPairs) TEST_F(StructuredBindingsTest, BindingsWithReferences) { std::pair p{10, "test"}; - + // Q: What does auto& [a, b] do? // A: // R: - + auto& [num, str] = p; - + static_assert(std::is_same::value, "num is int"); static_assert(std::is_same::value, "str is string"); - + num = 20; str = "modified"; - + // Q: What are the values of p.first and p.second? // A: // R: - + EXPECT_EQ(p.first, 20); EXPECT_EQ(p.second, "modified"); } @@ -97,17 +97,17 @@ TEST_F(StructuredBindingsTest, BindingsWithTuples) // Q: How many variables can we bind from a tuple? // A: // R: - + auto [a, b, c] = get_tuple(); - + EXPECT_EQ(a, 1); EXPECT_EQ(b, 2.5); EXPECT_EQ(c, "three"); - + // Q: What happens if we try to bind fewer variables than tuple elements? // A: // R: - + // auto [x, y] = get_tuple(); // This would fail to compile } @@ -132,22 +132,22 @@ TEST_F(StructuredBindingsTest, BindingsWithStructs) // Q: What requirement must a struct meet for structured bindings? // A: // R: - + auto [x, y, z] = get_point(); - + EXPECT_EQ(x, 10); EXPECT_EQ(y, 20); EXPECT_EQ(z, 30); - + // Q: What is the order of binding: declaration order or initialization order? // A: // R: - + Point p{1, 2, 3}; auto& [px, py, pz] = p; - + px = 100; - + EXPECT_EQ(p.x, 100); } @@ -161,28 +161,28 @@ TEST_F(StructuredBindingsTest, BindingsWithMaps) scores["Alice"] = 95; scores["Bob"] = 87; scores["Charlie"] = 92; - + // Q: What does auto& [key, value] bind to in a map iteration? // A: // R: - + for (const auto& [name, score] : scores) { EventLog::instance().record("Score: " + name + " = " + std::to_string(score)); } - + EXPECT_EQ(EventLog::instance().count_events("Score:"), 3); - + // Q: Can we modify the key through structured bindings? // A: // R: - + for (auto& [name, score] : scores) { score += 5; // name = "modified"; // This would fail to compile } - + EXPECT_EQ(scores["Alice"], 100); } @@ -198,20 +198,20 @@ std::pair make_tracked_pair() TEST_F(StructuredBindingsTest, BindingsWithTrackedObjects) { EventLog::instance().clear(); - + // Q: How many Tracked objects are constructed in make_tracked_pair? // A: // R: - + auto [first, second] = make_tracked_pair(); - + // Q: How many copy or move operations occurred? // A: // R: - + EXPECT_EQ(first.name(), "First"); EXPECT_EQ(second.name(), "Second"); - + // Q: What observable signal shows whether copy or move was used? // A: // R: diff --git a/learning_modern_cpp/tests/test_uniform_initialization.cpp b/learning_modern_cpp/tests/test_uniform_initialization.cpp index e6d4e53..bd4bfbc 100644 --- a/learning_modern_cpp/tests/test_uniform_initialization.cpp +++ b/learning_modern_cpp/tests/test_uniform_initialization.cpp @@ -4,10 +4,11 @@ // C++11 #include "instrumentation.h" + #include -#include -#include #include +#include +#include class UniformInitializationTest : public ::testing::Test { @@ -27,30 +28,30 @@ TEST_F(UniformInitializationTest, BasicUniformInitialization) // Q: What is uniform initialization (brace initialization)? // A: // R: - + int x{42}; int y = {42}; int z(42); int w = 42; - + EXPECT_EQ(x, 42); EXPECT_EQ(y, 42); EXPECT_EQ(z, 42); EXPECT_EQ(w, 42); - + // Q: What advantage does {} have over () for initialization? // A: // R: - + std::vector vec{1, 2, 3, 4, 5}; - + EXPECT_EQ(vec.size(), 5); EXPECT_EQ(vec[0], 1); - + // Q: What would std::vector vec(5) create? // A: // R: - + std::vector vec2(5); EXPECT_EQ(vec2.size(), 5); EXPECT_EQ(vec2[0], 0); @@ -65,22 +66,22 @@ TEST_F(UniformInitializationTest, NarrowingConversions) // Q: What is a narrowing conversion? // A: // R: - + double d = 3.14; - + // This compiles (narrowing allowed) int x = d; EXPECT_EQ(x, 3); - + // Q: What happens with brace initialization and narrowing? // A: // R: - + // int y{d}; // This would fail to compile (narrowing prevented) - + int safe = static_cast(d); EXPECT_EQ(safe, 3); - + // Q: Why does C++11 prevent narrowing with {}? // A: // R: @@ -99,27 +100,26 @@ class Container { values_.push_back(val); } - EventLog::instance().record("Container::ctor with initializer_list (size=" + - std::to_string(list.size()) + ")"); + EventLog::instance().record("Container::ctor with initializer_list (size=" + std::to_string(list.size()) + ")"); } - + Container(int count, int value) { values_.resize(count, value); - EventLog::instance().record("Container::ctor with count=" + std::to_string(count) + + EventLog::instance().record("Container::ctor with count=" + std::to_string(count) + ", value=" + std::to_string(value)); } - + size_t size() const { return values_.size(); } - + int operator[](size_t idx) const { return values_[idx]; } - + private: std::vector values_; }; @@ -129,23 +129,23 @@ TEST_F(UniformInitializationTest, InitializerListPriority) // Q: Which constructor is called? // A: // R: - + Container c1{1, 2, 3, 4, 5}; - + EXPECT_EQ(c1.size(), 5); EXPECT_EQ(EventLog::instance().count_events("initializer_list"), 1); - + // Q: Which constructor is called here? // A: // R: - + EventLog::instance().clear(); Container c2(3, 10); - + EXPECT_EQ(c2.size(), 3); EXPECT_EQ(c2[0], 10); EXPECT_EQ(EventLog::instance().count_events("count="), 1); - + // Q: How would you call the count/value constructor using brace initialization? // A: // R: @@ -162,7 +162,7 @@ class Widget { EventLog::instance().record("Widget::default_ctor"); } - + explicit Widget(int value) { EventLog::instance().record("Widget::ctor(int=" + std::to_string(value) + ")"); @@ -174,19 +174,19 @@ TEST_F(UniformInitializationTest, MostVexingParse) // Q: What does this declare? // A: // R: - + // Widget w(); // This is a function declaration, not an object! - + // Q: How does uniform initialization solve this? // A: // R: - + Widget w1{}; - + EXPECT_EQ(EventLog::instance().count_events("default_ctor"), 1); - + Widget w2{42}; - + EXPECT_EQ(EventLog::instance().count_events("ctor(int="), 1); } @@ -206,23 +206,23 @@ TEST_F(UniformInitializationTest, AggregateInitialization) // Q: What is an aggregate type? // A: // R: - + Point p1{1, 2, 3}; - + EXPECT_EQ(p1.x, 1); EXPECT_EQ(p1.y, 2); EXPECT_EQ(p1.z, 3); - + // Q: What happens if we provide fewer initializers than members? // A: // R: - + Point p2{10, 20}; - + EXPECT_EQ(p2.x, 10); EXPECT_EQ(p2.y, 20); EXPECT_EQ(p2.z, 0); - + // Q: Can we use designated initializers in C++17? // A: // R: diff --git a/learning_move_semantics/tests/test_move_assignment.cpp b/learning_move_semantics/tests/test_move_assignment.cpp index 4a1cd2e..f3f17b4 100644 --- a/learning_move_semantics/tests/test_move_assignment.cpp +++ b/learning_move_semantics/tests/test_move_assignment.cpp @@ -1,4 +1,5 @@ #include "move_instrumentation.h" + #include #include @@ -15,65 +16,60 @@ TEST_F(MoveAssignmentTest, BasicMoveAssignment) { MoveTracked obj1("Source"); MoveTracked obj2("Destination"); - + EventLog::instance().clear(); - + obj2 = std::move(obj1); - + // Q: What EventLog entries confirm the move assignment and what happened to obj2's original resource? - // A: - // R: - - // Q: Move constructor initializes a new object; move assignment replaces an existing one. What resource management must move assignment handle that move constructor does not? - // A: - // R: - + // A: + // R: + + // Q: Move constructor initializes a new object; move assignment replaces an existing one. What resource management + // must move assignment handle that move constructor does not? A: R: + auto events = EventLog::instance().events(); size_t move_assign_count = EventLog::instance().count_events("move_assign"); - + EXPECT_EQ(move_assign_count, 1); } TEST_F(MoveAssignmentTest, SelfMoveAssignment) { MoveTracked obj("SelfMove"); - + EventLog::instance().clear(); - + obj = std::move(obj); - + // Q: What observable behavior results from self-move-assignment? What EventLog entries appear? - // A: - // R: - - // Q: If the move assignment operator lacked a self-assignment check and deleted its resource before stealing from 'other', what would happen when this == &other? - // A: - // R: - + // A: + // R: + + // Q: If the move assignment operator lacked a self-assignment check and deleted its resource before stealing from + // 'other', what would happen when this == &other? A: R: + auto events = EventLog::instance().events(); size_t move_assign_count = EventLog::instance().count_events("move_assign"); - + EXPECT_EQ(move_assign_count, 1); } class RuleOfFive { public: - explicit RuleOfFive(const std::string& name) - : tracked_(name) + explicit RuleOfFive(const std::string& name) : tracked_(name) { } - - RuleOfFive(const RuleOfFive& other) - : tracked_(other.tracked_) + + RuleOfFive(const RuleOfFive& other) : tracked_(other.tracked_) { } - - RuleOfFive(RuleOfFive&& other) noexcept - : tracked_(std::move(other.tracked_)) + + RuleOfFive(RuleOfFive&& other) noexcept : tracked_(std::move(other.tracked_)) { } - + RuleOfFive& operator=(const RuleOfFive& other) { if (this != &other) @@ -82,7 +78,7 @@ class RuleOfFive } return *this; } - + RuleOfFive& operator=(RuleOfFive&& other) noexcept { if (this != &other) @@ -91,16 +87,16 @@ class RuleOfFive } return *this; } - + ~RuleOfFive() { } - + std::string name() const { return tracked_.name(); } - + private: MoveTracked tracked_; }; @@ -111,19 +107,18 @@ TEST_F(MoveAssignmentTest, RuleOfFiveComplete) RuleOfFive obj2(obj1); RuleOfFive obj3(obj2); RuleOfFive obj4(std::move(obj1)); - + // Q: What EventLog entries confirm the copy and move operations? How many of each occurred? - // A: - // R: - - // Q: If you define a move constructor but not a move assignment operator, what happens when you attempt move assignment? - // A: - // R: - + // A: + // R: + + // Q: If you define a move constructor but not a move assignment operator, what happens when you attempt move + // assignment? A: R: + auto events = EventLog::instance().events(); size_t copy_ctor = EventLog::instance().count_events("copy_ctor"); size_t move_ctor = EventLog::instance().count_events("move_ctor"); - + EXPECT_EQ(copy_ctor, 2); EXPECT_EQ(move_ctor, 1); } @@ -133,46 +128,44 @@ TEST_F(MoveAssignmentTest, MoveAssignmentChain) MoveTracked obj1("First"); MoveTracked obj2("Second"); MoveTracked obj3("Third"); - + EventLog::instance().clear(); - + obj2 = std::move(obj1); obj3 = std::move(obj2); - + // Q: After the two move assignments, which objects are in moved-from state and what EventLog entries confirm this? - // A: - // R: - - // Q: If you attempted obj4 = std::move(obj1) after obj1 is already moved-from, what guarantees does the standard provide about this operation? - // A: - // R: - + // A: + // R: + + // Q: If you attempted obj4 = std::move(obj1) after obj1 is already moved-from, what guarantees does the standard + // provide about this operation? A: R: + auto events = EventLog::instance().events(); size_t move_assign_count = EventLog::instance().count_events("move_assign"); - + EXPECT_EQ(move_assign_count, 2); } TEST_F(MoveAssignmentTest, MoveFromTemporary) { MoveTracked obj("Target"); - + EventLog::instance().clear(); - + obj = MoveTracked("Temporary"); - + // Q: What EventLog entries appear from this assignment? Does std::move appear anywhere in the code? - // A: - // R: - - // Q: What value category is MoveTracked("Temporary") and why does this determine which assignment operator is called? - // A: - // R: - + // A: + // R: + + // Q: What value category is MoveTracked("Temporary") and why does this determine which assignment operator is + // called? A: R: + auto events = EventLog::instance().events(); size_t ctor_count = EventLog::instance().count_events("::ctor [id="); size_t move_assign_count = EventLog::instance().count_events("move_assign"); - + EXPECT_GE(ctor_count, 1); EXPECT_EQ(move_assign_count, 1); } @@ -181,42 +174,37 @@ TEST_F(MoveAssignmentTest, MoveAssignmentExceptionSafety) { MoveTracked obj1("Safe1"); MoveTracked obj2("Safe2"); - - // Q: Why should move assignment be marked noexcept and what observable consequence occurs in std::vector if it's not? - // A: - // R: - - // Q: If move assignment can throw, what fallback does std::vector use during reallocation and why does this impact performance? - // A: - // R: - + + // Q: Why should move assignment be marked noexcept and what observable consequence occurs in std::vector if it's + // not? A: R: + + // Q: If move assignment can throw, what fallback does std::vector use during reallocation and why does this impact + // performance? A: R: + EXPECT_TRUE(true); } class ResourceWrapper { public: - explicit ResourceWrapper(const std::string& name) - : resource_(new MoveTracked(name)) + explicit ResourceWrapper(const std::string& name) : resource_(new MoveTracked(name)) { } - + ~ResourceWrapper() { delete resource_; } - - ResourceWrapper(const ResourceWrapper& other) - : resource_(new MoveTracked(*other.resource_)) + + ResourceWrapper(const ResourceWrapper& other) : resource_(new MoveTracked(*other.resource_)) { } - - ResourceWrapper(ResourceWrapper&& other) noexcept - : resource_(other.resource_) + + ResourceWrapper(ResourceWrapper&& other) noexcept : resource_(other.resource_) { other.resource_ = nullptr; } - + ResourceWrapper& operator=(const ResourceWrapper& other) { if (this != &other) @@ -226,7 +214,7 @@ class ResourceWrapper } return *this; } - + ResourceWrapper& operator=(ResourceWrapper&& other) noexcept { if (this != &other) @@ -237,12 +225,12 @@ class ResourceWrapper } return *this; } - + std::string name() const { return resource_ ? resource_->name() : "null"; } - + private: MoveTracked* resource_; }; @@ -251,22 +239,21 @@ TEST_F(MoveAssignmentTest, RawPointerMoveSemantics) { ResourceWrapper wrapper1("Resource1"); ResourceWrapper wrapper2("Resource2"); - + EventLog::instance().clear(); - + wrapper2 = std::move(wrapper1); - + // Q: What EventLog entries confirm wrapper2's original resource was destroyed and wrapper1's pointer was nulled? - // A: - // R: - - // Q: If the move assignment operator did not null out other.resource_, what would happen when wrapper1's destructor runs? - // A: - // R: - + // A: + // R: + + // Q: If the move assignment operator did not null out other.resource_, what would happen when wrapper1's destructor + // runs? A: R: + auto events = EventLog::instance().events(); size_t dtor_count = EventLog::instance().count_events("::dtor"); - + EXPECT_EQ(dtor_count, 1); } @@ -274,22 +261,22 @@ TEST_F(MoveAssignmentTest, MovedFromStateAccess) { MoveTracked obj1("Original"); MoveTracked obj2(std::move(obj1)); - + // Q: What guarantees does the standard provide about calling obj1.name() after the move? What could it return? - // A: - // R: - + // A: + // R: + // Q: Which operations on moved-from obj1 are well-defined and which would be undefined behavior? - // A: - // R: - + // A: + // R: + obj1 = MoveTracked("Reassigned"); - + bool obj1_valid = !obj1.name().empty(); - + // Q: After reassignment, what EventLog entries confirm obj1 is no longer in a moved-from state? - // A: - // R: - + // A: + // R: + EXPECT_TRUE(obj1_valid); } diff --git a/learning_move_semantics/tests/test_move_only_types.cpp b/learning_move_semantics/tests/test_move_only_types.cpp index 31d7611..09df9b7 100644 --- a/learning_move_semantics/tests/test_move_only_types.cpp +++ b/learning_move_semantics/tests/test_move_only_types.cpp @@ -1,7 +1,8 @@ #include "move_instrumentation.h" + #include -#include #include +#include #include class MoveOnlyTypesTest : public ::testing::Test @@ -17,18 +18,17 @@ TEST_F(MoveOnlyTypesTest, UniquePtrBasics) { auto ptr1 = std::make_unique("Unique"); auto ptr2 = std::move(ptr1); - + // Q: Why is unique_ptr move-only and what would break if it were copyable? - // A: - // R: - - // Q: After the move, what does ptr1 contain and what EventLog entries confirm the MoveTracked object was not destroyed? - // A: - // R: - + // A: + // R: + + // Q: After the move, what does ptr1 contain and what EventLog entries confirm the MoveTracked object was not + // destroyed? A: R: + auto events = EventLog::instance().events(); size_t ctor_count = EventLog::instance().count_events("::ctor [id="); - + EXPECT_EQ(ctor_count, 1); } @@ -36,18 +36,18 @@ TEST_F(MoveOnlyTypesTest, MoveOnlyResourceClass) { Resource res1("MoveOnly"); Resource res2(std::move(res1)); - + // Q: What special member functions must be deleted or defaulted to make a class move-only? - // A: - // R: - + // A: + // R: + // Q: After the move, what operations on res1 are well-defined and what would be undefined behavior? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t move_ctor = EventLog::instance().count_events("move_ctor"); - + EXPECT_EQ(move_ctor, 1); } @@ -55,56 +55,56 @@ TEST_F(MoveOnlyTypesTest, MoveOnlyInContainer) { std::vector vec; Resource resource("InVector"); - + vec.push_back(std::move(resource)); - + // Q: What prevents push_back(resource) without std::move from compiling? - // A: - // R: - + // A: + // R: + // Q: When the vector resizes, what operation does it use to relocate move-only elements? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t move_ctor = EventLog::instance().count_events("move_ctor"); - + EXPECT_GE(move_ctor, 1); } Resource create_resource() { Resource res("Created"); - + // Q: Do you need std::move when returning a local variable? - // A: - // R: - + // A: + // R: + // Q: What is automatic move from local variables? - // A: - // R: - + // A: + // R: + return res; } TEST_F(MoveOnlyTypesTest, ReturnValueOptimization) { EventLog::instance().clear(); - + Resource result = create_resource(); - + // Q: What EventLog entries show how many constructor calls occurred? What optimization eliminated move operations? - // A: - // R: - + // A: + // R: + // Q: What conditions must be met for RVO to apply? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t ctor_count = EventLog::instance().count_events("::ctor [id="); size_t move_ctor = EventLog::instance().count_events("move_ctor"); - + EXPECT_EQ(ctor_count, 1); EXPECT_EQ(move_ctor, 0); } @@ -121,84 +121,83 @@ Resource create_conditional(bool condition) Resource res2("Branch2"); return res2; } - + // Q: Can RVO apply when there are multiple return paths? - // A: - // R: - + // A: + // R: + // Q: What happens to the non-returned object? - // A: - // R: + // A: + // R: } TEST_F(MoveOnlyTypesTest, ConditionalReturn) { EventLog::instance().clear(); - + Resource result = create_conditional(true); - + // Q: What EventLog entries show how many constructors and moves occurred? Can RVO apply with multiple return paths? - // A: - // R: - + // A: + // R: + // Q: What happens to the Resource object in the non-taken branch? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t ctor_count = EventLog::instance().count_events("::ctor [id="); - + EXPECT_EQ(ctor_count, 1); } Resource wrong_return_move() { Resource res("WrongMove"); - + // Q: Should you use std::move(res) when returning? - // A: - // R: - + // A: + // R: + // Q: How does std::move affect RVO? - // A: - // R: - + // A: + // R: + return std::move(res); } TEST_F(MoveOnlyTypesTest, ReturnMoveAntiPattern) { EventLog::instance().clear(); - + Resource result = wrong_return_move(); - + // Q: What EventLog entries show whether RVO was applied? How many move operations occurred? - // A: - // R: - + // A: + // R: + // Q: Why does `return std::move(local);` prevent RVO and what performance cost does this introduce? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t move_ctor = EventLog::instance().count_events("move_ctor"); - + EXPECT_EQ(move_ctor, 1); } struct MoveOnlyWrapper { - explicit MoveOnlyWrapper(const std::string& name) - : resource_(name) + explicit MoveOnlyWrapper(const std::string& name) : resource_(name) { } - + MoveOnlyWrapper(const MoveOnlyWrapper&) = delete; MoveOnlyWrapper& operator=(const MoveOnlyWrapper&) = delete; - + MoveOnlyWrapper(MoveOnlyWrapper&&) = default; MoveOnlyWrapper& operator=(MoveOnlyWrapper&&) = default; - + Resource resource_; }; @@ -206,18 +205,18 @@ TEST_F(MoveOnlyTypesTest, DefaultedMoveOperations) { MoveOnlyWrapper wrapper1("Wrapper1"); MoveOnlyWrapper wrapper2(std::move(wrapper1)); - + // Q: What does = default generate for the move constructor and what EventLog entries confirm the member-wise move? - // A: - // R: - + // A: + // R: + // Q: After the move, what state is wrapper1.resource_ in? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t move_ctor = EventLog::instance().count_events("move_ctor"); - + EXPECT_GE(move_ctor, 1); } @@ -225,17 +224,17 @@ TEST_F(MoveOnlyTypesTest, UniquePtrOwnershipTransfer) { auto ptr1 = std::make_unique("UniqueOwner"); auto ptr2 = std::move(ptr1); - + // Q: What is the value of ptr1 after the transfer and what happens if you dereference it? - // A: - // R: - + // A: + // R: + // Q: What EventLog entries confirm the MoveTracked object was not destroyed during the transfer? - // A: - // R: - + // A: + // R: + bool ptr1_is_null = (ptr1 == nullptr); - + EXPECT_TRUE(ptr1_is_null); } @@ -247,38 +246,38 @@ std::unique_ptr factory_pattern(const std::string& name) TEST_F(MoveOnlyTypesTest, FactoryWithUniquePtr) { EventLog::instance().clear(); - + auto ptr = factory_pattern("Factory"); - + // Q: What EventLog entries show how many moves occurred? Why is returning unique_ptr efficient? - // A: - // R: - + // A: + // R: + // Q: Why is the factory pattern common with unique_ptr and what ownership semantics does it establish? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t ctor_count = EventLog::instance().count_events("::ctor [id="); - + EXPECT_GE(ctor_count, 0); } TEST_F(MoveOnlyTypesTest, MoveOnlyInVector) { std::vector> vec; - + vec.push_back(std::make_unique("First")); vec.push_back(std::make_unique("Second")); - + // Q: What prevents vector> from being copyable? - // A: - // R: - + // A: + // R: + // Q: When you move a vector>, what happens to the unique_ptrs and their managed objects? - // A: - // R: - + // A: + // R: + size_t vec_size = vec.size(); EXPECT_GE(vec_size, 0); } @@ -286,50 +285,48 @@ TEST_F(MoveOnlyTypesTest, MoveOnlyInVector) TEST_F(MoveOnlyTypesTest, TemporaryMoveOnly) { std::vector vec; - + vec.push_back(Resource("Temporary")); - + // Q: Why is std::move not needed when pushing back a temporary? - // A: - // R: - + // A: + // R: + // Q: What value category is Resource("Temporary") and how does this enable move operations? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t ctor_count = EventLog::instance().count_events("::ctor [id="); - + EXPECT_GE(ctor_count, 1); } class MoveCounter { public: - MoveCounter() - : move_count_(0) + MoveCounter() : move_count_(0) { } - + MoveCounter(const MoveCounter&) = delete; MoveCounter& operator=(const MoveCounter&) = delete; - - MoveCounter(MoveCounter&& other) noexcept - : move_count_(other.move_count_ + 1) + + MoveCounter(MoveCounter&& other) noexcept : move_count_(other.move_count_ + 1) { } - + MoveCounter& operator=(MoveCounter&& other) noexcept { move_count_ = other.move_count_ + 1; return *this; } - + int move_count() const { return move_count_; } - + private: int move_count_; }; @@ -339,14 +336,14 @@ TEST_F(MoveOnlyTypesTest, TrackingMoveOperations) MoveCounter counter; MoveCounter c2(std::move(counter)); MoveCounter c3(std::move(c2)); - + // Q: What is c3.move_count() and how does the MoveCounter implementation track the number of moves? - // A: - // R: - + // A: + // R: + // Q: After multiple moves, which object holds the final state and what happened to the previous objects? - // A: - // R: - + // A: + // R: + EXPECT_TRUE(true); } diff --git a/learning_move_semantics/tests/test_perfect_forwarding.cpp b/learning_move_semantics/tests/test_perfect_forwarding.cpp index d17ab02..976a4d4 100644 --- a/learning_move_semantics/tests/test_perfect_forwarding.cpp +++ b/learning_move_semantics/tests/test_perfect_forwarding.cpp @@ -1,7 +1,8 @@ #include "move_instrumentation.h" + #include -#include #include +#include class PerfectForwardingTest : public ::testing::Test { @@ -12,72 +13,68 @@ class PerfectForwardingTest : public ::testing::Test } }; -template -void sink(T&& param) +template void sink(T&& param) { EventLog::instance().record("sink called"); } -template -void forward_to_sink(T&& param) +template void forward_to_sink(T&& param) { sink(std::forward(param)); - + // Q: What happens if you use sink(param) instead of sink(std::forward(param))? - // A: - // R: + // A: + // R: } TEST_F(PerfectForwardingTest, BasicForwarding) { MoveTracked obj("Forwarded"); - + forward_to_sink(obj); forward_to_sink(std::move(obj)); - + // Q: What does "perfect forwarding" preserve and how does T&& enable this? - // A: - // R: - + // A: + // R: + // Q: What would break if forward_to_sink used T& instead of T&&? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t sink_calls = EventLog::instance().count_events("sink called"); - + EXPECT_EQ(sink_calls, 2); } -template -void imperfect_forward(T&& param) +template void imperfect_forward(T&& param) { // Q: What is wrong with using std::move here instead of std::forward? - // A: - // R: - + // A: + // R: + sink(std::move(param)); } TEST_F(PerfectForwardingTest, ImperfectForwardingProblem) { MoveTracked obj("Imperfect"); - + imperfect_forward(obj); - + // Q: What happens to the lvalue obj when imperfect_forward uses std::move(param)? - // A: - // R: - + // A: + // R: + // Q: What observable failure would occur if you used obj after this call? - // A: - // R: - + // A: + // R: + EXPECT_TRUE(true); } -template -std::unique_ptr make_unique_impl(Args&&... args) +template std::unique_ptr make_unique_impl(Args&&... args) { return std::unique_ptr(new T(std::forward(args)...)); } @@ -85,193 +82,181 @@ std::unique_ptr make_unique_impl(Args&&... args) TEST_F(PerfectForwardingTest, VariadicForwarding) { auto ptr = make_unique_impl("Variadic"); - - // Q: How does Args&&... with std::forward(args)... preserve the value category of each argument independently? - // A: - // R: - + + // Q: How does Args&&... with std::forward(args)... preserve the value category of each argument + // independently? A: R: + // Q: If you passed both lvalues and rvalues to make_unique_impl, what would happen to each? - // A: - // R: - + // A: + // R: + EXPECT_TRUE(true); } -template -void universal_ref(T&& param) +template void universal_ref(T&& param) { // Q: Is T&& always an rvalue reference? - // A: - // R: - + // A: + // R: + // Q: What is a "universal reference" (forwarding reference)? - // A: - // R: - + // A: + // R: + // Q: When is T&& a universal reference vs rvalue reference? - // A: - // R: + // A: + // R: } TEST_F(PerfectForwardingTest, UniversalReferenceVsRvalueReference) { MoveTracked obj("Universal"); - - auto rvalue_only = [](MoveTracked&& param) - { - EventLog::instance().record("rvalue_only called"); - }; - + + auto rvalue_only = [](MoveTracked&& param) { EventLog::instance().record("rvalue_only called"); }; + // Q: Why does MoveTracked&& in rvalue_only reject lvalues while T&& in templates accepts them? - // A: - // R: - + // A: + // R: + rvalue_only(std::move(obj)); - + // Q: What is the difference between a deduced T&& and an explicit MoveTracked&&? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); EXPECT_GE(EventLog::instance().count_events("rvalue_only called"), 1); } -template -void type_deduction_forward(T&& param) +template void type_deduction_forward(T&& param) { // Q: If you call this with an lvalue, what is T deduced as? - // A: - // R: - + // A: + // R: + // Q: If you call this with an rvalue, what is T deduced as? - // A: - // R: - + // A: + // R: + // Q: After reference collapsing, what is T&& in each case? - // A: - // R: + // A: + // R: } TEST_F(PerfectForwardingTest, TypeDeductionRules) { MoveTracked obj("Deduction"); - + type_deduction_forward(obj); type_deduction_forward(std::move(obj)); - + // Q: When type_deduction_forward(obj) is called, what is T deduced as and what is T&& after reference collapsing? - // A: - // R: - - // Q: When type_deduction_forward(std::move(obj)) is called, what is T deduced as and what is T&& after reference collapsing? - // A: - // R: - + // A: + // R: + + // Q: When type_deduction_forward(std::move(obj)) is called, what is T deduced as and what is T&& after reference + // collapsing? A: R: + EXPECT_TRUE(true); } -template -class Wrapper +template class Wrapper { public: // Q: Is T&& in a class template a universal reference? - // A: - // R: - + // A: + // R: + void set(T&& value) { // Q: Is T&& here a universal reference? - // A: - // R: + // A: + // R: } - - template - void forward_set(U&& value) + + template void forward_set(U&& value) { // Q: Is U&& here a universal reference? - // A: - // R: + // A: + // R: } }; TEST_F(PerfectForwardingTest, UniversalReferenceContext) { // Q: Why is type deduction required for universal references? - // A: - // R: - + // A: + // R: + // Q: In what contexts do you see T&&? - // A: - // R: - + // A: + // R: + EXPECT_TRUE(true); } -template -void forward_multiple(Args&&... args) +template void forward_multiple(Args&&... args) { // TODO: Forward all args to a container // YOUR CODE HERE - + // Q: How does parameter pack expansion work with std::forward? - // A: - // R: - + // A: + // R: + // Q: Can you forward different types with different value categories? - // A: - // R: + // A: + // R: } TEST_F(PerfectForwardingTest, ParameterPackForwarding) { // TODO: Create multiple objects // YOUR CODE HERE - + // TODO: Call forward_multiple with mixed lvalues and rvalues // YOUR CODE HERE - + // Q: How does perfect forwarding handle heterogeneous arguments? - // A: - // R: - + // A: + // R: + EXPECT_TRUE(true); } -template -T&& forward_reference_collapsing(T&& param) +template T&& forward_reference_collapsing(T&& param) { // Q: What happens when T is deduced as MoveTracked&? - // A: - // R: - + // A: + // R: + // Q: What is the result of MoveTracked& && after reference collapsing? - // A: - // R: - + // A: + // R: + return std::forward(param); } TEST_F(PerfectForwardingTest, ReferenceCollapsing) { // Q: What are the four reference collapsing rules? - // A: - // R: - + // A: + // R: + // Q: Why do rvalue references collapse to lvalue references in some cases? - // A: - // R: - + // A: + // R: + EXPECT_TRUE(true); } struct EmplaceWrapper { - template - void emplace_back(Args&&... args) + template void emplace_back(Args&&... args) { items_.push_back(MoveTracked(std::forward(args)...)); } - + std::vector items_; }; @@ -279,45 +264,44 @@ TEST_F(PerfectForwardingTest, EmplaceBackPattern) { EmplaceWrapper wrapper; MoveTracked obj("Emplace"); - + wrapper.emplace_back(obj); wrapper.emplace_back("Direct"); - + // Q: What EventLog entries show the construction operations? How does emplace_back differ from push_back? - // A: - // R: - + // A: + // R: + // Q: When emplace_back("Direct") is called, how many MoveTracked objects are constructed? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t ctor_count = EventLog::instance().count_events("::ctor [id="); - + EXPECT_GE(ctor_count, 2); } -template -void deduce_and_forward(T&& param) +template void deduce_and_forward(T&& param) { // Q: After T is deduced, how does std::forward know the original value category? - // A: - // R: - + // A: + // R: + // Q: What information is encoded in the template parameter T? - // A: - // R: + // A: + // R: } TEST_F(PerfectForwardingTest, ForwardingMechanism) { // Q: How does std::forward differ from static_cast? - // A: - // R: - + // A: + // R: + // Q: Why can't we just use std::move everywhere? - // A: - // R: - + // A: + // R: + EXPECT_TRUE(true); } diff --git a/learning_move_semantics/tests/test_rvalue_references.cpp b/learning_move_semantics/tests/test_rvalue_references.cpp index fec7cdb..38eaa29 100644 --- a/learning_move_semantics/tests/test_rvalue_references.cpp +++ b/learning_move_semantics/tests/test_rvalue_references.cpp @@ -1,4 +1,5 @@ #include "move_instrumentation.h" + #include #include #include @@ -16,22 +17,22 @@ TEST_F(RvalueReferencesTest, LvalueVsRvalue) { MoveTracked obj("Lvalue"); std::vector vec; - + vec.push_back(obj); vec.push_back(MoveTracked("Rvalue")); - + // Q: What EventLog entries distinguish the push_back(obj) from push_back(MoveTracked("Rvalue"))? - // A: - // R: - + // A: + // R: + // Q: What property of obj versus MoveTracked("Rvalue") determines which constructor overload is selected? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t copy_ctor_count = EventLog::instance().count_events("copy_ctor"); size_t move_ctor_count = EventLog::instance().count_events("move_ctor"); - + EXPECT_EQ(copy_ctor_count, 1); EXPECT_GE(move_ctor_count, 1); } @@ -40,20 +41,20 @@ TEST_F(RvalueReferencesTest, StdMoveBasics) { MoveTracked obj1("Original"); MoveTracked obj2(std::move(obj1)); - + // Q: What does std::move(obj1) do and what state is obj1 in after the move constructor completes? - // A: - // R: - + // A: + // R: + // Q: What EventLog entry confirms a move occurred rather than a copy? - // A: - // R: - + // A: + // R: + bool obj1_moved = obj1.name().empty(); - + auto events = EventLog::instance().events(); size_t move_ctor_count = EventLog::instance().count_events("move_ctor"); - + EXPECT_TRUE(obj1_moved); EXPECT_EQ(move_ctor_count, 1); } @@ -63,24 +64,24 @@ TEST_F(RvalueReferencesTest, TemporaryLifetime) { const MoveTracked& ref = MoveTracked("Temp"); MoveTracked&& rref = MoveTracked("RTemp"); - + // Q: What EventLog entries show when each temporary was constructed and when will each be destroyed? - // A: - // R: - + // A: + // R: + // Q: If you removed the const from the lvalue reference, would the code compile? - // A: - // R: + // A: + // R: } - + // Q: At this point, what EventLog entries confirm both temporaries were destroyed? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t ctor_count = EventLog::instance().count_events("::ctor"); size_t dtor_count = EventLog::instance().count_events("::dtor"); - + EXPECT_GE(ctor_count, 1); EXPECT_GE(dtor_count, 1); } @@ -89,58 +90,52 @@ TEST_F(RvalueReferencesTest, MoveConstructorElision) { std::vector vec; vec.reserve(2); - + EventLog::instance().clear(); - + vec.emplace_back("First"); - + // Q: What EventLog entries appear from this emplace_back? How many MoveTracked objects were constructed? - // A: - // R: - + // A: + // R: + // Q: How does emplace_back differ from push_back in terms of construction location? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t ctor_count = EventLog::instance().count_events("::ctor [id="); size_t move_ctor_count = EventLog::instance().count_events("move_ctor"); - + EXPECT_EQ(ctor_count, 1); EXPECT_EQ(move_ctor_count, 0); } TEST_F(RvalueReferencesTest, RvalueReferenceFunctionOverloading) { - auto process_lvalue = [](MoveTracked& obj) - { - EventLog::instance().record("process_lvalue called"); - }; - - auto process_rvalue = [](MoveTracked&& obj) - { - EventLog::instance().record("process_rvalue called"); - }; - + auto process_lvalue = [](MoveTracked& obj) { EventLog::instance().record("process_lvalue called"); }; + + auto process_rvalue = [](MoveTracked&& obj) { EventLog::instance().record("process_rvalue called"); }; + MoveTracked obj("Overload"); - + EventLog::instance().clear(); - + process_lvalue(obj); process_rvalue(std::move(obj)); - + // Q: What determines which overload is selected and what EventLog entries confirm the selections? - // A: - // R: - + // A: + // R: + // Q: After process_rvalue(std::move(obj)), obj is still valid. What operations on obj would be well-defined? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t lvalue_calls = EventLog::instance().count_events("process_lvalue"); size_t rvalue_calls = EventLog::instance().count_events("process_rvalue"); - + EXPECT_EQ(lvalue_calls, 1); EXPECT_EQ(rvalue_calls, 1); } @@ -149,23 +144,23 @@ TEST_F(RvalueReferencesTest, MoveIntoContainer) { std::vector vec; MoveTracked obj("ToMove"); - + EventLog::instance().clear(); - + vec.push_back(std::move(obj)); - + // Q: What EventLog entries confirm zero copies and at least one move? - // A: - // R: - + // A: + // R: + // Q: For a type with expensive copy operations, what resource operations does move avoid? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t copy_count = EventLog::instance().count_events("copy_ctor"); size_t move_count = EventLog::instance().count_events("move_ctor"); - + EXPECT_EQ(copy_count, 0); EXPECT_GE(move_count, 1); } @@ -173,14 +168,14 @@ TEST_F(RvalueReferencesTest, MoveIntoContainer) TEST_F(RvalueReferencesTest, ValueCategoryInExpression) { MoveTracked obj("Value"); - + // Q: What are the value categories of: obj, std::move(obj), and MoveTracked("Temp")? - // A: - // R: - + // A: + // R: + // Q: Does std::move change obj's type or just cast it? What is the return type of std::move(obj)? - // A: - // R: - + // A: + // R: + EXPECT_TRUE(true); } diff --git a/learning_move_semantics/tests/test_std_move.cpp b/learning_move_semantics/tests/test_std_move.cpp index 8beabe4..3517a2c 100644 --- a/learning_move_semantics/tests/test_std_move.cpp +++ b/learning_move_semantics/tests/test_std_move.cpp @@ -1,4 +1,5 @@ #include "move_instrumentation.h" + #include #include #include @@ -15,61 +16,59 @@ class StdMoveTest : public ::testing::Test TEST_F(StdMoveTest, StdMoveCast) { MoveTracked obj("ToCast"); - + // Q: What does std::move do to obj and what type does it return? - // A: - // R: - + // A: + // R: + MoveTracked obj2(std::move(obj)); - + // Q: What EventLog entry confirms the move constructor was invoked? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t move_ctor = EventLog::instance().count_events("move_ctor"); - + EXPECT_EQ(move_ctor, 1); } -template -void forward_lvalue(T&& param) +template void forward_lvalue(T&& param) { // TODO: Forward param to another function using std::forward // YOUR CODE HERE - + // Q: If param binds to an lvalue, what does std::forward(param) return? - // A: - // R: - + // A: + // R: + // Q: If param binds to an rvalue, what does std::forward(param) return? - // A: - // R: + // A: + // R: } -template -void forward_rvalue(T&& param) +template void forward_rvalue(T&& param) { // TODO: Forward param using std::forward // YOUR CODE HERE - + // Q: Why do we use std::forward in template functions instead of std::move? - // A: - // R: + // A: + // R: } TEST_F(StdMoveTest, StdMoveVsStdForward) { MoveTracked obj("Test"); - + // Q: What is the key difference between std::move and std::forward in terms of what they preserve? - // A: - // R: - + // A: + // R: + // Q: In what context would using std::move instead of std::forward cause incorrect behavior? - // A: - // R: - + // A: + // R: + EXPECT_TRUE(true); } @@ -86,49 +85,48 @@ void consume_by_rvalue(MoveTracked&& obj) TEST_F(StdMoveTest, StdMoveInFunctionCall) { MoveTracked obj("ToConsume"); - + EventLog::instance().clear(); - + consume_by_value(std::move(obj)); - + // Q: What EventLog entries show the move operation? How many moves occurred? - // A: - // R: - + // A: + // R: + // Q: If you called consume_by_rvalue(std::move(obj)) instead, would there be a move constructor call? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t move_ctor = EventLog::instance().count_events("move_ctor"); - + EXPECT_EQ(move_ctor, 1); } TEST_F(StdMoveTest, StdMoveInReturn) { - auto create_object = []() -> MoveTracked - { + auto create_object = []() -> MoveTracked { MoveTracked local("Local"); return local; }; - + EventLog::instance().clear(); - + MoveTracked result = create_object(); - + // Q: What EventLog entries show how many copy/move operations occurred? What optimization eliminated them? - // A: - // R: - + // A: + // R: + // Q: If you changed the return to `return std::move(local);`, what would happen to RVO? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t copy_ctor = EventLog::instance().count_events("copy_ctor"); size_t move_ctor = EventLog::instance().count_events("move_ctor"); - + EXPECT_EQ(copy_ctor, 0); } @@ -136,23 +134,23 @@ TEST_F(StdMoveTest, StdMoveOnConst) { const MoveTracked obj("Const"); std::vector vec; - + EventLog::instance().clear(); - + vec.push_back(std::move(obj)); - + // Q: What EventLog entries show which constructor was called? Why wasn't the move constructor invoked? - // A: - // R: - + // A: + // R: + // Q: What type does std::move(obj) return when obj is const? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t copy_ctor = EventLog::instance().count_events("copy_ctor"); size_t move_ctor = EventLog::instance().count_events("move_ctor"); - + EXPECT_GE(copy_ctor, 1); EXPECT_EQ(move_ctor, 0); } @@ -161,45 +159,44 @@ TEST_F(StdMoveTest, RepeatedStdMove) { MoveTracked obj("Multi"); auto&& ref = std::move(std::move(std::move(obj))); - + // Q: Does calling std::move multiple times have any additional effect beyond the first call? - // A: - // R: - + // A: + // R: + // Q: What is the type of ref after the nested std::move calls? - // A: - // R: - + // A: + // R: + EXPECT_TRUE(true); } TEST_F(StdMoveTest, StdMoveWithReferenceCollapse) { MoveTracked obj("Reference"); - + // Q: What is MoveTracked&& & (rvalue reference to lvalue reference)? - // A: - // R: - + // A: + // R: + // Q: What is MoveTracked&& && (rvalue reference to rvalue reference)? - // A: - // R: - + // A: + // R: + // Q: What is MoveTracked& && (lvalue reference to rvalue reference)? - // A: - // R: - + // A: + // R: + EXPECT_TRUE(true); } -template -void wrapper_move(T&& param) +template void wrapper_move(T&& param) { std::vector vec; - + // TODO: Move param into vector (incorrect - always moves) // vec.push_back(std::move(param)); - + // TODO: Forward param into vector (correct - preserves value category) // vec.push_back(std::forward(param)); } @@ -207,21 +204,21 @@ void wrapper_move(T&& param) TEST_F(StdMoveTest, ForwardingAndValueCategory) { MoveTracked obj("Forward"); - + EventLog::instance().clear(); - + wrapper_move(obj); - + // Q: If wrapper_move uses std::move(param), what EventLog entries would show for an lvalue argument? - // A: - // R: - + // A: + // R: + // Q: If wrapper_move uses std::forward(param), what EventLog entries would show for an lvalue argument? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); - + EXPECT_GE(EventLog::instance().count_events("::ctor"), 0); } @@ -232,25 +229,25 @@ TEST_F(StdMoveTest, MoveSemanticOptimization) vec1.push_back(std::move(a)); vec1.push_back(std::move(b)); vec1.push_back(std::move(c)); - + std::vector vec2; MoveTracked x("X"), y("Y"), z("Z"); vec2.push_back(x); vec2.push_back(y); vec2.push_back(z); - + // Q: What EventLog entries distinguish the operations on vec1 versus vec2? - // A: - // R: - + // A: + // R: + // Q: For a type managing heap-allocated memory, what resource operations does move avoid compared to copy? - // A: - // R: - + // A: + // R: + auto events = EventLog::instance().events(); size_t copy_count = EventLog::instance().count_events("copy_ctor"); size_t move_count = EventLog::instance().count_events("move_ctor"); - + EXPECT_GE(copy_count, 3); EXPECT_GE(move_count, 3); } @@ -258,16 +255,16 @@ TEST_F(StdMoveTest, MoveSemanticOptimization) TEST_F(StdMoveTest, StdMoveWithUniquePtrAnalogy) { // Q: How is std::move similar to transferring unique_ptr ownership? - // A: - // R: - + // A: + // R: + // Q: After moving, why is the moved-from object still valid but unspecified? - // A: - // R: - + // A: + // R: + // Q: Can you compare a moved-from object for equality? - // A: - // R: - + // A: + // R: + EXPECT_TRUE(true); } diff --git a/learning_performance/tests/test_benchmarking.cpp b/learning_performance/tests/test_benchmarking.cpp index a326798..41ce0ff 100644 --- a/learning_performance/tests/test_benchmarking.cpp +++ b/learning_performance/tests/test_benchmarking.cpp @@ -2,14 +2,14 @@ // Estimated Time: 3 hours // Difficulty: Hard - -#include #include "instrumentation.h" -#include -#include + #include -#include +#include #include +#include +#include +#include class BenchmarkingTest : public ::testing::Test { @@ -309,8 +309,7 @@ TEST_F(BenchmarkingTest, BenchmarkStabilityAndOutliers) TEST_F(BenchmarkingTest, IterationCountSelection) { - auto measure_operation = [](int iterations) -> double - { + auto measure_operation = [](int iterations) -> double { std::vector data(1000); std::iota(data.begin(), data.end(), 0); diff --git a/learning_performance/tests/test_cache_friendly.cpp b/learning_performance/tests/test_cache_friendly.cpp index 6314aa5..70a5802 100644 --- a/learning_performance/tests/test_cache_friendly.cpp +++ b/learning_performance/tests/test_cache_friendly.cpp @@ -2,15 +2,15 @@ // Estimated Time: 3 hours // Difficulty: Moderate to Hard +#include "instrumentation.h" +#include +#include #include -#include "instrumentation.h" -#include #include -#include #include -#include #include +#include class CacheFriendlyTest : public ::testing::Test { @@ -42,13 +42,20 @@ struct ParticlesSoA void resize(size_t n) { - x.resize(n); y.resize(n); z.resize(n); - vx.resize(n); vy.resize(n); vz.resize(n); + x.resize(n); + y.resize(n); + z.resize(n); + vx.resize(n); + vy.resize(n); + vz.resize(n); mass.resize(n); id.resize(n); } - size_t size() const { return x.size(); } + size_t size() const + { + return x.size(); + } }; TEST_F(CacheFriendlyTest, ArrayOfStructsVsStructOfArrays) @@ -178,8 +185,7 @@ TEST_F(CacheFriendlyTest, LoopFusionOptimization) for (size_t i = 0; i < count; ++i) { - transforms[i] = {static_cast(i), static_cast(i * 2), - static_cast(i * 3), 1.0f}; + transforms[i] = {static_cast(i), static_cast(i * 2), static_cast(i * 3), 1.0f}; } auto start_separate = std::chrono::high_resolution_clock::now(); @@ -196,13 +202,12 @@ TEST_F(CacheFriendlyTest, LoopFusionOptimization) transforms[i].z *= transforms[i].scale; } auto end_separate = std::chrono::high_resolution_clock::now(); - auto separate_duration = std::chrono::duration_cast( - end_separate - start_separate).count(); + auto separate_duration = + std::chrono::duration_cast(end_separate - start_separate).count(); for (size_t i = 0; i < count; ++i) { - transforms[i] = {static_cast(i), static_cast(i * 2), - static_cast(i * 3), 1.0f}; + transforms[i] = {static_cast(i), static_cast(i * 2), static_cast(i * 3), 1.0f}; } auto start_fused = std::chrono::high_resolution_clock::now(); @@ -213,8 +218,7 @@ TEST_F(CacheFriendlyTest, LoopFusionOptimization) transforms[i].z *= transforms[i].scale; } auto end_fused = std::chrono::high_resolution_clock::now(); - auto fused_duration = std::chrono::duration_cast( - end_fused - start_fused).count(); + auto fused_duration = std::chrono::duration_cast(end_fused - start_fused).count(); EventLog::instance().record("Separate loops: " + std::to_string(separate_duration) + " us"); EventLog::instance().record("Fused loop: " + std::to_string(fused_duration) + " us"); @@ -456,8 +460,8 @@ TEST_F(CacheFriendlyTest, BranchPredictionImpact) total_unsorted += sum; } auto end_unsorted = std::chrono::high_resolution_clock::now(); - auto unsorted_duration = std::chrono::duration_cast( - end_unsorted - start_unsorted).count(); + auto unsorted_duration = + std::chrono::duration_cast(end_unsorted - start_unsorted).count(); long long total_sorted = 0; auto start_sorted = std::chrono::high_resolution_clock::now(); @@ -474,8 +478,7 @@ TEST_F(CacheFriendlyTest, BranchPredictionImpact) total_sorted += sum; } auto end_sorted = std::chrono::high_resolution_clock::now(); - auto sorted_duration = std::chrono::duration_cast( - end_sorted - start_sorted).count(); + auto sorted_duration = std::chrono::duration_cast(end_sorted - start_sorted).count(); EventLog::instance().record("Unsorted branch: " + std::to_string(unsorted_duration) + " ms"); EventLog::instance().record("Sorted branch: " + std::to_string(sorted_duration) + " ms"); diff --git a/learning_performance/tests/test_constexpr.cpp b/learning_performance/tests/test_constexpr.cpp index f4a906d..c2864ce 100644 --- a/learning_performance/tests/test_constexpr.cpp +++ b/learning_performance/tests/test_constexpr.cpp @@ -2,11 +2,11 @@ // Estimated Time: 3 hours // Difficulty: Moderate to Hard - -#include #include "instrumentation.h" + #include #include +#include class ConstexprTest : public ::testing::Test { @@ -97,13 +97,18 @@ TEST_F(ConstexprTest, ConstexprVsConst) class Point { public: - constexpr Point(int x, int y) - : x_(x), y_(y) + constexpr Point(int x, int y) : x_(x), y_(y) { } - constexpr int x() const { return x_; } - constexpr int y() const { return y_; } + constexpr int x() const + { + return x_; + } + constexpr int y() const + { + return y_; + } constexpr int distance_squared() const { @@ -142,8 +147,7 @@ TEST_F(ConstexprTest, ConstexprConstructorsAndObjects) // TEST 4: constexpr if in C++17 - Moderate // ============================================================================ -template -constexpr T absolute_value(T value) +template constexpr T absolute_value(T value) { if constexpr (std::is_unsigned_v) { diff --git a/learning_performance/tests/test_copy_elision_rvo.cpp b/learning_performance/tests/test_copy_elision_rvo.cpp index 0f8ce9a..6b15599 100644 --- a/learning_performance/tests/test_copy_elision_rvo.cpp +++ b/learning_performance/tests/test_copy_elision_rvo.cpp @@ -2,11 +2,11 @@ // Estimated Time: 3 hours // Difficulty: Moderate +#include "instrumentation.h" #include -#include "instrumentation.h" -#include #include +#include class CopyElisionTest : public ::testing::Test { @@ -149,25 +149,25 @@ class Container EventLog::instance().record("Container::ctor"); } - explicit Container(std::vector data) - : data_(std::move(data)) + explicit Container(std::vector data) : data_(std::move(data)) { EventLog::instance().record("Container::ctor(vector)"); } - Container(const Container& other) - : data_(other.data_) + Container(const Container& other) : data_(other.data_) { EventLog::instance().record("Container::copy_ctor"); } - Container(Container&& other) noexcept - : data_(std::move(other.data_)) + Container(Container&& other) noexcept : data_(std::move(other.data_)) { EventLog::instance().record("Container::move_ctor"); } - size_t size() const { return data_.size(); } + size_t size() const + { + return data_.size(); + } private: std::vector data_; diff --git a/learning_performance/tests/test_profiling.cpp b/learning_performance/tests/test_profiling.cpp index b79acca..224ba77 100644 --- a/learning_performance/tests/test_profiling.cpp +++ b/learning_performance/tests/test_profiling.cpp @@ -2,15 +2,15 @@ // Estimated Time: 3 hours // Difficulty: Moderate to Hard - -#include #include "instrumentation.h" -#include -#include + #include -#include +#include +#include #include +#include #include +#include class ProfilingTest : public ::testing::Test { @@ -62,13 +62,15 @@ TEST_F(ProfilingTest, BasicProfilingWithEventLog) int compute_fibonacci(int n) { - if (n <= 1) return n; + if (n <= 1) + return n; return compute_fibonacci(n - 1) + compute_fibonacci(n - 2); } int compute_fibonacci_optimized(int n) { - if (n <= 1) return n; + if (n <= 1) + return n; int prev = 0, curr = 1; for (int i = 2; i <= n; ++i) { @@ -130,8 +132,14 @@ class AllocationTracker allocation_count_ = 0; } - static size_t total_bytes() { return total_bytes_; } - static size_t allocation_count() { return allocation_count_; } + static size_t total_bytes() + { + return total_bytes_; + } + static size_t allocation_count() + { + return allocation_count_; + } private: static size_t total_bytes_; @@ -320,8 +328,7 @@ TEST_F(ProfilingTest, ProfilingAllocationPatterns) } } auto end_vector = std::chrono::high_resolution_clock::now(); - auto vector_duration = std::chrono::duration_cast( - end_vector - start_vector).count(); + auto vector_duration = std::chrono::duration_cast(end_vector - start_vector).count(); auto start_reserved = std::chrono::high_resolution_clock::now(); for (int i = 0; i < iterations; ++i) @@ -334,8 +341,8 @@ TEST_F(ProfilingTest, ProfilingAllocationPatterns) } } auto end_reserved = std::chrono::high_resolution_clock::now(); - auto reserved_duration = std::chrono::duration_cast( - end_reserved - start_reserved).count(); + auto reserved_duration = + std::chrono::duration_cast(end_reserved - start_reserved).count(); EventLog::instance().record("Without reserve: " + std::to_string(vector_duration) + " us"); EventLog::instance().record("With reserve: " + std::to_string(reserved_duration) + " us"); diff --git a/learning_performance/tests/test_small_object_optimization.cpp b/learning_performance/tests/test_small_object_optimization.cpp index f7ccfd1..9834933 100644 --- a/learning_performance/tests/test_small_object_optimization.cpp +++ b/learning_performance/tests/test_small_object_optimization.cpp @@ -2,13 +2,13 @@ // Estimated Time: 3 hours // Difficulty: Moderate to Hard +#include "instrumentation.h" +#include +#include #include -#include "instrumentation.h" #include #include -#include -#include class SmallObjectOptimizationTest : public ::testing::Test { @@ -34,10 +34,8 @@ TEST_F(SmallObjectOptimizationTest, StringSSO) const void* small_obj_addr = &small; const void* large_obj_addr = &large; - ptrdiff_t small_offset = reinterpret_cast(small_ptr) - - reinterpret_cast(small_obj_addr); - ptrdiff_t large_offset = reinterpret_cast(large_ptr) - - reinterpret_cast(large_obj_addr); + ptrdiff_t small_offset = reinterpret_cast(small_ptr) - reinterpret_cast(small_obj_addr); + ptrdiff_t large_offset = reinterpret_cast(large_ptr) - reinterpret_cast(large_obj_addr); EventLog::instance().record("Small string offset: " + std::to_string(small_offset)); EventLog::instance().record("Large string offset: " + std::to_string(large_offset)); @@ -75,8 +73,7 @@ TEST_F(SmallObjectOptimizationTest, SSOPerformanceImpact) (void)c; } auto end_small = std::chrono::high_resolution_clock::now(); - auto small_duration = std::chrono::duration_cast( - end_small - start_small).count(); + auto small_duration = std::chrono::duration_cast(end_small - start_small).count(); auto start_large = std::chrono::high_resolution_clock::now(); for (int i = 0; i < iterations; ++i) @@ -86,8 +83,7 @@ TEST_F(SmallObjectOptimizationTest, SSOPerformanceImpact) (void)c; } auto end_large = std::chrono::high_resolution_clock::now(); - auto large_duration = std::chrono::duration_cast( - end_large - start_large).count(); + auto large_duration = std::chrono::duration_cast(end_large - start_large).count(); EventLog::instance().record("Small string creation: " + std::to_string(small_duration) + " us"); EventLog::instance().record("Large string creation: " + std::to_string(large_duration) + " us"); @@ -133,12 +129,10 @@ TEST_F(SmallObjectOptimizationTest, FunctionSSO) // TEST 4: Implementing Custom SSO Container - Hard // ============================================================================ -template -class SmallVector +template class SmallVector { public: - SmallVector() - : size_(0), capacity_(BufferSize), heap_data_(nullptr) + SmallVector() : size_(0), capacity_(BufferSize), heap_data_(nullptr) { EventLog::instance().record("SmallVector::ctor"); } @@ -173,8 +167,14 @@ class SmallVector } } - size_t size() const { return size_; } - bool uses_heap() const { return heap_data_ != nullptr; } + size_t size() const + { + return size_; + } + bool uses_heap() const + { + return heap_data_ != nullptr; + } private: void grow() diff --git a/learning_raii/tests/test_custom_resource_managers.cpp b/learning_raii/tests/test_custom_resource_managers.cpp index e74468b..d565360 100644 --- a/learning_raii/tests/test_custom_resource_managers.cpp +++ b/learning_raii/tests/test_custom_resource_managers.cpp @@ -3,6 +3,7 @@ // Difficulty: Moderate #include "instrumentation.h" + #include #include #include @@ -23,13 +24,11 @@ class CustomResourceManagersTest : public ::testing::Test class ResourceHandle { public: - explicit ResourceHandle(int id) - : id_(id) - , valid_(true) + explicit ResourceHandle(int id) : id_(id), valid_(true) { EventLog::instance().record("ResourceHandle::acquire id=" + std::to_string(id_)); } - + ~ResourceHandle() { if (valid_) @@ -37,47 +36,45 @@ class ResourceHandle EventLog::instance().record("ResourceHandle::release id=" + std::to_string(id_)); } } - - ResourceHandle(ResourceHandle&& other) noexcept - : id_(other.id_) - , valid_(other.valid_) + + ResourceHandle(ResourceHandle&& other) noexcept : id_(other.id_), valid_(other.valid_) { other.valid_ = false; EventLog::instance().record("ResourceHandle::move_ctor id=" + std::to_string(id_)); } - + ResourceHandle& operator=(ResourceHandle&& other) noexcept { if (this != &other) { if (valid_) { - EventLog::instance().record("ResourceHandle::release id=" + std::to_string(id_) + - " (before move_assign)"); + EventLog::instance().record("ResourceHandle::release id=" + std::to_string(id_) + + " (before move_assign)"); } - + id_ = other.id_; valid_ = other.valid_; other.valid_ = false; - + EventLog::instance().record("ResourceHandle::move_assign id=" + std::to_string(id_)); } return *this; } - + int id() const { return id_; } - + bool is_valid() const { return valid_; } - + ResourceHandle(const ResourceHandle&) = delete; ResourceHandle& operator=(const ResourceHandle&) = delete; - + private: int id_; bool valid_; @@ -92,22 +89,22 @@ TEST_F(CustomResourceManagersTest, BasicResourceAcquisition) // Q: What does RAII stand for? // A: // R: - + { ResourceHandle handle(1); - + EXPECT_TRUE(handle.is_valid()); EXPECT_EQ(EventLog::instance().count_events("acquire"), 1); - + // Q: When will the resource be released? // A: // R: } - + // Q: What observable signal confirms release occurred? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("release"), 1); } @@ -118,23 +115,23 @@ TEST_F(CustomResourceManagersTest, BasicResourceAcquisition) TEST_F(CustomResourceManagersTest, ResourceTransferViaMove) { ResourceHandle handle1(10); - + EventLog::instance().clear(); - + // Q: What happens to handle1's resource during the move? // A: // R: - + ResourceHandle handle2(std::move(handle1)); - + // Q: Is handle1 still valid after the move? // A: // R: - + EXPECT_FALSE(handle1.is_valid()); EXPECT_TRUE(handle2.is_valid()); EXPECT_EQ(EventLog::instance().count_events("move_ctor"), 1); - + // Q: How many times will the resource be released? // A: // R: @@ -148,25 +145,25 @@ TEST_F(CustomResourceManagersTest, MoveAssignmentWithActiveResource) { ResourceHandle handle1(20); ResourceHandle handle2(30); - + EventLog::instance().clear(); - + // Q: What happens to handle2's resource when we assign handle1 to it? // A: // R: - + handle2 = std::move(handle1); - + // Q: How many release events occurred during move assignment? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("release"), 1); - + // Both "before move_assign" and "move_assign id=" contain "move_assign" // So we check for the specific move_assign event EXPECT_EQ(EventLog::instance().count_events("move_assign id="), 1); - + EXPECT_FALSE(handle1.is_valid()); EXPECT_TRUE(handle2.is_valid()); EXPECT_EQ(handle2.id(), 20); @@ -179,25 +176,24 @@ TEST_F(CustomResourceManagersTest, MoveAssignmentWithActiveResource) class TrackedResourceManager { public: - explicit TrackedResourceManager(const std::string& name) - : resource_(std::make_shared(name)) + explicit TrackedResourceManager(const std::string& name) : resource_(std::make_shared(name)) { EventLog::instance().record("TrackedResourceManager::ctor"); } - + ~TrackedResourceManager() { EventLog::instance().record("TrackedResourceManager::dtor"); } - + std::string name() const { return resource_->name(); } - + TrackedResourceManager(const TrackedResourceManager&) = delete; TrackedResourceManager& operator=(const TrackedResourceManager&) = delete; - + private: std::shared_ptr resource_; }; @@ -207,22 +203,22 @@ TEST_F(CustomResourceManagersTest, ResourceManagerWithTracked) // Q: What resources does TrackedResourceManager manage? // A: // R: - + { TrackedResourceManager manager("ManagedResource"); - + EXPECT_EQ(manager.name(), "ManagedResource"); EXPECT_EQ(EventLog::instance().count_events("TrackedResourceManager::ctor"), 1); - + // Q: What is the destruction order: TrackedResourceManager or Tracked? // A: // R: } - + // Q: Walk through the observable signals in EventLog for destruction order // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("TrackedResourceManager::dtor"), 1); EXPECT_EQ(EventLog::instance().count_events("Tracked(ManagedResource)::dtor"), 1); } @@ -237,13 +233,13 @@ class ThrowingResource explicit ThrowingResource(bool should_throw) { EventLog::instance().record("ThrowingResource::acquire"); - + if (should_throw) { throw std::runtime_error("Acquisition failed"); } } - + ~ThrowingResource() { EventLog::instance().record("ThrowingResource::release"); @@ -255,9 +251,9 @@ TEST_F(CustomResourceManagersTest, ExceptionSafetyInAcquisition) // Q: What happens if the constructor throws? // A: // R: - + bool exception_caught = false; - + try { ThrowingResource resource(true); @@ -266,16 +262,16 @@ TEST_F(CustomResourceManagersTest, ExceptionSafetyInAcquisition) { exception_caught = true; } - + EXPECT_TRUE(exception_caught); - + // Q: Was the destructor called for the failed resource? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("acquire"), 1); EXPECT_EQ(EventLog::instance().count_events("release"), 0); - + // Q: Why is this behavior correct for RAII? // A: // R: diff --git a/learning_raii/tests/test_file_socket_management.cpp b/learning_raii/tests/test_file_socket_management.cpp index 0c0d19a..15c29df 100644 --- a/learning_raii/tests/test_file_socket_management.cpp +++ b/learning_raii/tests/test_file_socket_management.cpp @@ -3,10 +3,11 @@ // Difficulty: Easy #include "instrumentation.h" -#include + +#include #include +#include #include -#include class FileSocketManagementTest : public ::testing::Test { @@ -15,7 +16,7 @@ class FileSocketManagementTest : public ::testing::Test { EventLog::instance().clear(); } - + void TearDown() override { std::remove("test_file.txt"); @@ -31,32 +32,32 @@ TEST_F(FileSocketManagementTest, FileHandleRAII) // Q: What resource does std::fstream manage? // A: // R: - + { std::ofstream file("test_file.txt"); - + // Q: When is the file opened? // A: // R: - + EXPECT_TRUE(file.is_open()); - + file << "RAII test content\n"; - + // Q: When will the file be closed? // A: // R: } - + // Q: Is the file closed now? // A: // R: - + // Verify file was written std::ifstream read_file("test_file.txt"); std::string content; std::getline(read_file, content); - + EXPECT_EQ(content, "RAII test content"); } @@ -69,32 +70,32 @@ TEST_F(FileSocketManagementTest, FileHandleWithException) // Q: What happens to the file handle if an exception is thrown? // A: // R: - + try { std::ofstream file("test_file.txt"); EXPECT_TRUE(file.is_open()); - + file << "Before exception\n"; - + throw std::runtime_error("Test exception"); - + file << "After exception\n"; } catch (const std::runtime_error&) { // Exception caught } - + // Q: Was the file properly closed despite the exception? // A: // R: - + // Verify file was written and closed std::ifstream read_file("test_file.txt"); std::string content; std::getline(read_file, content); - + EXPECT_EQ(content, "Before exception"); } @@ -105,12 +106,10 @@ TEST_F(FileSocketManagementTest, FileHandleWithException) class FileHandle { public: - explicit FileHandle(const std::string& filename) - : file_(nullptr) - , filename_(filename) + explicit FileHandle(const std::string& filename) : file_(nullptr), filename_(filename) { file_ = std::fopen(filename.c_str(), "w"); - + if (file_) { EventLog::instance().record("FileHandle::open " + filename_); @@ -120,7 +119,7 @@ class FileHandle EventLog::instance().record("FileHandle::open_failed " + filename_); } } - + ~FileHandle() { if (file_) @@ -129,7 +128,7 @@ class FileHandle EventLog::instance().record("FileHandle::close " + filename_); } } - + void write(const std::string& data) { if (file_) @@ -137,15 +136,15 @@ class FileHandle std::fputs(data.c_str(), file_); } } - + bool is_open() const { return file_ != nullptr; } - + FileHandle(const FileHandle&) = delete; FileHandle& operator=(const FileHandle&) = delete; - + private: FILE* file_; std::string filename_; @@ -156,27 +155,27 @@ TEST_F(FileSocketManagementTest, CustomFileHandleWrapper) // Q: Why do we delete the copy constructor and copy assignment? // A: // R: - + { FileHandle handle("test_file.txt"); - + EXPECT_TRUE(handle.is_open()); EXPECT_EQ(EventLog::instance().count_events("FileHandle::open"), 1); - + handle.write("Custom wrapper\n"); } - + // Q: What observable signal confirms the file was closed? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("FileHandle::close"), 1); - + // Verify content std::ifstream read_file("test_file.txt"); std::string content; std::getline(read_file, content); - + EXPECT_EQ(content, "Custom wrapper"); } @@ -187,31 +186,27 @@ TEST_F(FileSocketManagementTest, CustomFileHandleWrapper) class MovableFileHandle { public: - explicit MovableFileHandle(const std::string& filename) - : file_(nullptr) - , filename_(filename) + explicit MovableFileHandle(const std::string& filename) : file_(nullptr), filename_(filename) { file_ = std::fopen(filename.c_str(), "w"); - + if (file_) { EventLog::instance().record("MovableFileHandle::open " + filename_); } } - + ~MovableFileHandle() { close(); } - - MovableFileHandle(MovableFileHandle&& other) noexcept - : file_(other.file_) - , filename_(std::move(other.filename_)) + + MovableFileHandle(MovableFileHandle&& other) noexcept : file_(other.file_), filename_(std::move(other.filename_)) { other.file_ = nullptr; EventLog::instance().record("MovableFileHandle::move_ctor"); } - + MovableFileHandle& operator=(MovableFileHandle&& other) noexcept { if (this != &other) @@ -224,7 +219,7 @@ class MovableFileHandle } return *this; } - + void write(const std::string& data) { if (file_) @@ -232,15 +227,15 @@ class MovableFileHandle std::fputs(data.c_str(), file_); } } - + bool is_open() const { return file_ != nullptr; } - + MovableFileHandle(const MovableFileHandle&) = delete; MovableFileHandle& operator=(const MovableFileHandle&) = delete; - + private: void close() { @@ -251,7 +246,7 @@ class MovableFileHandle file_ = nullptr; } } - + FILE* file_; std::string filename_; }; @@ -261,23 +256,23 @@ TEST_F(FileSocketManagementTest, MoveOnlyFileHandle) // Q: Why would we want a movable file handle? // A: // R: - + MovableFileHandle handle1("test_file.txt"); EXPECT_TRUE(handle1.is_open()); - + EventLog::instance().clear(); - + // TODO: Move handle1 to handle2 MovableFileHandle handle2(std::move(handle1)); - + // Q: What is the state of handle1 after the move? // A: // R: - + EXPECT_FALSE(handle1.is_open()); EXPECT_TRUE(handle2.is_open()); EXPECT_EQ(EventLog::instance().count_events("move_ctor"), 1); - + // Q: How many times will the file be closed? // A: // R: @@ -290,24 +285,22 @@ TEST_F(FileSocketManagementTest, MoveOnlyFileHandle) class MultiResourceManager { public: - MultiResourceManager(const std::string& file1, const std::string& file2) - : handle1_(file1) - , handle2_(file2) + MultiResourceManager(const std::string& file1, const std::string& file2) : handle1_(file1), handle2_(file2) { EventLog::instance().record("MultiResourceManager::ctor"); } - + ~MultiResourceManager() { EventLog::instance().record("MultiResourceManager::dtor"); } - + void write_both(const std::string& data) { handle1_.write(data); handle2_.write(data); } - + private: FileHandle handle1_; FileHandle handle2_; @@ -317,29 +310,29 @@ TEST_F(FileSocketManagementTest, MultipleResourcesRAII) { std::remove("file1.txt"); std::remove("file2.txt"); - + // Q: In what order are handle1_ and handle2_ constructed? // A: // R: - + { MultiResourceManager manager("file1.txt", "file2.txt"); - + EXPECT_EQ(EventLog::instance().count_events("FileHandle::open"), 2); - + manager.write_both("data\n"); } - + // Q: In what order are handle1_ and handle2_ destroyed? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("FileHandle::close"), 2); - + // Q: What happens if handle1_ construction succeeds but handle2_ construction throws? // A: // R: - + std::remove("file1.txt"); std::remove("file2.txt"); } diff --git a/learning_raii/tests/test_scope_guards.cpp b/learning_raii/tests/test_scope_guards.cpp index 1245486..9c0fbac 100644 --- a/learning_raii/tests/test_scope_guards.cpp +++ b/learning_raii/tests/test_scope_guards.cpp @@ -3,8 +3,9 @@ // Difficulty: Easy #include "instrumentation.h" -#include + #include +#include #include class ScopeGuardsTest : public ::testing::Test @@ -20,17 +21,14 @@ class ScopeGuardsTest : public ::testing::Test // Basic Scope Guard Implementation // ============================================================================ -template -class ScopeGuard +template class ScopeGuard { public: - explicit ScopeGuard(Func&& func) - : func_(std::forward(func)) - , active_(true) + explicit ScopeGuard(Func&& func) : func_(std::forward(func)), active_(true) { EventLog::instance().record("ScopeGuard::ctor"); } - + ~ScopeGuard() { if (active_) @@ -43,22 +41,21 @@ class ScopeGuard EventLog::instance().record("ScopeGuard::dtor - dismissed"); } } - + void dismiss() { active_ = false; } - + ScopeGuard(const ScopeGuard&) = delete; ScopeGuard& operator=(const ScopeGuard&) = delete; - + private: Func func_; bool active_; }; -template -ScopeGuard make_scope_guard(Func&& func) +template ScopeGuard make_scope_guard(Func&& func) { return ScopeGuard(std::forward(func)); } @@ -72,26 +69,26 @@ TEST_F(ScopeGuardsTest, BasicScopeGuard) // Q: What is RAII? // A: // R: - + bool cleanup_called = false; - + { auto guard = make_scope_guard([&cleanup_called]() { cleanup_called = true; EventLog::instance().record("Cleanup executed"); }); - + // Q: When will the cleanup function be called? // A: // R: - + EXPECT_FALSE(cleanup_called); } - + // Q: What guarantees that cleanup_called is now true? // A: // R: - + EXPECT_TRUE(cleanup_called); EXPECT_EQ(EventLog::instance().count_events("Cleanup executed"), 1); } @@ -103,29 +100,29 @@ TEST_F(ScopeGuardsTest, BasicScopeGuard) TEST_F(ScopeGuardsTest, ScopeGuardWithException) { bool cleanup_called = false; - + // Q: What happens to the scope guard if an exception is thrown? // A: // R: - + try { auto guard = make_scope_guard([&cleanup_called]() { cleanup_called = true; EventLog::instance().record("Exception cleanup"); }); - + throw std::runtime_error("Test exception"); } catch (const std::runtime_error&) { // Exception caught } - + // Q: Was the cleanup function called despite the exception? // A: // R: - + EXPECT_TRUE(cleanup_called); EXPECT_EQ(EventLog::instance().count_events("Exception cleanup"), 1); } @@ -137,23 +134,21 @@ TEST_F(ScopeGuardsTest, ScopeGuardWithException) TEST_F(ScopeGuardsTest, DismissibleScopeGuard) { bool cleanup_called = false; - + { - auto guard = make_scope_guard([&cleanup_called]() { - cleanup_called = true; - }); - + auto guard = make_scope_guard([&cleanup_called]() { cleanup_called = true; }); + // Q: What does dismiss() do? // A: // R: - + guard.dismiss(); } - + // Q: Was the cleanup function called? // A: // R: - + EXPECT_FALSE(cleanup_called); EXPECT_EQ(EventLog::instance().count_events("dismissed"), 1); } @@ -165,29 +160,23 @@ TEST_F(ScopeGuardsTest, DismissibleScopeGuard) TEST_F(ScopeGuardsTest, MultipleScopeGuards) { std::vector execution_order; - + { - auto guard1 = make_scope_guard([&execution_order]() { - execution_order.push_back(1); - }); - - auto guard2 = make_scope_guard([&execution_order]() { - execution_order.push_back(2); - }); - - auto guard3 = make_scope_guard([&execution_order]() { - execution_order.push_back(3); - }); - + auto guard1 = make_scope_guard([&execution_order]() { execution_order.push_back(1); }); + + auto guard2 = make_scope_guard([&execution_order]() { execution_order.push_back(2); }); + + auto guard3 = make_scope_guard([&execution_order]() { execution_order.push_back(3); }); + // Q: In what order will the guards execute their cleanup functions? // A: // R: } - + // Q: Why is this order important for resource cleanup? // A: // R: - + ASSERT_EQ(execution_order.size(), 3); EXPECT_EQ(execution_order[0], 3); EXPECT_EQ(execution_order[1], 2); @@ -201,33 +190,31 @@ TEST_F(ScopeGuardsTest, MultipleScopeGuards) TEST_F(ScopeGuardsTest, ScopeGuardWithTracked) { std::shared_ptr ptr = std::make_shared("Resource"); - + EventLog::instance().clear(); - + // Q: What is ptr's use_count before the scope guard? // A: // R: - + EXPECT_EQ(ptr.use_count(), 1); - + { - auto guard = make_scope_guard([ptr]() { - EventLog::instance().record("Guard cleanup with " + ptr->name()); - }); - + auto guard = make_scope_guard([ptr]() { EventLog::instance().record("Guard cleanup with " + ptr->name()); }); + // Q: What is ptr's use_count inside the scope? // A: // R: - + EXPECT_EQ(ptr.use_count(), 2); } - + // Q: What is ptr's use_count after the scope guard is destroyed? // A: // R: - + EXPECT_EQ(ptr.use_count(), 1); - + // Q: Walk through the destruction order: guard's destructor, then what? // A: // R: @@ -244,7 +231,7 @@ class ManualResource { EventLog::instance().record("ManualResource::acquire"); } - + void release() { EventLog::instance().record("ManualResource::release"); @@ -258,12 +245,12 @@ class RAIIResource { EventLog::instance().record("RAIIResource::acquire"); } - + ~RAIIResource() { EventLog::instance().record("RAIIResource::release"); } - + RAIIResource(const RAIIResource&) = delete; RAIIResource& operator=(const RAIIResource&) = delete; }; @@ -273,30 +260,30 @@ TEST_F(ScopeGuardsTest, RAIIvsManualCleanup) // Manual cleanup - error prone { ManualResource manual; - + // Q: What happens if an exception is thrown before release()? // A: // R: - + // manual.release(); // Easy to forget! } - + EXPECT_EQ(EventLog::instance().count_events("ManualResource::release"), 0); - + EventLog::instance().clear(); - + // RAII cleanup - automatic { RAIIResource raii; - + // Q: What guarantees that release will be called? // A: // R: } - + // Q: What observable signal shows RAII cleanup occurred? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("RAIIResource::release"), 1); } diff --git a/learning_raii/tests/test_smart_pointers_from_scratch.cpp b/learning_raii/tests/test_smart_pointers_from_scratch.cpp index 6fe2e1c..2bd4eb9 100644 --- a/learning_raii/tests/test_smart_pointers_from_scratch.cpp +++ b/learning_raii/tests/test_smart_pointers_from_scratch.cpp @@ -3,9 +3,10 @@ // Difficulty: Moderate #include "instrumentation.h" + +#include #include #include -#include class SmartPointersFromScratchTest : public ::testing::Test { @@ -20,16 +21,14 @@ class SmartPointersFromScratchTest : public ::testing::Test // Custom unique_ptr Implementation // ============================================================================ -template -class UniquePtr +template class UniquePtr { public: - explicit UniquePtr(T* ptr = nullptr) - : ptr_(ptr) + explicit UniquePtr(T* ptr = nullptr) : ptr_(ptr) { EventLog::instance().record("UniquePtr::ctor"); } - + ~UniquePtr() { if (ptr_) @@ -42,14 +41,13 @@ class UniquePtr EventLog::instance().record("UniquePtr::dtor - empty"); } } - - UniquePtr(UniquePtr&& other) noexcept - : ptr_(other.ptr_) + + UniquePtr(UniquePtr&& other) noexcept : ptr_(other.ptr_) { other.ptr_ = nullptr; EventLog::instance().record("UniquePtr::move_ctor"); } - + UniquePtr& operator=(UniquePtr&& other) noexcept { if (this != &other) @@ -64,27 +62,27 @@ class UniquePtr } return *this; } - + T* get() const { return ptr_; } - + T& operator*() const { return *ptr_; } - + T* operator->() const { return ptr_; } - + explicit operator bool() const { return ptr_ != nullptr; } - + T* release() { T* temp = ptr_; @@ -92,7 +90,7 @@ class UniquePtr EventLog::instance().record("UniquePtr::release"); return temp; } - + void reset(T* ptr = nullptr) { if (ptr_) @@ -102,10 +100,10 @@ class UniquePtr ptr_ = ptr; EventLog::instance().record("UniquePtr::reset"); } - + UniquePtr(const UniquePtr&) = delete; UniquePtr& operator=(const UniquePtr&) = delete; - + private: T* ptr_; }; @@ -119,23 +117,23 @@ TEST_F(SmartPointersFromScratchTest, CustomUniquePtrBasics) // Q: What ownership semantics does unique_ptr provide? // A: // R: - + { UniquePtr ptr(new Tracked("Unique")); - + EXPECT_TRUE(ptr); EXPECT_EQ(ptr->name(), "Unique"); EXPECT_EQ(EventLog::instance().count_events("UniquePtr::ctor"), 1); - + // Q: When will the Tracked object be deleted? // A: // R: } - + // Q: What observable signal confirms deletion occurred? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("UniquePtr::dtor - deleting"), 1); EXPECT_EQ(EventLog::instance().count_events("Tracked(Unique)::dtor"), 1); } @@ -147,24 +145,24 @@ TEST_F(SmartPointersFromScratchTest, CustomUniquePtrBasics) TEST_F(SmartPointersFromScratchTest, UniquePtrMoveSemantics) { UniquePtr ptr1(new Tracked("Movable")); - + EventLog::instance().clear(); - + // Q: Why can't we copy a unique_ptr? // A: // R: - + UniquePtr ptr2(std::move(ptr1)); - + // Q: What is the state of ptr1 after the move? // A: // R: - + EXPECT_FALSE(ptr1); EXPECT_TRUE(ptr2); EXPECT_EQ(ptr2->name(), "Movable"); EXPECT_EQ(EventLog::instance().count_events("move_ctor"), 1); - + // Q: How many Tracked objects will be deleted? // A: // R: @@ -177,33 +175,33 @@ TEST_F(SmartPointersFromScratchTest, UniquePtrMoveSemantics) TEST_F(SmartPointersFromScratchTest, UniquePtrReleaseAndReset) { UniquePtr ptr(new Tracked("Original")); - + EventLog::instance().clear(); - + // Q: What does release() do? // A: // R: - + Tracked* raw_ptr = ptr.release(); - + EXPECT_FALSE(ptr); EXPECT_EQ(raw_ptr->name(), "Original"); EXPECT_EQ(EventLog::instance().count_events("UniquePtr::release"), 1); - + // Q: Who is responsible for deleting raw_ptr now? // A: // R: - + delete raw_ptr; - + EventLog::instance().clear(); - + // Q: What does reset() do? // A: // R: - + ptr.reset(new Tracked("Reset")); - + EXPECT_TRUE(ptr); EXPECT_EQ(ptr->name(), "Reset"); EXPECT_EQ(EventLog::instance().count_events("UniquePtr::reset"), 1); @@ -213,35 +211,28 @@ TEST_F(SmartPointersFromScratchTest, UniquePtrReleaseAndReset) // Custom shared_ptr Implementation (Simplified) // ============================================================================ -template -class SharedPtr +template class SharedPtr { public: - explicit SharedPtr(T* ptr = nullptr) - : ptr_(ptr) - , ref_count_(ptr ? new std::atomic(1) : nullptr) + explicit SharedPtr(T* ptr = nullptr) : ptr_(ptr), ref_count_(ptr ? new std::atomic(1) : nullptr) { - EventLog::instance().record("SharedPtr::ctor use_count=" + - std::to_string(use_count())); + EventLog::instance().record("SharedPtr::ctor use_count=" + std::to_string(use_count())); } - + ~SharedPtr() { release(); } - - SharedPtr(const SharedPtr& other) - : ptr_(other.ptr_) - , ref_count_(other.ref_count_) + + SharedPtr(const SharedPtr& other) : ptr_(other.ptr_), ref_count_(other.ref_count_) { if (ref_count_) { (*ref_count_)++; } - EventLog::instance().record("SharedPtr::copy_ctor use_count=" + - std::to_string(use_count())); + EventLog::instance().record("SharedPtr::copy_ctor use_count=" + std::to_string(use_count())); } - + SharedPtr& operator=(const SharedPtr& other) { if (this != &other) @@ -253,46 +244,44 @@ class SharedPtr { (*ref_count_)++; } - EventLog::instance().record("SharedPtr::copy_assign use_count=" + - std::to_string(use_count())); + EventLog::instance().record("SharedPtr::copy_assign use_count=" + std::to_string(use_count())); } return *this; } - + T* get() const { return ptr_; } - + T& operator*() const { return *ptr_; } - + T* operator->() const { return ptr_; } - + long use_count() const { return ref_count_ ? ref_count_->load() : 0; } - + explicit operator bool() const { return ptr_ != nullptr; } - + private: void release() { if (ref_count_) { long count = --(*ref_count_); - EventLog::instance().record("SharedPtr::release use_count=" + - std::to_string(count)); - + EventLog::instance().record("SharedPtr::release use_count=" + std::to_string(count)); + if (count == 0) { delete ptr_; @@ -301,7 +290,7 @@ class SharedPtr } } } - + T* ptr_; std::atomic* ref_count_; }; @@ -315,24 +304,24 @@ TEST_F(SmartPointersFromScratchTest, CustomSharedPtrBasics) // Q: What is the key difference between unique_ptr and shared_ptr? // A: // R: - + SharedPtr ptr1(new Tracked("Shared")); - + EXPECT_EQ(ptr1.use_count(), 1); - + // Q: What does the control block (ref_count_) store? // A: // R: - + SharedPtr ptr2 = ptr1; - + // Q: What happened to use_count during the copy? // A: // R: - + EXPECT_EQ(ptr1.use_count(), 2); EXPECT_EQ(ptr2.use_count(), 2); - + EXPECT_EQ(ptr1.get(), ptr2.get()); } @@ -343,39 +332,39 @@ TEST_F(SmartPointersFromScratchTest, CustomSharedPtrBasics) TEST_F(SmartPointersFromScratchTest, SharedPtrReferenceCounting) { SharedPtr ptr1(new Tracked("RefCount")); - + EventLog::instance().clear(); - + { SharedPtr ptr2 = ptr1; EXPECT_EQ(ptr1.use_count(), 2); - + { SharedPtr ptr3 = ptr1; EXPECT_EQ(ptr1.use_count(), 3); - + // Q: What happens when ptr3 goes out of scope? // A: // R: } - + // Q: What is use_count now? // A: // R: - + EXPECT_EQ(ptr1.use_count(), 2); } - + // Q: What is use_count after ptr2 is destroyed? // A: // R: - + EXPECT_EQ(ptr1.use_count(), 1); - + // Q: Walk through the EventLog: how many release events occurred? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("SharedPtr::release"), 2); } @@ -389,26 +378,26 @@ TEST_F(SmartPointersFromScratchTest, SharedPtrFinalDeletion) SharedPtr ptr1(new Tracked("Final")); SharedPtr ptr2 = ptr1; SharedPtr ptr3 = ptr1; - + EXPECT_EQ(ptr1.use_count(), 3); - + EventLog::instance().clear(); - + // Q: When will the Tracked object be deleted? // A: // R: } - + // Q: How many release events occurred? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("SharedPtr::release"), 3); - + // Q: What observable signal confirms the object was deleted? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("deleted_object"), 1); EXPECT_EQ(EventLog::instance().count_events("Tracked(Final)::dtor"), 1); } diff --git a/learning_shared_ptr/tests/test_allocation_patterns.cpp b/learning_shared_ptr/tests/test_allocation_patterns.cpp index 76fc86e..9712bc4 100644 --- a/learning_shared_ptr/tests/test_allocation_patterns.cpp +++ b/learning_shared_ptr/tests/test_allocation_patterns.cpp @@ -1,7 +1,8 @@ #include "instrumentation.h" + +#include #include #include -#include class AllocationPatternsTest : public ::testing::Test { protected: @@ -15,41 +16,43 @@ TEST_F(AllocationPatternsTest, MakeSharedVsNew) long new_count = 0; long make_shared_count = 0; // Q: When you create a shared_ptr using `new`, how many heap allocations occur? -// A: -// R: + // A: + // R: std::shared_ptr p1(new Tracked("New")); new_count = p1.use_count(); // Q: When you create a shared_ptr using `make_shared`, how many heap allocations occur? -// A: -// R: + // A: + // R: std::shared_ptr p2 = std::make_shared("MakeShared"); make_shared_count = p2.use_count(); - // Q: Both use_count values are 1. What does this tell you about the relationship between allocation strategy and reference counting? -// A: -// A: -// R: -// R: -// R: + // Q: Both use_count values are 1. What does this tell you about the relationship between allocation strategy and + // reference counting? + // A: + // A: + // R: + // R: + // R: EXPECT_EQ(new_count, 1); EXPECT_EQ(make_shared_count, 1); - // Q: Given that `new` requires 2 allocations (object + control block) and `make_shared` requires 1 (combined), what are the performance implications? -// A: -// R: + // Q: Given that `new` requires 2 allocations (object + control block) and `make_shared` requires 1 (combined), what + // are the performance implications? + // A: + // R: // Q: What happens to cache locality when the object and control block are allocated separately vs together? -// A: -// R: + // A: + // R: } class ThrowingResource { public: - explicit ThrowingResource(bool should_throw) - : tracked_("Throwing") + explicit ThrowingResource(bool should_throw) : tracked_("Throwing") { if (should_throw) { throw std::runtime_error("Construction failed"); } } + private: Tracked tracked_; }; @@ -79,12 +82,12 @@ TEST_F(AllocationPatternsTest, ExceptionSafetyMakeShared) { EventLog::instance().clear(); // Q: When p2's construction throws, what happens to p1? Walk through the stack unwinding. -// A: -// R: + // A: + // R: // Q: At what point in the statement `std::shared_ptr p1(new T(true))` could an exception leave a memory leak? -// A: -// R: -// R: + // A: + // R: + // R: process_with_make_shared(false, true); auto events = EventLog::instance().events(); size_t ctor_count = 0; @@ -101,8 +104,8 @@ TEST_F(AllocationPatternsTest, ExceptionSafetyMakeShared) } } // Q: The test expects ctor_count == dtor_count. What does this verify about exception safety? -// A: -// R: + // A: + // R: EXPECT_EQ(ctor_count, dtor_count); } struct FileDeleter @@ -121,13 +124,13 @@ TEST_F(AllocationPatternsTest, CustomDeleterFileHandle) { FILE* file = std::tmpfile(); // Q: Why can't you use make_shared with a custom deleter? -// A: -// R: -// R: + // A: + // R: + // R: // Q: Where is the FileDeleter stored—in the control block or with the shared_ptr object itself? -// A: -// R: -// R: + // A: + // R: + // R: std::shared_ptr file_ptr(file, FileDeleter()); long use_count = file_ptr.use_count(); EXPECT_EQ(use_count, 1); @@ -142,18 +145,18 @@ TEST_F(AllocationPatternsTest, CustomDeleterFileHandle) } } // Q: When exactly is the custom deleter invoked relative to the control block's destruction? -// A: -// R: -// R: + // A: + // R: + // R: EXPECT_TRUE(deleter_called); } class Resource { public: - explicit Resource(const std::string& name) - : tracked_(name) + explicit Resource(const std::string& name) : tracked_(name) { } + private: Tracked tracked_; }; @@ -169,8 +172,8 @@ TEST_F(AllocationPatternsTest, CustomDeleterWithTrackedObject) { { // Q: What is the sequence of operations when the shared_ptr is destroyed? (deleter call vs object destructor) -// A: -// R: + // A: + // R: std::shared_ptr res_ptr(new Resource("Res1"), ResourceDeleter()); long use_count = res_ptr.use_count(); EXPECT_EQ(use_count, 1); @@ -190,8 +193,8 @@ TEST_F(AllocationPatternsTest, CustomDeleterWithTrackedObject) } } // Q: The deleter calls `delete res`. What happens inside `delete` that triggers the Tracked destructor? -// A: -// R: + // A: + // R: EXPECT_TRUE(deleter_called); EXPECT_TRUE(dtor_called); } @@ -199,26 +202,24 @@ TEST_F(AllocationPatternsTest, ArrayAllocationWithCustomDeleter) { { // Q: Why must you use a custom deleter for arrays? What happens if you use the default deleter with `new T[]`? -// A: -// R: -// R: + // A: + // R: + // R: // Q: What is the difference between `delete ptr` and `delete[] ptr`? -// A: -// A: -// A: -// R: -// R: -// R: -// R: -// R: -// R: -// R: -// R: -// R: - std::shared_ptr arr_ptr( - new Tracked[3]{Tracked("Arr1"), Tracked("Arr2"), Tracked("Arr3")}, - LoggingArrayDeleter("ArrayDeleter") - ); + // A: + // A: + // A: + // R: + // R: + // R: + // R: + // R: + // R: + // R: + // R: + // R: + std::shared_ptr arr_ptr(new Tracked[3]{Tracked("Arr1"), Tracked("Arr2"), Tracked("Arr3")}, + LoggingArrayDeleter("ArrayDeleter")); long use_count = arr_ptr.use_count(); EXPECT_EQ(use_count, 1); } @@ -252,19 +253,20 @@ TEST_F(AllocationPatternsTest, DeleterTypeErasure) } }; // Q: Both Deleter1 and Deleter2 have different types. How can p1 and p2 have the same type `shared_ptr`? -// A: -// R: -// R: -// R: + // A: + // R: + // R: + // R: std::shared_ptr p1(new Tracked("T1"), Deleter1()); std::shared_ptr p2(new Tracked("T2"), Deleter2()); // Q: When you assign p2 to p1, what happens to p1's original object and which deleter is invoked? -// A: -// A: -// R: - // Q: After assignment, p1 now points to p2's object. Which deleter does p1's control block store—Deleter1 or Deleter2? -// A: -// R: + // A: + // A: + // R: + // Q: After assignment, p1 now points to p2's object. Which deleter does p1's control block store—Deleter1 or + // Deleter2? + // A: + // R: p1 = p2; auto events = EventLog::instance().events(); bool deleter1_called = false; @@ -282,8 +284,7 @@ TEST_F(AllocationPatternsTest, AliasingWithCustomDeleter) struct Container { Tracked member; - explicit Container(const std::string& name) - : member(name) + explicit Container(const std::string& name) : member(name) { } }; @@ -298,15 +299,17 @@ TEST_F(AllocationPatternsTest, AliasingWithCustomDeleter) long alias_count = 0; { std::shared_ptr owner(new Container("Owner"), ContainerDeleter()); - // Q: The alias points to `&owner->member` (a Tracked*), but shares the control block with owner. Which deleter is stored in the control block? -// A: -// R: + // Q: The alias points to `&owner->member` (a Tracked*), but shares the control block with owner. Which deleter + // is stored in the control block? + // A: + // R: std::shared_ptr alias(owner, &owner->member); alias_count = alias.use_count(); - // Q: After owner.reset(), only alias remains. When alias is destroyed, what gets deleted—the Tracked member or the Container? -// A: -// R: -// R: + // Q: After owner.reset(), only alias remains. When alias is destroyed, what gets deleted—the Tracked member or + // the Container? + // A: + // R: + // R: owner.reset(); } auto events = EventLog::instance().events(); @@ -333,13 +336,13 @@ TEST_F(AllocationPatternsTest, SharedDeleterBetweenPointers) }; std::shared_ptr p1(new Tracked("Shared"), SharedDeleter()); // Q: When you copy p1 to p2, do they share the same control block? What about the deleter stored in it? -// A: -// R: + // A: + // R: std::shared_ptr p2 = p1; long use_count = p1.use_count(); // Q: After p1.reset(), the use_count drops to 1. Why isn't the deleter called yet? -// A: -// R: + // A: + // R: p1.reset(); auto events_after_reset = EventLog::instance().events(); bool deleter_called_early = false; diff --git a/learning_shared_ptr/tests/test_anti_patterns.cpp b/learning_shared_ptr/tests/test_anti_patterns.cpp index 21f065d..1f61809 100644 --- a/learning_shared_ptr/tests/test_anti_patterns.cpp +++ b/learning_shared_ptr/tests/test_anti_patterns.cpp @@ -1,4 +1,5 @@ #include "instrumentation.h" + #include #include @@ -14,51 +15,46 @@ class AntiPatternsTest : public ::testing::Test TEST_F(AntiPatternsTest, SharedPtrAsGlobal) { auto global_ptr = std::make_shared("Global"); - + long use_count = global_ptr.use_count(); - // Q: If this were a global shared_ptr, what observable problem would arise when multiple translation units access it during static initialization? - // A: - // R: - + // Q: If this were a global shared_ptr, what observable problem would arise when multiple translation units access + // it during static initialization? A: R: + EXPECT_EQ(use_count, 1); } TEST_F(AntiPatternsTest, PassingByValueUnnecessarily) { - auto process = [](std::shared_ptr item) - { - auto copy = *item; - }; - + auto process = [](std::shared_ptr item) { auto copy = *item; }; + auto ptr = std::make_shared("Item"); - + EventLog::instance().clear(); - + process(ptr); - + auto events = EventLog::instance().events(); - // Q: What observable overhead does passing `shared_ptr` by value introduce compared to passing by `const shared_ptr&`? - // A: - // R: - + // Q: What observable overhead does passing `shared_ptr` by value introduce compared to passing by `const + // shared_ptr&`? A: R: + EXPECT_GT(events.size(), 0); } TEST_F(AntiPatternsTest, CreatingTwoSharedPtrsFromSameRaw) { Tracked* raw = new Tracked("Dangerous"); - + std::shared_ptr p1(raw); - + long use_count = p1.use_count(); // Q: If you uncommented `std::shared_ptr p2(raw);`, what would `p1.use_count()` return and why? // A: // R: - + // Q: What runtime failure would occur when both `p1` and the hypothetical `p2` go out of scope? // A: // R: - + EXPECT_EQ(use_count, 1); } @@ -66,41 +62,40 @@ TEST_F(AntiPatternsTest, HoldingSharedPtrToThis) { // ANTI-PATTERN: Storing shared_ptr to this without enable_shared_from_this // Problem: Circular reference, memory leak - + class BadSelfReference { public: - explicit BadSelfReference(const std::string& name) - : tracked_(name) + explicit BadSelfReference(const std::string& name) : tracked_(name) { } - + void set_self(std::shared_ptr self) { - self_ = self; // Creates circular reference! + self_ = self; // Creates circular reference! } - + private: Tracked tracked_; std::shared_ptr self_; }; - + { auto obj = std::make_shared("SelfRef"); - + obj->set_self(obj); - + long use_count = obj.use_count(); // Q: Why does `use_count` equal 2 after `set_self(obj)`? // A: // R: - + EXPECT_EQ(use_count, 2); } - + auto events = EventLog::instance().events(); size_t dtor_count = 0; - + for (const auto& event : events) { if (event.find("::dtor") != std::string::npos) @@ -111,31 +106,30 @@ TEST_F(AntiPatternsTest, HoldingSharedPtrToThis) // Q: After the scope exits, why does `dtor_count` equal 0? What prevents the destructor from being called? // A: // R: - + EXPECT_EQ(dtor_count, 0); } TEST_F(AntiPatternsTest, UsingSharedPtrForUniqueOwnership) { auto ptr = std::make_shared("Unique"); - + long use_count = ptr.use_count(); - // Q: If `use_count` is always 1 throughout the object's lifetime, what runtime overhead does shared_ptr impose compared to unique_ptr? - // A: - // R: - + // Q: If `use_count` is always 1 throughout the object's lifetime, what runtime overhead does shared_ptr impose + // compared to unique_ptr? A: R: + EXPECT_EQ(use_count, 1); } TEST_F(AntiPatternsTest, NotUsingMakeShared) { std::shared_ptr p1(new Tracked("WithNew")); - + auto p2 = std::make_shared("WithMakeShared"); // Q: How many heap allocations does `p1` require compared to `p2`? What gets allocated separately? // A: // R: - + EXPECT_NE(p1, nullptr); EXPECT_NE(p2, nullptr); } @@ -143,36 +137,35 @@ TEST_F(AntiPatternsTest, NotUsingMakeShared) TEST_F(AntiPatternsTest, StoringWeakPtrWhenSharedPtrNeeded) { std::weak_ptr weak; - + { auto shared = std::make_shared("Temporary"); weak = shared; } - + bool expired = weak.expired(); - // Q: What design decision would lead you to store a weak_ptr instead of a shared_ptr? What ownership semantics does this express? - // A: - // R: - + // Q: What design decision would lead you to store a weak_ptr instead of a shared_ptr? What ownership semantics does + // this express? A: R: + EXPECT_TRUE(expired); } TEST_F(AntiPatternsTest, IgnoringWeakPtrLockResult) { std::weak_ptr weak; - + { auto shared = std::make_shared("Temporary"); weak = shared; } - + auto locked = weak.lock(); - + bool is_null = (locked == nullptr); // Q: What runtime failure would occur if you dereferenced `locked` without checking `is_null` first? // A: // R: - + EXPECT_TRUE(is_null); } @@ -180,25 +173,24 @@ TEST_F(AntiPatternsTest, CyclicReferencesWithoutWeakPtr) { // ANTI-PATTERN: Creating cycles without breaking them with weak_ptr // Problem: Memory leak - + class Node { public: - explicit Node(const std::string& name) - : tracked_(name) + explicit Node(const std::string& name) : tracked_(name) { } - + void set_next(std::shared_ptr next) { next_ = next; } - + private: Tracked tracked_; - std::shared_ptr next_; // Should be weak_ptr! + std::shared_ptr next_; // Should be weak_ptr! }; - + { auto node1 = std::make_shared("Node1"); auto node2 = std::make_shared("Node2"); @@ -208,10 +200,10 @@ TEST_F(AntiPatternsTest, CyclicReferencesWithoutWeakPtr) // A: // R: } - + auto events = EventLog::instance().events(); size_t dtor_count = 0; - + for (const auto& event : events) { if (event.find("::dtor") != std::string::npos) @@ -222,37 +214,37 @@ TEST_F(AntiPatternsTest, CyclicReferencesWithoutWeakPtr) // Q: After the scope exits, why does `dtor_count` equal 0? What prevents the reference count from reaching zero? // A: // R: - + EXPECT_EQ(dtor_count, 0); } TEST_F(AntiPatternsTest, UsingGetToCreateNewSharedPtr) { auto original = std::make_shared("Original"); - + Tracked* raw = original.get(); // Q: What is the safe use case for calling `get()` on a shared_ptr? Under what condition does it become dangerous? // A: // R: - + long use_count = original.use_count(); - + EXPECT_EQ(use_count, 1); } TEST_F(AntiPatternsTest, HoldingSharedPtrInUnstableContainer) { std::vector> cache; - + { auto resource = std::make_shared("Cached"); - + cache.push_back(resource); } - + auto events = EventLog::instance().events(); size_t dtor_count = 0; - + for (const auto& event : events) { if (event.find("::dtor") != std::string::npos) @@ -260,13 +252,12 @@ TEST_F(AntiPatternsTest, HoldingSharedPtrInUnstableContainer) ++dtor_count; } } - // Q: After the scope exits, why does `dtor_count` equal 0? What ownership decision does storing `shared_ptr` in the cache express? - // A: - // R: - + // Q: After the scope exits, why does `dtor_count` equal 0? What ownership decision does storing `shared_ptr` in the + // cache express? A: R: + // Q: If the cache stored `weak_ptr` instead, what would `dtor_count` be after the scope exits? // A: // R: - + EXPECT_EQ(dtor_count, 0); } diff --git a/learning_shared_ptr/tests/test_asio_basics.cpp b/learning_shared_ptr/tests/test_asio_basics.cpp deleted file mode 100644 index 02150c9..0000000 --- a/learning_shared_ptr/tests/test_asio_basics.cpp +++ /dev/null @@ -1,677 +0,0 @@ -#include "instrumentation.h" - -#include -#include -#include -#include -#include -#include -#include - -// ============================================================================ -// ASIO CORE CONCEPTS -// ============================================================================ -// -// asio::post(io_context, handler) -// - Submits handler to io_context's task queue -// - Non-blocking: returns immediately -// - Thread-safe: can call from any thread -// - FIFO order: handlers execute in submission order -// -// io_context::run() -// - Blocks calling thread and executes queued handlers -// - Returns when queue is empty (no more work) -// - Can be called from multiple threads (thread pool pattern) -// - Returns: number of handlers executed -// -// Pattern: -// asio::io_context io; -// asio::post(io, []() { /* work */ }); // Queue work -// io.run(); // Execute work (blocks) -// -// ============================================================================ - -class AsioBasicsTest : public ::testing::Test -{ -protected: - void SetUp() override - { - EventLog::instance().clear(); - } -}; - -TEST_F(AsioBasicsTest, Lesson1_BasicPostAndRun) -{ - asio::io_context io; - bool callback_executed = false; - - // Q: What does asio::post() do? - // A: - // R: - - asio::post(io, [&callback_executed]() { callback_executed = true; }); - - // Q: At this point (before io.run()), has the callback executed? - // A: - // R: - - EXPECT_FALSE(callback_executed); - - // Q: What does io.run() do? - // A: - // R: - - io.run(); - - // Q: After io.run(), has the callback executed? - // A: - // R: - - EXPECT_TRUE(callback_executed); -} - -TEST_F(AsioBasicsTest, Lesson2_MultipleCallbacksOrder) -{ - asio::io_context io; - std::vector execution_order; - - // Post 3 callbacks - asio::post(io, [&execution_order]() { execution_order.push_back(1); }); - asio::post(io, [&execution_order]() { execution_order.push_back(2); }); - asio::post(io, [&execution_order]() { execution_order.push_back(3); }); - - // Q: In what order will the callbacks execute? - // A: - // R: - - io.run(); - - // Q: Why does asio::post() guarantee FIFO (first-in-first-out) order? - // A: - // R: - - EXPECT_EQ(execution_order.size(), 3); - EXPECT_EQ(execution_order[0], 1); - EXPECT_EQ(execution_order[1], 2); - EXPECT_EQ(execution_order[2], 3); -} - -TEST_F(AsioBasicsTest, Lesson3_RunInSeparateThread) -{ - asio::io_context io; - std::atomic callback_executed{false}; - - asio::post(io, [&callback_executed]() { callback_executed = true; }); - - // Q: What happens when we run io.run() in a separate thread? - // A: - // R: - - std::thread io_thread([&io]() { - io.run(); // Runs in separate thread - }); - - // Q: Why do we need to join the thread? - // A: - // R: - - io_thread.join(); - - EXPECT_TRUE(callback_executed); -} - -// ============================================================================ -// LESSON 4: Capturing Variables by Value vs Reference -// ============================================================================ - -TEST_F(AsioBasicsTest, Lesson4_CaptureByValueVsReference) -{ - asio::io_context io; - int value = 10; - int result_by_value = 0; - int result_by_reference = 0; - - // Capture by value - asio::post(io, [value, &result_by_value]() { result_by_value = value; }); - - // Capture by reference - asio::post(io, [&value, &result_by_reference]() { result_by_reference = value; }); - - // Change value before running - value = 20; - - // Q: What will result_by_value be? Why? - // A: - // R: - - // Q: What will result_by_reference be? Why? - // A: - // R: - - io.run(); - - EXPECT_EQ(result_by_value, 10); // Captured value at time of post - EXPECT_EQ(result_by_reference, 20); // Captured reference, sees changed value -} - -// ============================================================================ -// LESSON 5: Basic shared_ptr with asio::post() -// ============================================================================ - -TEST_F(AsioBasicsTest, Lesson5_SharedPtrWithPost) -{ - asio::io_context io; - long use_count_in_callback = 0; - - { - std::shared_ptr ptr = std::make_shared("Obj1"); - - // Q: What is the use_count before posting? - // A: - // R: - - long before_post = ptr.use_count(); - EXPECT_EQ(before_post, 1); - - // Capture shared_ptr by value - asio::post(io, [ptr, &use_count_in_callback]() { use_count_in_callback = ptr.use_count(); }); - - // Q: What is the use_count after posting but before io.run()? - // A: - // R: - - long after_post = ptr.use_count(); - EXPECT_EQ(after_post, 2); // ptr + lambda's copy - - // Q: When this scope ends, what happens to ptr? - // A: - // R: - } - - // Q: Is the Tracked object destroyed at this point? - // A: - // R: - - io.run(); - - // Q: What was the use_count inside the callback? - // A: - // R: - - EXPECT_EQ(use_count_in_callback, 1); // Only lambda's copy remains -} - -// ============================================================================ -// LESSON 6: Danger of Raw Pointers with asio::post() -// ============================================================================ - -TEST_F(AsioBasicsTest, Lesson6_RawPointerDanger) -{ - asio::io_context io; - bool callback_executed = false; - - Tracked* raw_ptr = nullptr; - - { - std::shared_ptr ptr = std::make_shared("Obj2"); - raw_ptr = ptr.get(); - - // DANGER: Capturing raw pointer - asio::post(io, [raw_ptr, &callback_executed]() { - // Q: Is it safe to use raw_ptr here? - // A: - // R: - - // We won't actually dereference it to avoid crash - callback_executed = true; - }); - - // Q: When ptr goes out of scope, what happens to the object? - // A: - // R: - } - - // Q: At this point, what does raw_ptr point to? - // A: - // R: - - io.run(); - - EXPECT_TRUE(callback_executed); - // NOTE: Dereferencing raw_ptr here would be undefined behavior! -} - -// ============================================================================ -// LESSON 7: Safe Pattern - Capturing shared_ptr -// ============================================================================ - -TEST_F(AsioBasicsTest, Lesson7_SafeSharedPtrCapture) -{ - asio::io_context io; - std::string result; - - { - std::shared_ptr ptr = std::make_shared("Obj3"); - - // SAFE: Capture shared_ptr by value - asio::post(io, [ptr, &result]() { - // Q: Why is it safe to use ptr here? - // A: - // R: - - result = ptr->name(); - }); - - // Q: Even though ptr goes out of scope, why isn't the object destroyed? - // A: - // R: - } - - io.run(); - - // Q: When is the Tracked object finally destroyed? - // A: - // R: - - EXPECT_EQ(result, "Obj3"); -} - -// ============================================================================ -// LESSON 8: Multiple Threads Running io_context -// ============================================================================ - -TEST_F(AsioBasicsTest, Lesson8_MultipleThreadsRunningIoContext) -{ - asio::io_context io; - std::atomic tasks_executed{0}; - - // Post 10 tasks - for (int i = 0; i < 10; ++i) - { - asio::post(io, [&tasks_executed]() { - ++tasks_executed; - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - }); - } - - // Q: What happens when multiple threads call io.run()? - // A: - // R: - - std::vector threads; - for (int i = 0; i < 3; ++i) - { - threads.emplace_back([&io]() { - io.run(); // Multiple threads processing the same queue - }); - } - - for (auto& t : threads) - { - t.join(); - } - - // Q: How many tasks were executed? - // A: - // R: - - EXPECT_EQ(tasks_executed, 10); -} - -// ============================================================================ -// LESSON 9: Basic Timer (asio::steady_timer) -// ============================================================================ - -TEST_F(AsioBasicsTest, Lesson9_BasicTimer) -{ - asio::io_context io; - bool timer_fired = false; - - asio::steady_timer timer(io); - - // Q: What does expires_after() do? - // A: - // R: - - timer.expires_after(std::chrono::milliseconds(50)); - - // Q: What does async_wait() do? - // A: - // R: - - timer.async_wait([&timer_fired](const asio::error_code& ec) { - if (!ec) - { - timer_fired = true; - } - }); - - // Q: At this point, has the timer fired? - // A: - // R: - - EXPECT_FALSE(timer_fired); - - // Q: What happens when io.run() is called? - // A: - // R: - - io.run(); // Blocks until timer fires - - EXPECT_TRUE(timer_fired); -} - -// ============================================================================ -// LESSON 10: Timer with shared_ptr Lifetime -// ============================================================================ - -TEST_F(AsioBasicsTest, Lesson10_TimerWithSharedPtrLifetime) -{ - asio::io_context io; - std::string result; - asio::steady_timer timer(io); - - { - std::shared_ptr ptr = std::make_shared("TimerObj"); - - timer.expires_after(std::chrono::milliseconds(50)); - - // Q: What happens if we capture ptr by value in the timer callback? - // A: - // R: - - timer.async_wait([ptr, &result](const asio::error_code& ec) { - if (!ec) - { - result = ptr->name(); - } - }); - - // Q: When this scope ends, is the Tracked object destroyed? - // A: - // R: - } - - io.run(); - - // Q: Why was the object still alive when the timer fired? - // A: - // R: - - EXPECT_EQ(result, "TimerObj"); -} - -// ============================================================================ -// LESSON 11: std::bind Basics -// ============================================================================ - -class SimpleClass -{ -public: - explicit SimpleClass(const std::string& name) : tracked_(name) - { - } - - void method(int value) - { - last_value_ = value; - } - - int last_value() const - { - return last_value_; - } - -private: - Tracked tracked_; - int last_value_ = 0; -}; - -TEST_F(AsioBasicsTest, Lesson11_StdBindBasics) -{ - asio::io_context io; - - std::shared_ptr obj = std::make_shared("BindObj"); - - // Q: What does std::bind do? - // A: - // R: - - // Using std::bind to bind member function - asio::post(io, std::bind(&SimpleClass::method, obj, 42)); - - // Q: What arguments does std::bind capture? - // A: - // R: - - io.run(); - - EXPECT_EQ(obj->last_value(), 42); -} - -// ============================================================================ -// LESSON 12: std::bind vs Lambda -// ============================================================================ - -TEST_F(AsioBasicsTest, Lesson12_BindVsLambda) -{ - asio::io_context io; - - std::shared_ptr obj = std::make_shared("CompareObj"); - - // Using std::bind - asio::post(io, std::bind(&SimpleClass::method, obj, 10)); - - // Using lambda (equivalent) - asio::post(io, [obj]() { obj->method(20); }); - - // Q: Which is more readable: std::bind or lambda? - // A: - // R: - - // Q: When would you prefer std::bind over lambda? - // A: - // R: - - io.run(); - - EXPECT_EQ(obj->last_value(), 20); // Lambda executed second -} - -// ============================================================================ -// LESSON 13: Synchronization with std::mutex -// ============================================================================ - -TEST_F(AsioBasicsTest, Lesson13_BasicMutex) -{ - asio::io_context io; - int counter = 0; - std::mutex mutex; - - // Post 100 tasks that increment counter - for (int i = 0; i < 100; ++i) - { - asio::post(io, [&counter, &mutex]() { - // Q: Why do we need std::lock_guard here? - // A: - // R: - - std::lock_guard lock(mutex); - ++counter; - }); - } - - // Run with multiple threads - std::vector threads; - for (int i = 0; i < 4; ++i) - { - threads.emplace_back([&io]() { io.run(); }); - } - - for (auto& t : threads) - { - t.join(); - } - - // Q: Without the mutex, what could happen to counter? - // A: - // R: - - EXPECT_EQ(counter, 100); -} - -// ============================================================================ -// LESSON 14: Atomic Variables -// ============================================================================ - -TEST_F(AsioBasicsTest, Lesson14_AtomicVariables) -{ - asio::io_context io; - std::atomic atomic_counter{0}; - int regular_counter = 0; - std::mutex mutex; - - // Post 100 tasks - for (int i = 0; i < 100; ++i) - { - asio::post(io, [&atomic_counter, ®ular_counter, &mutex]() { - // Atomic increment (thread-safe without mutex) - ++atomic_counter; - - // Regular increment (needs mutex) - std::lock_guard lock(mutex); - ++regular_counter; - }); - } - - // Q: What is the difference between std::atomic and regular int? - // A: - // R: - - // Q: When should you use std::atomic vs std::mutex? - // A: - // R: - - std::vector threads; - for (int i = 0; i < 4; ++i) - { - threads.emplace_back([&io]() { io.run(); }); - } - - for (auto& t : threads) - { - t.join(); - } - - EXPECT_EQ(atomic_counter, 100); - EXPECT_EQ(regular_counter, 100); -} - -// ============================================================================ -// LESSON 15: Putting It All Together - Safe Async Pattern -// ============================================================================ - -class AsyncWorker : public std::enable_shared_from_this -{ -public: - explicit AsyncWorker(const std::string& name) : tracked_(name), completed_(false) - { - } - - void start_work(asio::io_context& io) - { - // Q: Why do we use shared_from_this() here? - // A: - // R: - - asio::post(io, std::bind(&AsyncWorker::do_work, shared_from_this())); - } - - bool is_completed() const - { - return completed_; - } - -private: - void do_work() - { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - completed_ = true; - } - - Tracked tracked_; - bool completed_; -}; - -TEST_F(AsioBasicsTest, Lesson15_SafeAsyncPattern) -{ - asio::io_context io; - - { - std::shared_ptr worker = std::make_shared("Worker1"); - - // Q: What keeps the worker alive after this scope ends? - // A: - // R: - - worker->start_work(io); - - // Q: Is it safe for worker to go out of scope here? - // A: - // R: - } - - // Q: When will the AsyncWorker object be destroyed? - // A: - // R: - - io.run(); - - // At this point, the worker has been destroyed - // (after do_work() completed and the callback finished) -} - -// ============================================================================ -// SUMMARY TEST: All Concepts Together -// ============================================================================ - -TEST_F(AsioBasicsTest, Summary_AllConceptsTogether) -{ - asio::io_context io; - std::atomic tasks_completed{0}; - - // Create multiple workers - for (int i = 0; i < 5; ++i) - { - std::shared_ptr worker = std::make_shared("Worker" + std::to_string(i)); - - // Post work using lambda with shared_ptr capture - asio::post(io, [worker, &tasks_completed]() { - // Simulate work - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - ++tasks_completed; - }); - } - - // Run on multiple threads - std::vector threads; - for (int i = 0; i < 3; ++i) - { - threads.emplace_back([&io]() { io.run(); }); - } - - for (auto& t : threads) - { - t.join(); - } - - // Q: Review - What are the key concepts demonstrated in this test? - // A: - // A: - // A: - // A: - // A: - // R: - - EXPECT_EQ(tasks_completed, 5); -} diff --git a/learning_shared_ptr/tests/test_collection_patterns.cpp b/learning_shared_ptr/tests/test_collection_patterns.cpp index a48e846..24b6a5b 100644 --- a/learning_shared_ptr/tests/test_collection_patterns.cpp +++ b/learning_shared_ptr/tests/test_collection_patterns.cpp @@ -1,9 +1,10 @@ #include "instrumentation.h" + #include -#include #include -#include +#include #include +#include class CollectionPatternsTest : public ::testing::Test { @@ -17,16 +18,15 @@ class CollectionPatternsTest : public ::testing::Test class CachedResource { public: - explicit CachedResource(const std::string& name) - : tracked_(name) + explicit CachedResource(const std::string& name) : tracked_(name) { } - + std::string name() const { return tracked_.name(); } - + private: Tracked tracked_; }; @@ -38,29 +38,29 @@ class ResourceCache std::shared_ptr get(const std::string& key) { auto it = cache_.find(key); - + if (it != cache_.end()) { std::shared_ptr resource = it->second.lock(); - + if (resource) { return resource; } - + cache_.erase(it); } - + std::shared_ptr new_resource = std::make_shared(key); cache_[key] = new_resource; return new_resource; } - + size_t size() const { return cache_.size(); } - + void cleanup() { for (auto it = cache_.begin(); it != cache_.end();) @@ -75,7 +75,7 @@ class ResourceCache } } } - + private: std::map> cache_; }; @@ -83,25 +83,25 @@ class ResourceCache TEST_F(CollectionPatternsTest, WeakPtrCacheBasic) { ResourceCache cache; - + size_t initial_size = cache.size(); - + auto r1 = cache.get("Resource1"); - + size_t after_first_get = cache.size(); - + auto r2 = cache.get("Resource1"); - + size_t after_second_get = cache.size(); // Q: Why does `after_second_get` remain at 1 instead of increasing to 2? // A: // R: - + long use_count = r1.use_count(); // Q: What are the two owners contributing to `use_count == 2`? // A: // R: - + EXPECT_EQ(initial_size, 0); EXPECT_EQ(after_first_get, 1); EXPECT_EQ(after_second_get, 1); @@ -111,23 +111,23 @@ TEST_F(CollectionPatternsTest, WeakPtrCacheBasic) TEST_F(CollectionPatternsTest, WeakPtrCacheExpiration) { ResourceCache cache; - + { auto temp = cache.get("Temp"); } - + size_t before_cleanup = cache.size(); // Q: After the scope exits, why does `before_cleanup` still equal 1? What does the cache still contain? // A: // R: - + cache.cleanup(); - + size_t after_cleanup = cache.size(); // Q: What operation in `cleanup()` causes `after_cleanup` to become 0? // A: // R: - + EXPECT_EQ(before_cleanup, 1); EXPECT_EQ(after_cleanup, 0); } @@ -135,29 +135,27 @@ TEST_F(CollectionPatternsTest, WeakPtrCacheExpiration) TEST_F(CollectionPatternsTest, WeakPtrCacheAutoRecreate) { ResourceCache cache; - + { auto r1 = cache.get("AutoRecreate"); } - + auto r2 = cache.get("AutoRecreate"); - + long use_count = r2.use_count(); - // Q: After the first scope exits, what happens when `cache.get("AutoRecreate")` is called again? Walk through the logic in `get()`. - // A: - // R: - + // Q: After the first scope exits, what happens when `cache.get("AutoRecreate")` is called again? Walk through the + // logic in `get()`. A: R: + EXPECT_EQ(use_count, 1); } class Event { public: - explicit Event(const std::string& name) - : tracked_(name) + explicit Event(const std::string& name) : tracked_(name) { } - + private: Tracked tracked_; }; @@ -165,15 +163,14 @@ class Event class Observer { public: - explicit Observer(const std::string& name) - : tracked_(name) + explicit Observer(const std::string& name) : tracked_(name) { } - + void notify(const Event& event) { } - + private: Tracked tracked_; }; @@ -186,13 +183,13 @@ class Subject { observers_.push_back(observer); } - + void notify_all(const Event& event) { for (auto it = observers_.begin(); it != observers_.end();) { std::shared_ptr observer = it->lock(); - + if (observer) { observer->notify(event); @@ -204,12 +201,12 @@ class Subject } } } - + size_t observer_count() const { return observers_.size(); } - + private: std::vector> observers_; }; @@ -217,48 +214,47 @@ class Subject TEST_F(CollectionPatternsTest, ObserverPatternBasic) { Subject subject; - + auto obs1 = std::make_shared("Obs1"); auto obs2 = std::make_shared("Obs2"); - + subject.attach(obs1); subject.attach(obs2); - + size_t count = subject.observer_count(); - + Event event("Event1"); subject.notify_all(event); - // Q: Why does `Subject` store `weak_ptr` instead of `shared_ptr`? What problem does this prevent? - // A: - // R: - + // Q: Why does `Subject` store `weak_ptr` instead of `shared_ptr`? What problem does this + // prevent? A: R: + EXPECT_EQ(count, 2); } TEST_F(CollectionPatternsTest, ObserverPatternAutoRemoval) { Subject subject; - + { auto obs1 = std::make_shared("Obs1"); auto obs2 = std::make_shared("Obs2"); subject.attach(obs1); subject.attach(obs2); } - + size_t before_notify = subject.observer_count(); // Q: After the scope exits, why does `before_notify` still equal 2? What does the vector still contain? // A: // R: - + Event event("Event1"); subject.notify_all(event); - + size_t after_notify = subject.observer_count(); // Q: What operation in `notify_all()` causes `after_notify` to become 0? // A: // R: - + EXPECT_EQ(before_notify, 2); EXPECT_EQ(after_notify, 0); } @@ -266,26 +262,25 @@ TEST_F(CollectionPatternsTest, ObserverPatternAutoRemoval) TEST_F(CollectionPatternsTest, ObserverPatternPartialExpiration) { Subject subject; - + auto persistent = std::make_shared("Persistent"); - + subject.attach(persistent); - + { auto temporary = std::make_shared("Temporary"); subject.attach(temporary); } - + size_t before_notify = subject.observer_count(); - + Event event("Event1"); subject.notify_all(event); - + size_t after_notify = subject.observer_count(); - // Q: Why does `after_notify` equal 1 instead of 0? What differentiates the persistent observer from the temporary one? - // A: - // R: - + // Q: Why does `after_notify` equal 1 instead of 0? What differentiates the persistent observer from the temporary + // one? A: R: + EXPECT_EQ(before_notify, 2); EXPECT_EQ(after_notify, 1); } @@ -293,16 +288,15 @@ TEST_F(CollectionPatternsTest, ObserverPatternPartialExpiration) class RegistryEntry { public: - explicit RegistryEntry(const std::string& name) - : tracked_(name) + explicit RegistryEntry(const std::string& name) : tracked_(name) { } - + std::string name() const { return tracked_.name(); } - + private: Tracked tracked_; }; @@ -315,24 +309,24 @@ class Registry { entries_[key] = entry; } - + std::shared_ptr lookup(const std::string& key) { auto it = entries_.find(key); - + if (it != entries_.end()) { return it->second.lock(); } - + return nullptr; } - + size_t size() const { return entries_.size(); } - + void cleanup() { for (auto it = entries_.begin(); it != entries_.end();) @@ -347,7 +341,7 @@ class Registry } } } - + private: std::map> entries_; }; @@ -355,33 +349,31 @@ class Registry TEST_F(CollectionPatternsTest, RegistryPattern) { Registry registry; - + { auto entry1 = std::make_shared("Entry1"); registry.register_entry("key1", entry1); } - + auto entry2 = std::make_shared("Entry2"); registry.register_entry("key2", entry2); - + size_t before_cleanup = registry.size(); - // Q: After entry1's scope exits, why does `before_cleanup` still equal 2? What does the registry still contain for "key1"? - // A: - // R: - + // Q: After entry1's scope exits, why does `before_cleanup` still equal 2? What does the registry still contain for + // "key1"? A: R: + registry.cleanup(); - + size_t after_cleanup = registry.size(); - + auto lookup1 = registry.lookup("key1"); auto lookup2 = registry.lookup("key2"); - + bool lookup1_null = (lookup1 == nullptr); bool lookup2_not_null = (lookup2 != nullptr); - // Q: Why does `lookup1` return nullptr while `lookup2` returns a valid pointer? What differentiates their states in the registry? - // A: - // R: - + // Q: Why does `lookup1` return nullptr while `lookup2` returns a valid pointer? What differentiates their states in + // the registry? A: R: + EXPECT_EQ(before_cleanup, 2); EXPECT_EQ(after_cleanup, 1); EXPECT_TRUE(lookup1_null); @@ -391,20 +383,19 @@ TEST_F(CollectionPatternsTest, RegistryPattern) TEST_F(CollectionPatternsTest, MultipleObserversSharedLifetime) { Subject subject; - + auto obs1 = std::make_shared("Obs1"); - + subject.attach(obs1); subject.attach(obs1); subject.attach(obs1); - + size_t count = subject.observer_count(); - + long use_count = obs1.use_count(); - // Q: Why does `use_count` remain at 1 despite attaching the same observer three times? What does this reveal about weak_ptr's impact on reference counting? - // A: - // R: - + // Q: Why does `use_count` remain at 1 despite attaching the same observer three times? What does this reveal about + // weak_ptr's impact on reference counting? A: R: + EXPECT_EQ(count, 3); EXPECT_EQ(use_count, 1); } diff --git a/learning_shared_ptr/tests/test_conditional_lifetime.cpp b/learning_shared_ptr/tests/test_conditional_lifetime.cpp index 547dab5..e8de587 100644 --- a/learning_shared_ptr/tests/test_conditional_lifetime.cpp +++ b/learning_shared_ptr/tests/test_conditional_lifetime.cpp @@ -1,4 +1,5 @@ #include "instrumentation.h" + #include #include #include @@ -19,24 +20,24 @@ class LazyResource LazyResource() { } - + std::shared_ptr get_resource() { if (auto cached = resource_.lock()) { return cached; } - + auto new_resource = std::make_shared("LazyInit"); resource_ = new_resource; return new_resource; } - + bool is_initialized() const { return !resource_.expired(); } - + private: mutable std::weak_ptr resource_; }; @@ -44,20 +45,20 @@ class LazyResource TEST_F(ConditionalLifetimeTest, LazyInitializationBasic) { LazyResource lazy; - + bool initialized_before = lazy.is_initialized(); - + auto r1 = lazy.get_resource(); - + bool initialized_after = lazy.is_initialized(); - + auto r2 = lazy.get_resource(); - + long use_count = r1.use_count(); // Q: Why does `use_count` equal 2 after two calls to `get_resource()`? // A: // R: - + EXPECT_FALSE(initialized_before); EXPECT_TRUE(initialized_after); EXPECT_EQ(use_count, 2); @@ -66,16 +67,16 @@ TEST_F(ConditionalLifetimeTest, LazyInitializationBasic) TEST_F(ConditionalLifetimeTest, LazyInitializationMultipleCalls) { LazyResource lazy; - + auto r1 = lazy.get_resource(); auto r2 = lazy.get_resource(); auto r3 = lazy.get_resource(); - + long use_count = r1.use_count(); // Q: What happens to the weak_ptr stored in `resource_` after the first call to `get_resource()`? // A: // R: - + EXPECT_EQ(use_count, 3); } @@ -83,16 +84,15 @@ TEST_F(ConditionalLifetimeTest, LazyInitializationMultipleCalls) class CopyOnWriteString { public: - explicit CopyOnWriteString(const std::string& str) - : data_(std::make_shared(str)) + explicit CopyOnWriteString(const std::string& str) : data_(std::make_shared(str)) { } - + std::string get() const { return data_->name(); } - + void set(const std::string& str) { if (data_.use_count() > 1) @@ -104,12 +104,12 @@ class CopyOnWriteString data_ = std::make_shared(str); } } - + long use_count() const { return data_.use_count(); } - + private: std::shared_ptr data_; }; @@ -117,23 +117,22 @@ class CopyOnWriteString TEST_F(ConditionalLifetimeTest, CopyOnWriteBasic) { CopyOnWriteString s1("Original"); - + long use_count_single = s1.use_count(); - + CopyOnWriteString s2 = s1; - + long use_count_shared = s1.use_count(); // Q: After copying `s1` to `s2`, what does `use_count_shared == 2` tell you about the underlying Tracked object? // A: // R: - + s2.set("Modified"); - + long use_count_after_write = s2.use_count(); - // Q: Why does `s2.set()` result in `use_count_after_write == 1`? What observable signal in EventLog would confirm a new allocation occurred? - // A: - // R: - + // Q: Why does `s2.set()` result in `use_count_after_write == 1`? What observable signal in EventLog would confirm a + // new allocation occurred? A: R: + EXPECT_EQ(use_count_single, 1); EXPECT_EQ(use_count_shared, 2); EXPECT_EQ(use_count_after_write, 1); @@ -142,19 +141,19 @@ TEST_F(ConditionalLifetimeTest, CopyOnWriteBasic) TEST_F(ConditionalLifetimeTest, CopyOnWriteMultipleCopies) { CopyOnWriteString s1("Shared"); - + CopyOnWriteString s2 = s1; CopyOnWriteString s3 = s1; - + long use_count_before = s1.use_count(); - + s2.set("Modified"); - + long use_count_after = s1.use_count(); // Q: After `s2.set()`, why does `s1.use_count()` drop from 3 to 2 instead of remaining at 3? // A: // R: - + EXPECT_EQ(use_count_before, 3); EXPECT_EQ(use_count_after, 2); } @@ -163,11 +162,10 @@ TEST_F(ConditionalLifetimeTest, CopyOnWriteMultipleCopies) class DeferredConstruction { public: - DeferredConstruction() - : resource_(nullptr) + DeferredConstruction() : resource_(nullptr) { } - + void initialize(const std::string& name) { if (!resource_) @@ -175,17 +173,17 @@ class DeferredConstruction resource_ = std::make_shared(name); } } - + std::shared_ptr get() const { return resource_; } - + bool is_initialized() const { return resource_ != nullptr; } - + private: std::shared_ptr resource_; }; @@ -193,20 +191,20 @@ class DeferredConstruction TEST_F(ConditionalLifetimeTest, DeferredConstructionPattern) { DeferredConstruction deferred; - + bool initialized_before = deferred.is_initialized(); - + deferred.initialize("Deferred"); - + bool initialized_after = deferred.is_initialized(); - + auto resource = deferred.get(); - + long use_count = resource.use_count(); // Q: Why does `use_count` equal 2 after calling `get()`? What are the two owners? // A: // R: - + EXPECT_FALSE(initialized_before); EXPECT_TRUE(initialized_after); EXPECT_EQ(use_count, 2); @@ -215,20 +213,20 @@ TEST_F(ConditionalLifetimeTest, DeferredConstructionPattern) TEST_F(ConditionalLifetimeTest, DeferredConstructionIdempotent) { DeferredConstruction deferred; - + deferred.initialize("First"); - + auto r1 = deferred.get(); - + deferred.initialize("Second"); - + auto r2 = deferred.get(); - + bool same_resource = (r1 == r2); // Q: Given that `initialize()` was called twice with different names, why does `same_resource` evaluate to true? // A: // R: - + EXPECT_TRUE(same_resource); } @@ -236,31 +234,30 @@ TEST_F(ConditionalLifetimeTest, DeferredConstructionIdempotent) class ConditionalOwnership { public: - ConditionalOwnership() - : owned_(nullptr) + ConditionalOwnership() : owned_(nullptr) { } - + void take_ownership(std::shared_ptr resource) { owned_ = resource; } - + void release_ownership() { owned_.reset(); } - + bool has_ownership() const { return owned_ != nullptr; } - + long use_count() const { return owned_ ? owned_.use_count() : 0; } - + private: std::shared_ptr owned_; }; @@ -268,29 +265,27 @@ class ConditionalOwnership TEST_F(ConditionalLifetimeTest, ConditionalOwnershipPattern) { ConditionalOwnership owner; - + bool has_ownership_initial = owner.has_ownership(); - + auto resource = std::make_shared("Resource"); - + long use_count_before = resource.use_count(); - + owner.take_ownership(resource); - + long use_count_after = resource.use_count(); bool has_ownership_after = owner.has_ownership(); - // Q: After `take_ownership()`, what prevents the Tracked object from being destroyed if `resource` goes out of scope? - // A: - // R: - + // Q: After `take_ownership()`, what prevents the Tracked object from being destroyed if `resource` goes out of + // scope? A: R: + owner.release_ownership(); - + long use_count_released = resource.use_count(); bool has_ownership_released = owner.has_ownership(); - // Q: After `release_ownership()`, what observable signal confirms the owner no longer participates in reference counting? - // A: - // R: - + // Q: After `release_ownership()`, what observable signal confirms the owner no longer participates in reference + // counting? A: R: + EXPECT_FALSE(has_ownership_initial); EXPECT_EQ(use_count_before, 1); EXPECT_EQ(use_count_after, 2); @@ -311,10 +306,10 @@ class ResourcePool pool_.pop_back(); return resource; } - + return std::make_shared(name); } - + void release(std::shared_ptr resource) { if (resource.use_count() == 1) @@ -322,12 +317,12 @@ class ResourcePool pool_.push_back(resource); } } - + size_t pool_size() const { return pool_.size(); } - + private: std::vector> pool_; }; @@ -335,20 +330,19 @@ class ResourcePool TEST_F(ConditionalLifetimeTest, ResourcePoolPattern) { ResourcePool pool; - + size_t initial_size = pool.pool_size(); - + auto resource = pool.acquire("Resource1"); - + size_t after_acquire = pool.pool_size(); - + pool.release(resource); - + size_t after_release = pool.pool_size(); - // Q: Why does `after_release` equal 0 instead of 1? What condition in `release()` prevents the resource from being pooled? - // A: - // R: - + // Q: Why does `after_release` equal 0 instead of 1? What condition in `release()` prevents the resource from being + // pooled? A: R: + EXPECT_EQ(initial_size, 0); EXPECT_EQ(after_acquire, 0); EXPECT_EQ(after_release, 0); @@ -357,24 +351,24 @@ TEST_F(ConditionalLifetimeTest, ResourcePoolPattern) TEST_F(ConditionalLifetimeTest, ResourcePoolReuse) { ResourcePool pool; - + { auto r1 = pool.acquire("Resource1"); pool.release(std::move(r1)); } - + size_t pool_size = pool.pool_size(); // Q: After the scope exits, why does `pool_size` equal 1? What prevents the pooled resource from being destroyed? // A: // R: - + auto r2 = pool.acquire("Resource2"); - + size_t after_reacquire = pool.pool_size(); // Q: Why does `after_reacquire` equal 0? What happened to the pooled resource from the previous scope? // A: // R: - + EXPECT_EQ(pool_size, 1); EXPECT_EQ(after_reacquire, 0); } @@ -383,31 +377,30 @@ TEST_F(ConditionalLifetimeTest, ResourcePoolReuse) class OptionalResource { public: - OptionalResource() - : resource_(nullptr) + OptionalResource() : resource_(nullptr) { } - + void set(std::shared_ptr resource) { resource_ = resource; } - + std::shared_ptr get() const { return resource_; } - + bool has_value() const { return resource_ != nullptr; } - + void reset() { resource_.reset(); } - + private: std::shared_ptr resource_; }; @@ -415,23 +408,23 @@ class OptionalResource TEST_F(ConditionalLifetimeTest, OptionalResourcePattern) { OptionalResource optional; - + bool has_value_initial = optional.has_value(); - + auto resource = std::make_shared("Resource"); optional.set(resource); - + bool has_value_set = optional.has_value(); long use_count = resource.use_count(); - + optional.reset(); - + bool has_value_reset = optional.has_value(); long use_count_after_reset = resource.use_count(); // Q: After `optional.reset()`, what guarantees that the Tracked object remains valid for `resource` to use? // A: // R: - + EXPECT_FALSE(has_value_initial); EXPECT_TRUE(has_value_set); EXPECT_EQ(use_count, 2); @@ -442,16 +435,15 @@ TEST_F(ConditionalLifetimeTest, OptionalResourcePattern) TEST_F(ConditionalLifetimeTest, UseCountBasedDecision) { auto ptr = std::make_shared("Unique"); - + bool is_unique_before = (ptr.use_count() == 1); - + auto copy = ptr; - + bool is_unique_after = (ptr.use_count() == 1); - // Q: If you used `use_count() == 1` to decide whether to modify the object in-place, what race condition could occur in a multi-threaded context? - // A: - // R: - + // Q: If you used `use_count() == 1` to decide whether to modify the object in-place, what race condition could + // occur in a multi-threaded context? A: R: + EXPECT_TRUE(is_unique_before); EXPECT_FALSE(is_unique_after); } diff --git a/learning_shared_ptr/tests/test_interop_patterns.cpp b/learning_shared_ptr/tests/test_interop_patterns.cpp index 70e5d93..645b763 100644 --- a/learning_shared_ptr/tests/test_interop_patterns.cpp +++ b/learning_shared_ptr/tests/test_interop_patterns.cpp @@ -1,8 +1,9 @@ #include "instrumentation.h" -#include -#include + #include #include +#include +#include class InteropPatternsTest : public ::testing::Test { @@ -29,20 +30,20 @@ TEST_F(InteropPatternsTest, FileHandleCustomDeleter) { { FILE* file = std::tmpfile(); - + std::shared_ptr file_ptr(file, FileCloser()); - + long use_count = file_ptr.use_count(); - + bool not_null = (file_ptr.get() != nullptr); - + EXPECT_EQ(use_count, 1); EXPECT_TRUE(not_null); } - + auto events = EventLog::instance().events(); bool closer_called = false; - + for (const auto& event : events) { if (event.find("FileCloser::operator()") != std::string::npos) @@ -50,10 +51,9 @@ TEST_F(InteropPatternsTest, FileHandleCustomDeleter) closer_called = true; } } - // Q: What guarantees that `FileCloser::operator()` is called when the scope exits? What would happen if you used `delete` as the deleter instead? - // A: - // R: - + // Q: What guarantees that `FileCloser::operator()` is called when the scope exits? What would happen if you used + // `delete` as the deleter instead? A: R: + EXPECT_TRUE(closer_called); } @@ -73,20 +73,20 @@ TEST_F(InteropPatternsTest, MallocBufferCustomDeleter) { { char* buffer = static_cast(std::malloc(1024)); - + std::shared_ptr buffer_ptr(buffer, BufferDeleter()); - + long use_count = buffer_ptr.use_count(); - + bool not_null = (buffer_ptr.get() != nullptr); - + EXPECT_EQ(use_count, 1); EXPECT_TRUE(not_null); } - + auto events = EventLog::instance().events(); bool deleter_called = false; - + for (const auto& event : events) { if (event.find("BufferDeleter::operator()") != std::string::npos) @@ -94,10 +94,9 @@ TEST_F(InteropPatternsTest, MallocBufferCustomDeleter) deleter_called = true; } } - // Q: Why is a custom deleter necessary for malloc-allocated memory? What would happen if shared_ptr used its default deleter? - // A: - // R: - + // Q: Why is a custom deleter necessary for malloc-allocated memory? What would happen if shared_ptr used its + // default deleter? A: R: + EXPECT_TRUE(deleter_called); } @@ -112,19 +111,18 @@ void c_api_function(Tracked* raw_ptr) TEST_F(InteropPatternsTest, SafeGetUsageForCAPI) { auto shared = std::make_shared("Shared"); - + long before_call = shared.use_count(); - + c_api_function(shared.get()); - + long after_call = shared.use_count(); - // Q: Why does `use_count` remain at 1 before and after the C API call? What guarantee must hold for this pattern to be safe? - // A: - // R: - + // Q: Why does `use_count` remain at 1 before and after the C API call? What guarantee must hold for this pattern to + // be safe? A: R: + auto events = EventLog::instance().events(); bool api_called = false; - + for (const auto& event : events) { if (event.find("c_api_function") != std::string::npos) @@ -132,7 +130,7 @@ TEST_F(InteropPatternsTest, SafeGetUsageForCAPI) api_called = true; } } - + EXPECT_EQ(before_call, 1); EXPECT_EQ(after_call, 1); EXPECT_TRUE(api_called); @@ -141,12 +139,11 @@ TEST_F(InteropPatternsTest, SafeGetUsageForCAPI) struct ResourceHandle { int fd; - explicit ResourceHandle(int descriptor) - : fd(descriptor) + explicit ResourceHandle(int descriptor) : fd(descriptor) { EventLog::instance().record("ResourceHandle created"); } - + ~ResourceHandle() { EventLog::instance().record("ResourceHandle destroyed"); @@ -165,21 +162,18 @@ struct ResourceHandleDeleter TEST_F(InteropPatternsTest, RAIIWrapperForCResource) { { - std::shared_ptr handle( - new ResourceHandle(42), - ResourceHandleDeleter() - ); - + std::shared_ptr handle(new ResourceHandle(42), ResourceHandleDeleter()); + long use_count = handle.use_count(); - + EXPECT_EQ(use_count, 1); } - + auto events = EventLog::instance().events(); bool handle_created = false; bool handle_destroyed = false; bool deleter_called = false; - + for (const auto& event : events) { if (event.find("ResourceHandle created") != std::string::npos) @@ -195,10 +189,9 @@ TEST_F(InteropPatternsTest, RAIIWrapperForCResource) deleter_called = true; } } - // Q: What is the order of EventLog entries? Which appears first: "ResourceHandleDeleter::operator()" or "ResourceHandle destroyed"? - // A: - // R: - + // Q: What is the order of EventLog entries? Which appears first: "ResourceHandleDeleter::operator()" or + // "ResourceHandle destroyed"? A: R: + EXPECT_TRUE(handle_created); EXPECT_TRUE(handle_destroyed); EXPECT_TRUE(deleter_called); @@ -219,22 +212,20 @@ TEST_F(InteropPatternsTest, BridgingCAndCppOwnership) { { Tracked* raw = create_tracked_c_style("Bridged"); - - std::shared_ptr cpp_owned(raw, [](Tracked* ptr) { - destroy_tracked_c_style(ptr); - }); - + + std::shared_ptr cpp_owned(raw, [](Tracked* ptr) { destroy_tracked_c_style(ptr); }); + long use_count = cpp_owned.use_count(); // Q: Why is a lambda deleter necessary here? What would happen if you used the default deleter? // A: // R: - + EXPECT_EQ(use_count, 1); } - + auto events = EventLog::instance().events(); bool c_destroy_called = false; - + for (const auto& event : events) { if (event.find("destroy_tracked_c_style") != std::string::npos) @@ -242,23 +233,22 @@ TEST_F(InteropPatternsTest, BridgingCAndCppOwnership) c_destroy_called = true; } } - + EXPECT_TRUE(c_destroy_called); } TEST_F(InteropPatternsTest, NullptrSafetyInCAPI) { std::shared_ptr null_shared; - + Tracked* raw = null_shared.get(); - + bool is_null = (raw == nullptr); - + long use_count = null_shared.use_count(); - // Q: Why does a default-constructed shared_ptr return `use_count() == 0` instead of 1? What does this reveal about its internal state? - // A: - // R: - + // Q: Why does a default-constructed shared_ptr return `use_count() == 0` instead of 1? What does this reveal about + // its internal state? A: R: + EXPECT_TRUE(is_null); EXPECT_EQ(use_count, 0); } @@ -267,10 +257,8 @@ struct CStyleArray { Tracked* elements; size_t count; - - explicit CStyleArray(size_t n) - : elements(nullptr) - , count(n) + + explicit CStyleArray(size_t n) : elements(nullptr), count(n) { elements = new Tracked[3]{Tracked("E1"), Tracked("E2"), Tracked("E3")}; EventLog::instance().record("CStyleArray allocated"); @@ -294,18 +282,18 @@ TEST_F(InteropPatternsTest, CStyleArrayWrapping) { { std::shared_ptr arr(new CStyleArray(3), CStyleArrayDeleter()); - + long use_count = arr.use_count(); size_t count = arr->count; - + EXPECT_EQ(use_count, 1); EXPECT_EQ(count, 3); } - + auto events = EventLog::instance().events(); bool array_allocated = false; bool array_freed = false; - + for (const auto& event : events) { if (event.find("CStyleArray allocated") != std::string::npos) @@ -317,10 +305,9 @@ TEST_F(InteropPatternsTest, CStyleArrayWrapping) array_freed = true; } } - // Q: The custom deleter calls both `delete[] arr->elements` and `delete arr`. Why are two delete operations necessary? - // A: - // R: - + // Q: The custom deleter calls both `delete[] arr->elements` and `delete arr`. Why are two delete operations + // necessary? A: R: + EXPECT_TRUE(array_allocated); EXPECT_TRUE(array_freed); } @@ -328,18 +315,17 @@ TEST_F(InteropPatternsTest, CStyleArrayWrapping) TEST_F(InteropPatternsTest, GetWithTemporarySharedPtr) { Tracked* raw = nullptr; - + { auto temp = std::make_shared("Temp"); - + raw = temp.get(); } - + bool would_be_dangling = true; - // Q: After the scope exits, what state is `raw` in? What observable signal would confirm the Tracked object was destroyed? - // A: - // R: - + // Q: After the scope exits, what state is `raw` in? What observable signal would confirm the Tracked object was + // destroyed? A: R: + EXPECT_TRUE(would_be_dangling); } @@ -355,17 +341,16 @@ void c_callback(void* user_data) TEST_F(InteropPatternsTest, PassingRawPointerToCallback) { auto shared = std::make_shared("Shared"); - + c_callback(shared.get()); - + long use_count = shared.use_count(); - // Q: What assumption must hold about `c_callback`'s behavior for this pattern to be safe? What would break if the callback stored the pointer? - // A: - // R: - + // Q: What assumption must hold about `c_callback`'s behavior for this pattern to be safe? What would break if the + // callback stored the pointer? A: R: + auto events = EventLog::instance().events(); bool callback_invoked = false; - + for (const auto& event : events) { if (event.find("c_callback invoked") != std::string::npos) @@ -373,7 +358,7 @@ TEST_F(InteropPatternsTest, PassingRawPointerToCallback) callback_invoked = true; } } - + EXPECT_EQ(use_count, 1); EXPECT_TRUE(callback_invoked); } @@ -381,13 +366,13 @@ TEST_F(InteropPatternsTest, PassingRawPointerToCallback) TEST_F(InteropPatternsTest, SharedPtrFromGetDangerousPattern) { auto original = std::make_shared("Original"); - + Tracked* raw = original.get(); // Q: If you created `std::shared_ptr another(raw)`, what would `original.use_count()` return and why? // A: // R: - + long original_count = original.use_count(); - + EXPECT_EQ(original_count, 1); } diff --git a/learning_shared_ptr/tests/test_multi_threaded_patterns.cpp b/learning_shared_ptr/tests/test_multi_threaded_patterns.cpp deleted file mode 100644 index 30ad0cc..0000000 --- a/learning_shared_ptr/tests/test_multi_threaded_patterns.cpp +++ /dev/null @@ -1,1505 +0,0 @@ -#include "instrumentation.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// C++11 compatible latch implementation -class CountDownLatch -{ -public: - explicit CountDownLatch(int count) : count_(count) - { - } - - void arrive_and_wait() - { - std::unique_lock lock(mutex_); - if (--count_ == 0) - { - cv_.notify_all(); - } - else - { - cv_.wait(lock, [this]() { return count_ == 0; }); - } - } - -private: - std::mutex mutex_; - std::condition_variable cv_; - int count_; -}; - -// C++11 compatible barrier implementation -class Barrier -{ -public: - explicit Barrier(int count) : threshold_(count), count_(count), generation_(0) - { - } - - void arrive_and_wait() - { - std::unique_lock lock(mutex_); - int gen = generation_; - - if (--count_ == 0) - { - ++generation_; - count_ = threshold_; - cv_.notify_all(); - } - else - { - cv_.wait(lock, [this, gen]() { return gen != generation_; }); - } - } - -private: - std::mutex mutex_; - std::condition_variable cv_; - int threshold_; - int count_; - int generation_; -}; - -// Thread-safe EventLog wrapper for multi-threaded tests -class ThreadSafeEventLog -{ -public: - static ThreadSafeEventLog& instance() - { - static ThreadSafeEventLog instance; - return instance; - } - - void record(const std::string& event) - { - std::lock_guard lock(mutex_); - events_.push_back(event); - } - - void clear() - { - std::lock_guard lock(mutex_); - events_.clear(); - } - - std::vector events() const - { - std::lock_guard lock(mutex_); - return events_; - } - - size_t count(const std::string& pattern) const - { - std::lock_guard lock(mutex_); - size_t count = 0; - for (const auto& event : events_) - { - if (event.find(pattern) != std::string::npos) - { - ++count; - } - } - return count; - } - -private: - ThreadSafeEventLog() = default; - mutable std::mutex mutex_; - std::vector events_; -}; - -class MultiThreadedPatternsTest : public ::testing::Test -{ -protected: - void SetUp() override - { - EventLog::instance().clear(); - ThreadSafeEventLog::instance().clear(); - } -}; - -// ============================================================================ -// TEST 1: Ownership Transfer Across Threads (io_context) -// ============================================================================ - -class AsyncWorker : public std::enable_shared_from_this -{ -public: - explicit AsyncWorker(const std::string& name) : tracked_(name) - { - } - - void do_work_safe(asio::io_context& io) - { - // Safe: shared_from_this() keeps object alive during async operation - asio::post(io, std::bind(&AsyncWorker::work_impl, shared_from_this())); - } - - void do_work_unsafe(asio::io_context& io) - { - // UNSAFE: Using 'this' - object might be destroyed before callback runs - asio::post(io, std::bind(&AsyncWorker::work_impl, this)); - } - - std::string name() const - { - return tracked_.name(); - } - -private: - void work_impl() - { - ThreadSafeEventLog::instance().record("AsyncWorker::work_impl executed"); - } - - Tracked tracked_; -}; - -TEST_F(MultiThreadedPatternsTest, OwnershipTransferSafeWithSharedFromThis) -{ - asio::io_context io; - std::atomic use_count_in_callback{0}; - - { - std::shared_ptr worker = std::make_shared("Worker1"); - - // Q: What is the use_count before posting work? - // A: - // R: - - // Post work with shared_from_this() - asio::post(io, [worker, &use_count_in_callback]() { - use_count_in_callback = worker.use_count(); - ThreadSafeEventLog::instance().record("Lambda executed with worker alive"); - }); - - // Q: What happens to the worker object when this scope ends? - // A: - // R: - - // Q: Why is the worker object still alive when the callback executes? - // A: - // R: - } - - // Q: At this point (before io.run()), is the worker object destroyed? - // A: - // R: - - io.run(); - - // Q: What is the use_count observed inside the callback, and why? - // A: - // R: - - auto events = ThreadSafeEventLog::instance().events(); - EXPECT_EQ(events.size(), 1); - EXPECT_EQ(use_count_in_callback, 1); -} - -TEST_F(MultiThreadedPatternsTest, OwnershipTransferUnsafeWithRawThis) -{ - asio::io_context io; - bool callback_executed = false; - - { - std::shared_ptr worker = std::make_shared("Worker2"); - - // ANTI-PATTERN: Using raw 'this' pointer - asio::post(io, [worker_ptr = worker.get(), &callback_executed]() { - // Q: What is the danger of using worker_ptr here? - // A: - // R: - - callback_executed = true; - // Accessing worker_ptr->name() here would be undefined behavior if object destroyed - }); - - // Q: What happens when worker goes out of scope? - // A: - // R: - } - - // Q: Is it safe to run io.run() now? Why or why not? - // A: - // R: - - io.run(); - - EXPECT_TRUE(callback_executed); -} - -// ============================================================================ -// TEST 2: Reference Counting Across Threads (Atomic Operations) -// ============================================================================ - -TEST_F(MultiThreadedPatternsTest, AtomicUseCountIncrementAcrossThreads) -{ - std::shared_ptr shared = std::make_shared("Shared"); - std::vector use_counts; - std::mutex counts_mutex; - - // Q: Why is the use_count increment/decrement atomic in shared_ptr? - // A: - // R: - - CountDownLatch start_latch(4); // Synchronize thread start - std::vector threads; - - for (int i = 0; i < 4; ++i) - { - threads.emplace_back([&shared, &use_counts, &counts_mutex, &start_latch]() { - start_latch.arrive_and_wait(); // All threads start simultaneously - - std::shared_ptr local_copy = shared; - - // Q: What happens to the control block's use_count when local_copy is created? - // A: - // R: - - { - std::lock_guard lock(counts_mutex); - use_counts.push_back(local_copy.use_count()); - } - - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - }); - } - - for (auto& t : threads) - { - t.join(); - } - - // Q: What is the maximum use_count observed across all threads? - // A: - // R: - - // Q: Why don't we see race conditions in use_count despite concurrent modifications? - // A: - // R: - - EXPECT_GE(*std::max_element(use_counts.begin(), use_counts.end()), 4); -} - -TEST_F(MultiThreadedPatternsTest, ParallelCopyAndMoveOperations) -{ - std::shared_ptr source = std::make_shared("Source"); - std::atomic copy_count{0}; - std::atomic move_count{0}; - - Barrier sync_point(4); - std::vector threads; - - // Two threads copy, two threads move - for (int i = 0; i < 4; ++i) - { - threads.emplace_back([&, i]() { - sync_point.arrive_and_wait(); - - if (i < 2) - { - // Copy operation - std::shared_ptr copy = source; - ++copy_count; - - // Q: What atomic operation occurs in the control block during copy? - // A: - // R: - } - else - { - // Move operation - std::shared_ptr temp = source; - std::shared_ptr moved = std::move(temp); - ++move_count; - - // Q: What happens to the control block's use_count during move? - // A: - // R: - } - }); - } - - for (auto& t : threads) - { - t.join(); - } - - // Q: Why is move faster than copy in multi-threaded context? - // A: - // R: - - EXPECT_EQ(copy_count, 2); - EXPECT_EQ(move_count, 2); - EXPECT_EQ(source.use_count(), 1); -} - -// ============================================================================ -// TEST 3: Weak Callbacks and Expired Object Handling -// ============================================================================ - -class TimerCallback : public std::enable_shared_from_this -{ -public: - explicit TimerCallback(const std::string& name) : tracked_(name) - { - } - - void schedule_with_weak(asio::io_context& io, std::chrono::milliseconds delay) - { - std::weak_ptr weak_self = shared_from_this(); - - asio::post(io, [weak_self, delay]() { - std::this_thread::sleep_for(delay); - - if (std::shared_ptr self = weak_self.lock()) - { - ThreadSafeEventLog::instance().record("Timer callback executed - object alive"); - } - else - { - ThreadSafeEventLog::instance().record("Timer callback skipped - object expired"); - } - }); - } - - void schedule_with_shared(asio::io_context& io, std::chrono::milliseconds delay) - { - std::shared_ptr shared_self = shared_from_this(); - - asio::post(io, [shared_self, delay]() { - std::this_thread::sleep_for(delay); - ThreadSafeEventLog::instance().record("Timer callback executed - object kept alive"); - }); - } - -private: - Tracked tracked_; -}; - -TEST_F(MultiThreadedPatternsTest, WeakCallbackExpiresBeforeExecution) -{ - asio::io_context io; - std::thread io_thread; - - { - std::shared_ptr timer = std::make_shared("Timer1"); - - // Q: Why use weak_ptr in the callback instead of shared_ptr? - // A: - // R: - - timer->schedule_with_weak(io, std::chrono::milliseconds(50)); - - // Start io_context in separate thread - io_thread = std::thread([&io]() { io.run(); }); - - // Q: What happens when timer goes out of scope immediately? - // A: - // R: - } - - // Q: When the callback executes, what does weak_ptr::lock() return? - // A: - // R: - - io_thread.join(); - - auto events = ThreadSafeEventLog::instance().events(); - EXPECT_EQ(ThreadSafeEventLog::instance().count("skipped - object expired"), 1); -} - -TEST_F(MultiThreadedPatternsTest, SharedCallbackKeepsObjectAlive) -{ - asio::io_context io; - std::thread io_thread; - - { - std::shared_ptr timer = std::make_shared("Timer2"); - timer->schedule_with_shared(io, std::chrono::milliseconds(50)); - - io_thread = std::thread([&io]() { io.run(); }); - - // Q: Even though timer goes out of scope, why isn't the object destroyed? - // A: - // R: - } - - // Q: What is the lifetime of the TimerCallback object in this scenario? - // A: - // R: - - io_thread.join(); - - auto events = ThreadSafeEventLog::instance().events(); - EXPECT_EQ(ThreadSafeEventLog::instance().count("kept alive"), 1); -} - -// ============================================================================ -// TEST 4: Thread-Safe Cache with weak_ptr -// ============================================================================ - -class ThreadSafeCache -{ -public: - std::shared_ptr get_or_create(const std::string& key) - { - std::lock_guard lock(mutex_); - - auto it = cache_.find(key); - if (it != cache_.end()) - { - if (std::shared_ptr cached = it->second.lock()) - { - ThreadSafeEventLog::instance().record("Cache hit: " + key); - return cached; - } - } - - // Q: Why is lock() called inside the mutex-protected section? - // A: - // R: - - std::shared_ptr new_resource = std::make_shared(key); - cache_[key] = new_resource; - ThreadSafeEventLog::instance().record("Cache miss: " + key); - return new_resource; - } - - void cleanup() - { - std::lock_guard lock(mutex_); - - for (auto it = cache_.begin(); it != cache_.end();) - { - if (it->second.expired()) - { - it = cache_.erase(it); - } - else - { - ++it; - } - } - } - - size_t size() const - { - std::lock_guard lock(mutex_); - return cache_.size(); - } - -private: - mutable std::mutex mutex_; - std::map> cache_; -}; - -TEST_F(MultiThreadedPatternsTest, ThreadSafeCacheWithWeakPtr) -{ - ThreadSafeCache cache; - Barrier sync_point(4); - std::vector threads; - - // Q: Why does the cache store weak_ptr instead of shared_ptr? - // A: - // R: - - for (int i = 0; i < 4; ++i) - { - threads.emplace_back([&cache, &sync_point, i]() { - sync_point.arrive_and_wait(); - - std::shared_ptr r1 = cache.get_or_create("resource1"); - std::shared_ptr r2 = cache.get_or_create("resource2"); - - // Q: What prevents race conditions when multiple threads access the cache? - // A: - // R: - - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - }); - } - - for (auto& t : threads) - { - t.join(); - } - - // Q: How many "Cache miss" events should be logged? - // A: - // R: - - EXPECT_EQ(ThreadSafeEventLog::instance().count("Cache hit"), 6); // 4 threads * 2 resources - 2 initial misses - EXPECT_EQ(ThreadSafeEventLog::instance().count("Cache miss"), 2); -} - -// ============================================================================ -// TEST 5: Producer-Consumer with shared_ptr and std::queue -// ============================================================================ - -template class ThreadSafeQueue -{ -public: - void push(std::shared_ptr item) - { - std::lock_guard lock(mutex_); - queue_.push(item); - cv_.notify_one(); - - // Q: What happens to the use_count when item is pushed onto the queue? - // A: - // R: - } - - std::shared_ptr pop() - { - std::unique_lock lock(mutex_); - cv_.wait(lock, [this]() { return !queue_.empty() || done_; }); - - if (queue_.empty()) - { - return nullptr; - } - - std::shared_ptr item = queue_.front(); - queue_.pop(); - - // Q: What happens to the use_count when item is popped from the queue? - // A: - // R: - - return item; - } - - void set_done() - { - std::lock_guard lock(mutex_); - done_ = true; - cv_.notify_all(); - } - -private: - std::mutex mutex_; - std::condition_variable cv_; - std::queue> queue_; - bool done_ = false; -}; - -TEST_F(MultiThreadedPatternsTest, ProducerConsumerWithSharedPtr) -{ - ThreadSafeQueue queue; - std::atomic items_consumed{0}; - const int num_items = 10; - - // Producer thread - std::thread producer([&queue, num_items]() { - for (int i = 0; i < num_items; ++i) - { - std::shared_ptr item = std::make_shared("Item" + std::to_string(i)); - - // Q: When does the Tracked object get destroyed? - // A: - // R: - - queue.push(item); - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - } - queue.set_done(); - }); - - // Consumer thread - std::thread consumer([&queue, &items_consumed]() { - while (true) - { - std::shared_ptr item = queue.pop(); - if (!item) - { - break; - } - - ++items_consumed; - - // Q: What is the use_count of item at this point? - // A: - // R: - - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - }); - - producer.join(); - consumer.join(); - - // Q: Why is shared_ptr ownership transfer ideal for producer-consumer patterns? - // A: - // R: - - EXPECT_EQ(items_consumed, num_items); -} - -// ============================================================================ -// TEST 6: Destruction Order Across Threads -// ============================================================================ - -class DestructionTracker : public std::enable_shared_from_this -{ -public: - explicit DestructionTracker(const std::string& name) : tracked_(name), thread_id_(std::this_thread::get_id()) - { - ThreadSafeEventLog::instance().record("Constructed in thread " + thread_id_string()); - } - - ~DestructionTracker() - { - ThreadSafeEventLog::instance().record("Destroyed in thread " + current_thread_id_string()); - } - - void do_async_work(asio::io_context& io) - { - asio::post(io, [self = shared_from_this()]() { - ThreadSafeEventLog::instance().record("Work executed in thread " + self->current_thread_id_string()); - }); - } - -private: - std::string thread_id_string() const - { - std::ostringstream oss; - oss << thread_id_; - return oss.str(); - } - - std::string current_thread_id_string() const - { - std::ostringstream oss; - oss << std::this_thread::get_id(); - return oss.str(); - } - - Tracked tracked_; - std::thread::id thread_id_; -}; - -TEST_F(MultiThreadedPatternsTest, DestructionInDifferentThread) -{ - asio::io_context io; - std::thread io_thread; - - { - std::shared_ptr tracker = std::make_shared("Tracker1"); - - // Q: In which thread is the DestructionTracker constructed? - // A: - // R: - - tracker->do_async_work(io); - - io_thread = std::thread([&io]() { io.run(); }); - - // Q: When tracker goes out of scope here, is the object destroyed immediately? - // A: - // R: - } - - io_thread.join(); - - // Q: In which thread is the DestructionTracker destroyed, and why? - // A: - // R: - - auto events = ThreadSafeEventLog::instance().events(); - EXPECT_EQ(ThreadSafeEventLog::instance().count("Constructed"), 1); - EXPECT_EQ(ThreadSafeEventLog::instance().count("Destroyed"), 1); -} - -// ============================================================================ -// TEST 7: Aliasing Constructor with Concurrent Access -// ============================================================================ - -struct Container -{ - Tracked member; - std::mutex mutex; - - explicit Container(const std::string& name) : member(name) - { - } -}; - -TEST_F(MultiThreadedPatternsTest, AliasingWithConcurrentAccess) -{ - std::shared_ptr owner = std::make_shared("Container1"); - std::shared_ptr alias(owner, &owner->member); - - // Q: What is the use_count of both owner and alias? - // A: - // R: - - Barrier sync_point(3); - std::vector threads; - - // Thread 1: Access through owner - threads.emplace_back([&owner, &sync_point]() { - sync_point.arrive_and_wait(); - std::lock_guard lock(owner->mutex); - ThreadSafeEventLog::instance().record("Accessed via owner: " + owner->member.name()); - }); - - // Thread 2: Access through alias - threads.emplace_back([&alias, &owner, &sync_point]() { - sync_point.arrive_and_wait(); - std::lock_guard lock(owner->mutex); - ThreadSafeEventLog::instance().record("Accessed via alias: " + alias->name()); - }); - - // Thread 3: Reset owner - threads.emplace_back([&owner, &sync_point]() { - sync_point.arrive_and_wait(); - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - owner.reset(); - - // Q: After owner.reset(), is the Container object destroyed? - // A: - // R: - }); - - for (auto& t : threads) - { - t.join(); - } - - // Q: Why is the Container still alive even after owner.reset()? - // A: - // R: - - // Q: What object gets deleted when alias goes out of scope? - // A: - // R: - - EXPECT_EQ(alias.use_count(), 1); -} - -// ============================================================================ -// TEST 8: Weak Pointer with Aliasing in Multi-threaded Context -// ============================================================================ - -TEST_F(MultiThreadedPatternsTest, WeakAliasingAcrossThreads) -{ - std::weak_ptr weak_alias; - std::atomic successful_locks{0}; - std::atomic failed_locks{0}; - - { - std::shared_ptr owner = std::make_shared("Container2"); - std::shared_ptr alias(owner, &owner->member); - weak_alias = alias; - - // Q: What does weak_alias observe - the Container or the Tracked member? - // A: - // R: - - Barrier sync_point(4); - std::vector threads; - - for (int i = 0; i < 4; ++i) - { - threads.emplace_back([&weak_alias, &successful_locks, &failed_locks, &sync_point, i]() { - sync_point.arrive_and_wait(); - - std::this_thread::sleep_for(std::chrono::milliseconds(i * 10)); - - if (std::shared_ptr locked = weak_alias.lock()) - { - ++successful_locks; - ThreadSafeEventLog::instance().record("Lock succeeded: " + locked->name()); - } - else - { - ++failed_locks; - ThreadSafeEventLog::instance().record("Lock failed - expired"); - } - }); - } - - // Let first thread lock successfully, then destroy owner and alias - std::this_thread::sleep_for(std::chrono::milliseconds(15)); - - // Q: What happens to weak_alias when owner and alias go out of scope? - // A: - // R: - } - - // Wait for all threads - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - - // Q: Why do some threads successfully lock while others fail? - // A: - // R: - - EXPECT_GT(successful_locks, 0); - EXPECT_GT(failed_locks, 0); -} - -// ============================================================================ -// TEST 9: Thread Pool with Ownership Transfer -// ============================================================================ - -class WorkItem : public std::enable_shared_from_this -{ -public: - explicit WorkItem(int id) : tracked_("Work" + std::to_string(id)), id_(id) - { - } - - void execute() - { - ThreadSafeEventLog::instance().record("WorkItem " + std::to_string(id_) + " executed"); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - - int id() const - { - return id_; - } - -private: - Tracked tracked_; - int id_; -}; - -TEST_F(MultiThreadedPatternsTest, ThreadPoolOwnershipTransfer) -{ - asio::thread_pool pool(4); - std::atomic completed{0}; - - // Q: What is the advantage of using asio::thread_pool over asio::io_context? - // A: - // R: - - for (int i = 0; i < 20; ++i) - { - std::shared_ptr work = std::make_shared(i); - - // Q: What happens to work's use_count when posted to the thread pool? - // A: - // R: - - asio::post(pool, [work, &completed]() { - work->execute(); - ++completed; - }); - } - - pool.join(); - - // Q: When are the WorkItem objects destroyed? - // A: - // R: - - EXPECT_EQ(completed, 20); -} - -// ============================================================================ -// TEST 10: Race Condition Detection with shared_ptr -// ============================================================================ - -class RaceConditionDemo -{ -public: - explicit RaceConditionDemo(const std::string& name) : tracked_(name), counter_(0) - { - } - - void increment_unsafe() - { - // ANTI-PATTERN: No synchronization - int temp = counter_; - std::this_thread::sleep_for(std::chrono::microseconds(1)); - counter_ = temp + 1; - } - - void increment_safe() - { - std::lock_guard lock(mutex_); - ++counter_; - } - - int counter() const - { - return counter_; - } - -private: - Tracked tracked_; - int counter_; - std::mutex mutex_; -}; - -TEST_F(MultiThreadedPatternsTest, SharedPtrDoesNotPreventDataRaces) -{ - std::shared_ptr shared = std::make_shared("RaceDemo"); - - // Q: Does shared_ptr protect against data races on the contained object? - // A: - // R: - - std::vector threads; - - for (int i = 0; i < 10; ++i) - { - threads.emplace_back([shared]() { - for (int j = 0; j < 100; ++j) - { - shared->increment_unsafe(); - } - }); - } - - for (auto& t : threads) - { - t.join(); - } - - // Q: Why is the final counter value likely less than 1000? - // A: - // R: - - // Q: What does shared_ptr's atomicity guarantee, and what doesn't it guarantee? - // A: - // R: - - EXPECT_LT(shared->counter(), 1000); // Race condition causes lost updates -} - -// ============================================================================ -// TEST 11: Singleton with Thread-Safe Initialization -// ============================================================================ - -class ThreadSafeSingleton : public std::enable_shared_from_this -{ -public: - static std::shared_ptr instance() - { - std::call_once(init_flag_, []() { - instance_ = std::shared_ptr(new ThreadSafeSingleton()); - ThreadSafeEventLog::instance().record("Singleton initialized"); - }); - - return instance_; - } - - void do_work() - { - ThreadSafeEventLog::instance().record("Singleton work executed"); - } - -private: - ThreadSafeSingleton() : tracked_("Singleton") - { - } - - Tracked tracked_; - static std::shared_ptr instance_; - static std::once_flag init_flag_; -}; - -std::shared_ptr ThreadSafeSingleton::instance_; -std::once_flag ThreadSafeSingleton::init_flag_; - -TEST_F(MultiThreadedPatternsTest, ThreadSafeSingletonAccess) -{ - Barrier sync_point(10); - std::vector threads; - std::vector instance_addresses; - std::mutex addresses_mutex; - - // Q: What does std::call_once guarantee? - // A: - // R: - - for (int i = 0; i < 10; ++i) - { - threads.emplace_back([&sync_point, &instance_addresses, &addresses_mutex]() { - sync_point.arrive_and_wait(); // All threads try to get instance simultaneously - - std::shared_ptr inst = ThreadSafeSingleton::instance(); - inst->do_work(); - - { - std::lock_guard lock(addresses_mutex); - instance_addresses.push_back(inst.get()); - } - }); - } - - for (auto& t : threads) - { - t.join(); - } - - // Q: How many times is "Singleton initialized" logged? - // A: - // R: - - // Q: Why do all threads get the same instance address? - // A: - // R: - - EXPECT_EQ(ThreadSafeEventLog::instance().count("Singleton initialized"), 1); - - // Verify all addresses are the same - void* first_address = instance_addresses[0]; - for (void* addr : instance_addresses) - { - EXPECT_EQ(addr, first_address); - } -} - -// ============================================================================ -// TEST 12: Circular Reference with Threads (Anti-pattern) -// ============================================================================ - -class ThreadedNode : public std::enable_shared_from_this -{ -public: - explicit ThreadedNode(const std::string& name) : tracked_(name) - { - } - - void set_next_unsafe(std::shared_ptr next) - { - // ANTI-PATTERN: Creates circular reference - next_ = next; - } - - void set_next_safe(std::shared_ptr next) - { - // CORRECT: Use weak_ptr to break cycle - weak_next_ = next; - } - - std::shared_ptr next() const - { - return weak_next_.lock(); - } - -private: - Tracked tracked_; - std::shared_ptr next_; // Causes leak - std::weak_ptr weak_next_; // Prevents leak -}; - -TEST_F(MultiThreadedPatternsTest, CircularReferenceAcrossThreads) -{ - std::atomic dtors_called{0}; - - { - std::shared_ptr node1 = std::make_shared("Node1"); - std::shared_ptr node2 = std::make_shared("Node2"); - - std::thread t1([node1, node2]() { node1->set_next_unsafe(node2); }); - - std::thread t2([node1, node2]() { node2->set_next_unsafe(node1); }); - - t1.join(); - t2.join(); - - // Q: What is the use_count of node1 and node2 after the circular reference is created? - // A: - // R: - - // Q: Why won't the nodes be destroyed when this scope ends? - // A: - // R: - } - - auto events = EventLog::instance().events(); - size_t dtor_count = 0; - for (const auto& event : events) - { - if (event.find("::dtor") != std::string::npos) - { - ++dtor_count; - } - } - - // Q: How many destructors are called, and why? - // A: - // R: - - EXPECT_EQ(dtor_count, 0); // Memory leak! -} - -TEST_F(MultiThreadedPatternsTest, BreakCircularReferenceWithWeakPtr) -{ - { - std::shared_ptr node1 = std::make_shared("Node3"); - std::shared_ptr node2 = std::make_shared("Node4"); - - std::thread t1([node1, node2]() { node1->set_next_safe(node2); }); - - std::thread t2([node1, node2]() { node2->set_next_safe(node1); }); - - t1.join(); - t2.join(); - - // Q: What is the use_count of node1 and node2 with weak_ptr? - // A: - // R: - - // Q: Why are the nodes properly destroyed when this scope ends? - // A: - // R: - } - - auto events = EventLog::instance().events(); - size_t dtor_count = 0; - for (const auto& event : events) - { - if (event.find("::dtor") != std::string::npos) - { - ++dtor_count; - } - } - - // Q: How does weak_ptr prevent the memory leak? - // A: - // R: - - EXPECT_EQ(dtor_count, 2); // Both nodes destroyed -} - -// ============================================================================ -// TEST 13: Timer-Based Callbacks with Cancellation -// ============================================================================ - -class CancellableTimer : public std::enable_shared_from_this -{ -public: - explicit CancellableTimer(asio::io_context& io, const std::string& name) - : io_(io), timer_(io), tracked_(name), cancelled_(false) - { - } - - void start(std::chrono::milliseconds delay) - { - timer_.expires_after(delay); - - std::weak_ptr weak_self = shared_from_this(); - - timer_.async_wait([weak_self](const asio::error_code& ec) { - if (ec) - { - ThreadSafeEventLog::instance().record("Timer cancelled or error"); - return; - } - - if (std::shared_ptr self = weak_self.lock()) - { - if (!self->cancelled_) - { - ThreadSafeEventLog::instance().record("Timer fired: " + self->tracked_.name()); - } - else - { - ThreadSafeEventLog::instance().record("Timer skipped - cancelled"); - } - } - else - { - ThreadSafeEventLog::instance().record("Timer skipped - object expired"); - } - }); - - // Q: Why capture weak_ptr instead of shared_ptr in the timer callback? - // A: - // R: - } - - void cancel() - { - cancelled_ = true; - timer_.cancel(); - } - -private: - asio::io_context& io_; - asio::steady_timer timer_; - Tracked tracked_; - bool cancelled_; -}; - -TEST_F(MultiThreadedPatternsTest, TimerCallbackWithWeakPtr) -{ - asio::io_context io; - - { - std::shared_ptr timer = std::make_shared(io, "Timer1"); - timer->start(std::chrono::milliseconds(50)); - - // Q: What happens if timer goes out of scope before the timer fires? - // A: - // R: - } - - std::thread io_thread([&io]() { io.run(); }); - io_thread.join(); - - // Q: What does the timer callback observe when it executes? - // A: - // R: - - EXPECT_EQ(ThreadSafeEventLog::instance().count("object expired"), 1); -} - -TEST_F(MultiThreadedPatternsTest, TimerCancellation) -{ - asio::io_context io; - std::shared_ptr timer = std::make_shared(io, "Timer2"); - - timer->start(std::chrono::milliseconds(100)); - - std::thread io_thread([&io]() { io.run(); }); - - // Cancel after 20ms - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - timer->cancel(); - - io_thread.join(); - - // Q: What prevents the timer callback from executing after cancellation? - // A: - // R: - - EXPECT_GT(ThreadSafeEventLog::instance().count("cancelled"), 0); -} - -// ============================================================================ -// TEST 14: Connection Management Pattern (Real-world async I/O) -// ============================================================================ - -class Connection : public std::enable_shared_from_this -{ -public: - explicit Connection(asio::io_context& io, int id) - : io_(io), tracked_("Connection" + std::to_string(id)), id_(id), active_(true) - { - ThreadSafeEventLog::instance().record("Connection " + std::to_string(id_) + " created"); - } - - ~Connection() - { - ThreadSafeEventLog::instance().record("Connection " + std::to_string(id_) + " destroyed"); - } - - void start_async_read() - { - // Q: Why is it critical to use shared_from_this() here? - // A: - // R: - - asio::post(io_, std::bind(&Connection::handle_read, shared_from_this())); - } - - void close() - { - active_ = false; - } - - bool is_active() const - { - return active_; - } - -private: - void handle_read() - { - if (!active_) - { - ThreadSafeEventLog::instance().record("Connection " + std::to_string(id_) + " read cancelled"); - return; - } - - ThreadSafeEventLog::instance().record("Connection " + std::to_string(id_) + " read completed"); - - // Simulate another async read - if (active_) - { - asio::post(io_, std::bind(&Connection::handle_read, shared_from_this())); - } - } - - asio::io_context& io_; - Tracked tracked_; - int id_; - bool active_; -}; - -class ConnectionManager -{ -public: - explicit ConnectionManager(asio::io_context& io) : io_(io) - { - } - - void add_connection(int id) - { - std::lock_guard lock(mutex_); - - std::shared_ptr conn = std::make_shared(io_, id); - connections_[id] = conn; - conn->start_async_read(); - - // Q: Why store weak_ptr in the manager instead of shared_ptr? - // A: - // R: - } - - void close_connection(int id) - { - std::lock_guard lock(mutex_); - - auto it = connections_.find(id); - if (it != connections_.end()) - { - if (std::shared_ptr conn = it->second.lock()) - { - conn->close(); - } - connections_.erase(it); - } - } - - size_t active_count() const - { - std::lock_guard lock(mutex_); - - size_t count = 0; - for (const auto& pair : connections_) - { - if (std::shared_ptr conn = pair.second.lock()) - { - if (conn->is_active()) - { - ++count; - } - } - } - return count; - } - -private: - asio::io_context& io_; - mutable std::mutex mutex_; - std::map> connections_; -}; - -TEST_F(MultiThreadedPatternsTest, ConnectionManagementPattern) -{ - asio::io_context io; - ConnectionManager manager(io); - - // Add connections - manager.add_connection(1); - manager.add_connection(2); - manager.add_connection(3); - - std::thread io_thread([&io]() { io.run_for(std::chrono::milliseconds(100)); }); - - // Q: What keeps the Connection objects alive while async operations are pending? - // A: - // R: - - std::this_thread::sleep_for(std::chrono::milliseconds(30)); - manager.close_connection(2); - - // Q: After close_connection(2), when is Connection 2 destroyed? - // A: - // R: - - io_thread.join(); - - // Q: Why is weak_ptr ideal for connection managers? - // A: - // R: - - EXPECT_EQ(ThreadSafeEventLog::instance().count("created"), 3); - EXPECT_EQ(ThreadSafeEventLog::instance().count("destroyed"), 3); -} - -// ============================================================================ -// TEST 15: std::bind vs Lambda Capture Comparison -// ============================================================================ - -class BindVsLambda : public std::enable_shared_from_this -{ -public: - explicit BindVsLambda(const std::string& name) : tracked_(name) - { - } - - void method_with_args(int value, const std::string& text) - { - ThreadSafeEventLog::instance().record("method_with_args: " + std::to_string(value) + ", " + text); - } - - void schedule_with_bind(asio::io_context& io) - { - // Using std::bind - asio::post(io, std::bind(&BindVsLambda::method_with_args, shared_from_this(), 42, "bind")); - - // Q: What does std::bind capture, and how does it keep the object alive? - // A: - // R: - } - - void schedule_with_lambda(asio::io_context& io) - { - // Using lambda - asio::post(io, [self = shared_from_this()]() { self->method_with_args(42, "lambda"); }); - - // Q: What is the advantage of lambda capture over std::bind? - // A: - // R: - } - -private: - Tracked tracked_; -}; - -TEST_F(MultiThreadedPatternsTest, BindVsLambdaComparison) -{ - asio::io_context io; - - { - std::shared_ptr obj = std::make_shared("BindLambda"); - - obj->schedule_with_bind(io); - obj->schedule_with_lambda(io); - - // Q: Do both std::bind and lambda capture keep the object alive? - // A: - // R: - } - - io.run(); - - // Q: When should you prefer std::bind over lambda, and vice versa? - // A: - // R: - - EXPECT_EQ(ThreadSafeEventLog::instance().count("method_with_args"), 2); -} diff --git a/learning_shared_ptr/tests/test_polymorphism_patterns.cpp b/learning_shared_ptr/tests/test_polymorphism_patterns.cpp index 25f4569..2a8d5d2 100644 --- a/learning_shared_ptr/tests/test_polymorphism_patterns.cpp +++ b/learning_shared_ptr/tests/test_polymorphism_patterns.cpp @@ -1,4 +1,5 @@ #include "instrumentation.h" + #include #include @@ -14,18 +15,17 @@ class PolymorphismPatternsTest : public ::testing::Test class Base { public: - explicit Base(const std::string& name) - : tracked_(name) + explicit Base(const std::string& name) : tracked_(name) { } - + virtual ~Base() = default; - + virtual std::string type() const { return "Base"; } - + private: Tracked tracked_; }; @@ -33,17 +33,15 @@ class Base class Derived : public Base { public: - explicit Derived(const std::string& name) - : Base(name) - , derived_tracked_(name + "_derived") + explicit Derived(const std::string& name) : Base(name), derived_tracked_(name + "_derived") { } - + std::string type() const override { return "Derived"; } - + private: Tracked derived_tracked_; }; @@ -51,19 +49,18 @@ class Derived : public Base TEST_F(PolymorphismPatternsTest, DynamicPointerCastBasic) { std::shared_ptr base_ptr = std::make_shared("D1"); - + long base_count = base_ptr.use_count(); - + std::shared_ptr derived_ptr = std::dynamic_pointer_cast(base_ptr); - + long after_cast_base_count = base_ptr.use_count(); long after_cast_derived_count = derived_ptr.use_count(); - // Q: Why do both `base_ptr` and `derived_ptr` report `use_count() == 2`? What does this reveal about the control block? - // A: - // R: - + // Q: Why do both `base_ptr` and `derived_ptr` report `use_count() == 2`? What does this reveal about the control + // block? A: R: + bool cast_succeeded = (derived_ptr != nullptr); - + EXPECT_EQ(base_count, 1); EXPECT_EQ(after_cast_base_count, 2); EXPECT_EQ(after_cast_derived_count, 2); @@ -73,19 +70,18 @@ TEST_F(PolymorphismPatternsTest, DynamicPointerCastBasic) TEST_F(PolymorphismPatternsTest, DynamicPointerCastFailure) { std::shared_ptr base_ptr = std::make_shared("B1"); - + long base_count = base_ptr.use_count(); - + std::shared_ptr derived_ptr = std::dynamic_pointer_cast(base_ptr); - + long after_cast_base_count = base_ptr.use_count(); - // Q: When `dynamic_pointer_cast` fails, why does `after_cast_base_count` remain at 1? What does this reveal about failed cast behavior? - // A: - // R: - + // Q: When `dynamic_pointer_cast` fails, why does `after_cast_base_count` remain at 1? What does this reveal about + // failed cast behavior? A: R: + bool cast_failed = (derived_ptr == nullptr); long derived_count = derived_ptr ? derived_ptr.use_count() : 0; - + EXPECT_EQ(base_count, 1); EXPECT_EQ(after_cast_base_count, 1); EXPECT_TRUE(cast_failed); @@ -95,19 +91,19 @@ TEST_F(PolymorphismPatternsTest, DynamicPointerCastFailure) TEST_F(PolymorphismPatternsTest, StaticPointerCast) { std::shared_ptr derived_ptr = std::make_shared("D1"); - + std::shared_ptr base_ptr = std::static_pointer_cast(derived_ptr); - + long derived_count = derived_ptr.use_count(); long base_count = base_ptr.use_count(); // Q: Why do `derived_count` and `base_count` both equal 2? What does `static_pointer_cast` do to the control block? // A: // R: - + std::shared_ptr back_to_derived = std::static_pointer_cast(base_ptr); - + long final_count = derived_ptr.use_count(); - + EXPECT_EQ(derived_count, 2); EXPECT_EQ(base_count, 2); EXPECT_EQ(final_count, 3); @@ -116,17 +112,16 @@ TEST_F(PolymorphismPatternsTest, StaticPointerCast) TEST_F(PolymorphismPatternsTest, ConstPointerCast) { std::shared_ptr const_ptr = std::make_shared("Const"); - + long const_count = const_ptr.use_count(); - + std::shared_ptr mutable_ptr = std::const_pointer_cast(const_ptr); - + long after_cast_const_count = const_ptr.use_count(); long after_cast_mutable_count = mutable_ptr.use_count(); - // Q: After `const_pointer_cast`, both pointers report `use_count() == 2`. What does this reveal about const-casting's impact on the underlying object? - // A: - // R: - + // Q: After `const_pointer_cast`, both pointers report `use_count() == 2`. What does this reveal about + // const-casting's impact on the underlying object? A: R: + EXPECT_EQ(const_count, 1); EXPECT_EQ(after_cast_const_count, 2); EXPECT_EQ(after_cast_mutable_count, 2); @@ -135,15 +130,14 @@ TEST_F(PolymorphismPatternsTest, ConstPointerCast) class WidgetImpl { public: - explicit WidgetImpl(const std::string& name) - : tracked_(name) + explicit WidgetImpl(const std::string& name) : tracked_(name) { } - + void do_work() { } - + private: Tracked tracked_; }; @@ -152,21 +146,20 @@ class WidgetImpl class Widget { public: - explicit Widget(const std::string& name) - : pimpl_(std::make_shared(name)) + explicit Widget(const std::string& name) : pimpl_(std::make_shared(name)) { } - + void do_work() { pimpl_->do_work(); } - + long impl_use_count() const { return pimpl_.use_count(); } - + private: std::shared_ptr pimpl_; }; @@ -174,20 +167,19 @@ class Widget TEST_F(PolymorphismPatternsTest, PimplIdiomBasic) { Widget w1("Widget1"); - + long use_count_single = w1.impl_use_count(); - + Widget w2 = w1; - + long use_count_copied = w1.impl_use_count(); - // Q: After copying `w1` to `w2`, what does `use_count_copied == 2` reveal about the implementation object's lifetime? - // A: - // R: - + // Q: After copying `w1` to `w2`, what does `use_count_copied == 2` reveal about the implementation object's + // lifetime? A: R: + // Q: What observable consequence would occur if `w2.do_work()` modified the implementation state? // A: // R: - + EXPECT_EQ(use_count_single, 1); EXPECT_EQ(use_count_copied, 2); } @@ -197,9 +189,9 @@ TEST_F(PolymorphismPatternsTest, PimplIdiomSharedImpl) Widget w1("Widget1"); Widget w2 = w1; Widget w3 = w2; - + long use_count = w1.impl_use_count(); - + EXPECT_EQ(use_count, 3); } @@ -213,15 +205,14 @@ class AbstractInterface class ConcreteA : public AbstractInterface { public: - explicit ConcreteA(const std::string& name) - : tracked_(name) + explicit ConcreteA(const std::string& name) : tracked_(name) { } - + void execute() override { } - + private: Tracked tracked_; }; @@ -229,15 +220,14 @@ class ConcreteA : public AbstractInterface class ConcreteB : public AbstractInterface { public: - explicit ConcreteB(const std::string& name) - : tracked_(name) + explicit ConcreteB(const std::string& name) : tracked_(name) { } - + void execute() override { } - + private: Tracked tracked_; }; @@ -245,18 +235,18 @@ class ConcreteB : public AbstractInterface TEST_F(PolymorphismPatternsTest, PolymorphicContainer) { std::vector> container; - + container.push_back(std::make_shared("A1")); container.push_back(std::make_shared("B1")); container.push_back(std::make_shared("A2")); - + size_t container_size = container.size(); - + long first_use_count = container[0].use_count(); // Q: Why does storing different concrete types in the same container require shared_ptr instead of unique_ptr? // A: // R: - + EXPECT_EQ(container_size, 3); EXPECT_EQ(first_use_count, 1); } @@ -264,18 +254,17 @@ TEST_F(PolymorphismPatternsTest, PolymorphicContainer) TEST_F(PolymorphismPatternsTest, DowncastingInHierarchy) { std::shared_ptr interface = std::make_shared("A1"); - + auto as_a = std::dynamic_pointer_cast(interface); auto as_b = std::dynamic_pointer_cast(interface); - + bool a_succeeded = (as_a != nullptr); bool b_failed = (as_b == nullptr); - + long interface_count = interface.use_count(); - // Q: Why does `interface_count` equal 2 after two casts, one of which failed? What contributes to the reference count? - // A: - // R: - + // Q: Why does `interface_count` equal 2 after two casts, one of which failed? What contributes to the reference + // count? A: R: + EXPECT_TRUE(a_succeeded); EXPECT_TRUE(b_failed); EXPECT_EQ(interface_count, 2); @@ -284,17 +273,15 @@ TEST_F(PolymorphismPatternsTest, DowncastingInHierarchy) class MultiDerived : public Base { public: - explicit MultiDerived(const std::string& name) - : Base(name) - , multi_tracked_(name + "_multi") + explicit MultiDerived(const std::string& name) : Base(name), multi_tracked_(name + "_multi") { } - + std::string type() const override { return "MultiDerived"; } - + private: Tracked multi_tracked_; }; @@ -302,18 +289,17 @@ class MultiDerived : public Base TEST_F(PolymorphismPatternsTest, MultiLevelHierarchyCasting) { std::shared_ptr base = std::make_shared("MD1"); - + auto as_derived = std::dynamic_pointer_cast(base); auto as_multi = std::dynamic_pointer_cast(base); - + bool derived_failed = (as_derived == nullptr); bool multi_succeeded = (as_multi != nullptr); - // Q: Why does casting to `Derived` fail while casting to `MultiDerived` succeeds? What does this reveal about the actual object type? - // A: - // R: - + // Q: Why does casting to `Derived` fail while casting to `MultiDerived` succeeds? What does this reveal about the + // actual object type? A: R: + long base_count = base.use_count(); - + EXPECT_TRUE(derived_failed); EXPECT_TRUE(multi_succeeded); EXPECT_EQ(base_count, 2); @@ -324,22 +310,20 @@ TEST_F(PolymorphismPatternsTest, AliasingWithPolymorphicTypes) struct Container { Base base_member; - explicit Container(const std::string& name) - : base_member(name) + explicit Container(const std::string& name) : base_member(name) { } }; - + auto container = std::make_shared("Container1"); - + std::shared_ptr alias(container, &container->base_member); - + long container_count = container.use_count(); long alias_count = alias.use_count(); - // Q: The aliasing constructor points `alias` to `base_member` but shares ownership with `container`. What would happen if `container` were destroyed while `alias` still exists? - // A: - // R: - + // Q: The aliasing constructor points `alias` to `base_member` but shares ownership with `container`. What would + // happen if `container` were destroyed while `alias` still exists? A: R: + EXPECT_EQ(container_count, 2); EXPECT_EQ(alias_count, 2); } diff --git a/learning_shared_ptr/tests/test_self_reference_patterns.cpp b/learning_shared_ptr/tests/test_self_reference_patterns.cpp index c8c58ec..cc6b515 100644 --- a/learning_shared_ptr/tests/test_self_reference_patterns.cpp +++ b/learning_shared_ptr/tests/test_self_reference_patterns.cpp @@ -205,9 +205,7 @@ TEST_F(SelfReferencePatternsTest, TimerWithSelfReference) auto timer = std::make_shared("Timer1"); - timer->schedule([&captured_use_count](std::shared_ptr self) { - captured_use_count = self.use_count(); - }); + timer->schedule([&captured_use_count](std::shared_ptr self) { captured_use_count = self.use_count(); }); timer->execute(); @@ -258,9 +256,8 @@ TEST_F(SelfReferencePatternsTest, ChainedAsyncOperations) conn->async_read([&read_use_count, &write_use_count](std::shared_ptr self) { read_use_count = self.use_count(); - self->async_write([&write_use_count](std::shared_ptr inner_self) { - write_use_count = inner_self.use_count(); - }); + self->async_write( + [&write_use_count](std::shared_ptr inner_self) { write_use_count = inner_self.use_count(); }); }); EXPECT_EQ(read_use_count, 2); @@ -316,12 +313,8 @@ TEST_F(SelfReferencePatternsTest, EventEmitterWithWeakFromThis) auto emitter = std::make_shared("Emitter1"); - emitter->on_event([&handler_count](std::shared_ptr self) { - handler_count++; - }); - emitter->on_event([&handler_count](std::shared_ptr self) { - handler_count++; - }); + emitter->on_event([&handler_count](std::shared_ptr self) { handler_count++; }); + emitter->on_event([&handler_count](std::shared_ptr self) { handler_count++; }); emitter->emit(); diff --git a/learning_shared_ptr/tests/test_smart_pointer_contrast.cpp b/learning_shared_ptr/tests/test_smart_pointer_contrast.cpp index e0d5b19..6373066 100644 --- a/learning_shared_ptr/tests/test_smart_pointer_contrast.cpp +++ b/learning_shared_ptr/tests/test_smart_pointer_contrast.cpp @@ -94,10 +94,8 @@ TEST_F(SmartPointerContrastTest, RawPointerDangerVsSmartPointer) TEST_F(SmartPointerContrastTest, CustomDeleterComparison) { { - std::unique_ptr> u( - new Tracked("UniqueWithDeleter"), - LoggingDeleter("UniqueDeleter") - ); + std::unique_ptr> u(new Tracked("UniqueWithDeleter"), + LoggingDeleter("UniqueDeleter")); } auto unique_events = EventLog::instance().events(); @@ -105,10 +103,7 @@ TEST_F(SmartPointerContrastTest, CustomDeleterComparison) EventLog::instance().clear(); { - std::shared_ptr s( - new Tracked("SharedWithDeleter"), - LoggingDeleter("SharedDeleter") - ); + std::shared_ptr s(new Tracked("SharedWithDeleter"), LoggingDeleter("SharedDeleter")); } auto shared_events = EventLog::instance().events(); diff --git a/learning_stl/tests/test_algorithms.cpp b/learning_stl/tests/test_algorithms.cpp index bee6257..538128f 100644 --- a/learning_stl/tests/test_algorithms.cpp +++ b/learning_stl/tests/test_algorithms.cpp @@ -2,12 +2,12 @@ // Estimated Time: 3 hours // Difficulty: Moderate - #include "instrumentation.h" -#include -#include + #include +#include #include +#include #if defined(__has_include) #if __has_include() #include @@ -43,18 +43,18 @@ class AlgorithmsTest : public ::testing::Test TEST_F(AlgorithmsTest, Find_LinearComplexity) { // Easy: std::find has linear complexity - + std::vector vec = {1, 2, 3, 4, 5}; - + auto it = std::find(vec.begin(), vec.end(), 3); - + EXPECT_NE(it, vec.end()); EXPECT_EQ(*it, 3); - + // Q: What is the time complexity of std::find? // A: // R: - + // Q: Why can't std::find be faster than O(n)? // A: // R: @@ -63,23 +63,23 @@ TEST_F(AlgorithmsTest, Find_LinearComplexity) TEST_F(AlgorithmsTest, BinarySearch_LogarithmicComplexity) { // Moderate: std::binary_search requires sorted range - + std::vector vec = {1, 2, 3, 4, 5}; - + bool found = std::binary_search(vec.begin(), vec.end(), 3); EXPECT_TRUE(found); - + // Q: What is the time complexity of std::binary_search? // A: // R: - + // Q: What precondition must be met for binary_search? // A: // R: - + std::vector unsorted = {5, 2, 4, 1, 3}; bool found_unsorted = std::binary_search(unsorted.begin(), unsorted.end(), 3); - + // Q: What happens if you call binary_search on unsorted data? // A: // R: @@ -88,27 +88,26 @@ TEST_F(AlgorithmsTest, BinarySearch_LogarithmicComplexity) TEST_F(AlgorithmsTest, Sort_ComplexityGuarantee) { // Moderate: std::sort has O(n log n) complexity guarantee - + std::vector vec; vec.push_back(Tracked("C")); vec.push_back(Tracked("A")); vec.push_back(Tracked("B")); - + EventLog::instance().clear(); - - std::sort(vec.begin(), vec.end(), [](const Tracked& a, const Tracked& b) - { + + std::sort(vec.begin(), vec.end(), [](const Tracked& a, const Tracked& b) { EventLog::instance().record("Comparator called"); return a.name() < b.name(); }); - + // Q: What is the worst-case complexity of std::sort? // A: // R: - + // Verify comparisons happened EXPECT_GT(EventLog::instance().count_events("Comparator called"), 0); - + // Q: How does std::sort differ from std::stable_sort? // A: // R: @@ -121,15 +120,15 @@ TEST_F(AlgorithmsTest, Sort_ComplexityGuarantee) TEST_F(AlgorithmsTest, ParallelAlgorithms_ExecutionPolicies) { // Hard: C++17 parallel algorithms with execution policies - + std::vector vec(1000); std::iota(vec.begin(), vec.end(), 0); - + #if HAS_STD_EXECUTION_POLICIES // Sequential execution auto result1 = std::find(std::execution::seq, vec.begin(), vec.end(), 500); EXPECT_NE(result1, vec.end()); - + // Parallel execution (may use multiple threads) auto result2 = std::find(std::execution::par, vec.begin(), vec.end(), 500); EXPECT_NE(result2, vec.end()); @@ -140,11 +139,11 @@ TEST_F(AlgorithmsTest, ParallelAlgorithms_ExecutionPolicies) EXPECT_NE(result1, vec.end()); EXPECT_NE(result2, vec.end()); #endif - + // Q: What is the difference between std::execution::seq and std::execution::par? // A: // R: - + // Q: When would parallel execution be slower than sequential? // A: // R: @@ -153,19 +152,19 @@ TEST_F(AlgorithmsTest, ParallelAlgorithms_ExecutionPolicies) TEST_F(AlgorithmsTest, ParallelSort_ThreadSafety) { // Hard: Parallel algorithms require thread-safe operations - + std::vector vec = {5, 2, 8, 1, 9, 3, 7, 4, 6}; - + #if HAS_STD_EXECUTION_POLICIES - // TODO: Sort with parallel execution policy + // SOLUTION: Sort with parallel execution policy std::sort(std::execution::par, vec.begin(), vec.end()); #else // Fallback for standard libraries without execution policy support std::sort(vec.begin(), vec.end()); #endif - + EXPECT_TRUE(std::is_sorted(vec.begin(), vec.end())); - + // Q: What requirements does the comparator have for parallel sort? // A: // R: @@ -178,15 +177,14 @@ TEST_F(AlgorithmsTest, ParallelSort_ThreadSafety) TEST_F(AlgorithmsTest, Transform_Mapping) { // Easy: std::transform applies function to range - + std::vector input = {1, 2, 3, 4, 5}; std::vector output; - - std::transform(input.begin(), input.end(), std::back_inserter(output), - [](int x) { return x * 2; }); - + + std::transform(input.begin(), input.end(), std::back_inserter(output), [](int x) { return x * 2; }); + EXPECT_EQ(output, std::vector({2, 4, 6, 8, 10})); - + // Q: Can transform modify the input range in-place? // A: // R: @@ -195,17 +193,16 @@ TEST_F(AlgorithmsTest, Transform_Mapping) TEST_F(AlgorithmsTest, Accumulate_Reduction) { // Easy: std::accumulate reduces range to single value - + std::vector vec = {1, 2, 3, 4, 5}; - + int sum = std::accumulate(vec.begin(), vec.end(), 0); EXPECT_EQ(sum, 15); - + // Custom operation - int product = std::accumulate(vec.begin(), vec.end(), 1, - [](int acc, int x) { return acc * x; }); + int product = std::accumulate(vec.begin(), vec.end(), 1, [](int acc, int x) { return acc * x; }); EXPECT_EQ(product, 120); - + // Q: What is the difference between accumulate and reduce? // A: // R: @@ -218,26 +215,26 @@ TEST_F(AlgorithmsTest, Accumulate_Reduction) TEST_F(AlgorithmsTest, Predicates_UnaryAndBinary) { // Moderate: Understanding predicate requirements - + std::vector vec = {1, 2, 3, 4, 5, 6}; - + // Unary predicate auto is_even = [](int x) { return x % 2 == 0; }; - + auto it = std::find_if(vec.begin(), vec.end(), is_even); EXPECT_EQ(*it, 2); - + int count = std::count_if(vec.begin(), vec.end(), is_even); EXPECT_EQ(count, 3); - + // Q: What is a unary predicate? // A: // R: - + // Binary predicate for sorting std::sort(vec.begin(), vec.end(), std::greater()); EXPECT_EQ(vec[0], 6); - + // Q: What requirements must a predicate satisfy? // A: // R: @@ -250,28 +247,28 @@ TEST_F(AlgorithmsTest, Predicates_UnaryAndBinary) TEST_F(AlgorithmsTest, Algorithm_IteratorReturns) { // Moderate: Understanding what algorithms return - + std::vector vec = {1, 2, 3, 4, 5}; - + // find returns iterator to found element or end() auto it1 = std::find(vec.begin(), vec.end(), 3); EXPECT_EQ(*it1, 3); - + auto it2 = std::find(vec.begin(), vec.end(), 99); EXPECT_EQ(it2, vec.end()); - + // Q: Why do algorithms return iterators instead of indices? // A: // R: - + // remove returns iterator to new logical end auto new_end = std::remove(vec.begin(), vec.end(), 3); - + // Q: Does std::remove actually erase elements from the vector? // A: // R: - - EXPECT_EQ(vec.size(), 5); // Size unchanged - vec.erase(new_end, vec.end()); // Actually remove + + EXPECT_EQ(vec.size(), 5); // Size unchanged + vec.erase(new_end, vec.end()); // Actually remove EXPECT_EQ(vec.size(), 4); } diff --git a/learning_stl/tests/test_comparators_hash_functions.cpp b/learning_stl/tests/test_comparators_hash_functions.cpp index d41d5ba..20539d5 100644 --- a/learning_stl/tests/test_comparators_hash_functions.cpp +++ b/learning_stl/tests/test_comparators_hash_functions.cpp @@ -2,15 +2,15 @@ // Estimated Time: 3 hours // Difficulty: Moderate - #include "instrumentation.h" + +#include +#include #include -#include -#include #include +#include #include -#include -#include +#include #include class ComparatorsHashFunctionsTest : public ::testing::Test @@ -30,10 +30,8 @@ struct Person { std::string name; int age; - - Person(std::string n, int a) - : name(std::move(n)) - , age(a) + + Person(std::string n, int a) : name(std::move(n)), age(a) { EventLog::instance().record("Person(" + name + ")::ctor"); } @@ -51,24 +49,24 @@ struct CompareByAge TEST_F(ComparatorsHashFunctionsTest, CustomComparator_Set) { // Moderate: Using custom comparator with std::set - + std::set people; - + people.emplace("Alice", 30); people.emplace("Bob", 25); people.emplace("Charlie", 35); - + // Q: In what order are the people stored in the set? // A: // R: - + auto it = people.begin(); EXPECT_EQ(it->name, "Bob"); ++it; EXPECT_EQ(it->name, "Alice"); ++it; EXPECT_EQ(it->name, "Charlie"); - + // Q: What happens if two people have the same age? // A: // R: @@ -77,27 +75,25 @@ TEST_F(ComparatorsHashFunctionsTest, CustomComparator_Set) TEST_F(ComparatorsHashFunctionsTest, Comparator_StrictWeakOrdering) { // Hard: Understanding strict weak ordering requirements - - auto bad_comparator = [](int a, int b) - { + + auto bad_comparator = [](int a, int b) { EventLog::instance().record("bad_comparator called"); - return a <= b; // WRONG: not strict weak ordering + return a <= b; // WRONG: not strict weak ordering }; - - auto good_comparator = [](int a, int b) - { + + auto good_comparator = [](int a, int b) { EventLog::instance().record("good_comparator called"); - return a < b; // CORRECT: strict weak ordering + return a < b; // CORRECT: strict weak ordering }; - + // Q: What are the requirements for strict weak ordering? // A: // R: - + // Q: Why does <= violate strict weak ordering? // A: // R: - + std::vector vec = {3, 1, 2}; std::sort(vec.begin(), vec.end(), good_comparator); EXPECT_TRUE(std::is_sorted(vec.begin(), vec.end())); @@ -112,11 +108,11 @@ struct PersonHash std::size_t operator()(const Person& p) const { EventLog::instance().record("PersonHash called for " + p.name); - + // Combine name and age hashes std::size_t h1 = std::hash{}(p.name); std::size_t h2 = std::hash{}(p.age); - + return h1 ^ (h2 << 1); } }; @@ -133,22 +129,22 @@ struct PersonEqual TEST_F(ComparatorsHashFunctionsTest, CustomHash_UnorderedSet) { // Hard: Custom hash function for unordered containers - + std::unordered_set people; - + people.emplace("Alice", 30); people.emplace("Bob", 25); - + // Q: Why do unordered containers need both a hash function and equality operator? // A: // R: - + EXPECT_EQ(people.size(), 2); - + // Try to insert duplicate people.emplace("Alice", 30); - EXPECT_EQ(people.size(), 2); // No duplicate - + EXPECT_EQ(people.size(), 2); // No duplicate + // Q: What happens when two different objects have the same hash? // A: // R: @@ -157,24 +153,24 @@ TEST_F(ComparatorsHashFunctionsTest, CustomHash_UnorderedSet) TEST_F(ComparatorsHashFunctionsTest, HashCollisions_BucketStructure) { // Hard: Understanding hash collision handling - + std::unordered_map map; - + map[1] = "one"; map[2] = "two"; map[3] = "three"; - + // Q: How does unordered_map handle hash collisions? // A: // R: - + size_t bucket_count = map.bucket_count(); EXPECT_GT(bucket_count, 0); - + // Check load factor float load = map.load_factor(); EXPECT_GT(load, 0.0f); - + // Q: What happens when load factor exceeds max_load_factor? // A: // R: @@ -187,19 +183,19 @@ TEST_F(ComparatorsHashFunctionsTest, HashCollisions_BucketStructure) TEST_F(ComparatorsHashFunctionsTest, TransparentComparator_HeterogeneousLookup) { // Hard: std::less<> enables heterogeneous lookup - + std::set> string_set; string_set.insert("hello"); string_set.insert("world"); - + // Can find using const char* without creating temporary string auto it = string_set.find("hello"); EXPECT_NE(it, string_set.end()); - + // Q: What advantage does std::less<> provide over std::less? // A: // R: - + // Q: What is "transparent comparison"? // A: // R: @@ -212,7 +208,7 @@ TEST_F(ComparatorsHashFunctionsTest, TransparentComparator_HeterogeneousLookup) TEST_F(ComparatorsHashFunctionsTest, Comparator_ConsistencyRequirement) { // Moderate: Comparator must be consistent with equality - + struct InconsistentCompare { bool operator()(const Person& a, const Person& b) const @@ -221,7 +217,7 @@ TEST_F(ComparatorsHashFunctionsTest, Comparator_ConsistencyRequirement) return a.age < b.age; } }; - + struct InconsistentEqual { bool operator()(const Person& a, const Person& b) const @@ -230,10 +226,10 @@ TEST_F(ComparatorsHashFunctionsTest, Comparator_ConsistencyRequirement) return a.name == b.name && a.age == b.age; } }; - + // This creates inconsistency: two people with same age but different names // would be "equal" by comparator but "not equal" by equality - + // Q: What problems arise when comparator and equality are inconsistent? // A: // R: @@ -246,28 +242,28 @@ TEST_F(ComparatorsHashFunctionsTest, Comparator_ConsistencyRequirement) TEST_F(ComparatorsHashFunctionsTest, HashFunction_Distribution) { // Hard: Good hash functions distribute values uniformly - + struct BadHash { std::size_t operator()(int x) const { - return 42; // Always returns same hash! + return 42; // Always returns same hash! } }; - + std::unordered_set bad_set; bad_set.insert(1); bad_set.insert(2); bad_set.insert(3); - + // Q: What is the time complexity of find() with BadHash? // A: // R: - + // Q: Why does a bad hash function degrade performance? // A: // R: - + // With bad hash, all elements collide (but bucket_count may be larger) EXPECT_GE(bad_set.bucket_count(), 1); } diff --git a/learning_stl/tests/test_container_internals.cpp b/learning_stl/tests/test_container_internals.cpp index 9d35333..45cfffa 100644 --- a/learning_stl/tests/test_container_internals.cpp +++ b/learning_stl/tests/test_container_internals.cpp @@ -2,14 +2,14 @@ // Estimated Time: 3 hours // Difficulty: Easy - #include "instrumentation.h" -#include -#include + #include +#include +#include #include #include -#include +#include class ContainerInternalsTest : public ::testing::Test { @@ -27,60 +27,60 @@ class ContainerInternalsTest : public ::testing::Test TEST_F(ContainerInternalsTest, Vector_GrowthStrategy) { // Easy: Understanding vector capacity and reallocation - + std::vector vec; - + EventLog::instance().record("Initial capacity: " + std::to_string(vec.capacity())); - + // Q: What is the initial capacity of an empty vector? // A: // R: - + vec.push_back(Tracked("Item1")); size_t cap1 = vec.capacity(); EventLog::instance().record("After 1 push: capacity=" + std::to_string(cap1)); - + vec.push_back(Tracked("Item2")); size_t cap2 = vec.capacity(); EventLog::instance().record("After 2 push: capacity=" + std::to_string(cap2)); - + vec.push_back(Tracked("Item3")); size_t cap3 = vec.capacity(); EventLog::instance().record("After 3 push: capacity=" + std::to_string(cap3)); - + // Q: How does vector capacity grow? (e.g., linear, exponential) // A: // R: - + // Q: Why does vector grow exponentially rather than by a fixed amount? // A: // R: - + EXPECT_GT(cap3, 0); } TEST_F(ContainerInternalsTest, Vector_ReallocationCost) { // Moderate: Observing reallocation through move operations - + std::vector vec; vec.reserve(2); - + vec.push_back(Tracked("A")); vec.push_back(Tracked("B")); - + EventLog::instance().clear(); - + // Force reallocation vec.push_back(Tracked("C")); - + // Q: What happens to existing elements during reallocation? // A: // R: - + // Verify moves occurred (A and B moved to new storage) EXPECT_GE(EventLog::instance().count_events("::move_ctor"), 2); - + // Q: Why does vector move existing elements instead of copying? // A: // R: @@ -89,27 +89,27 @@ TEST_F(ContainerInternalsTest, Vector_ReallocationCost) TEST_F(ContainerInternalsTest, Vector_ReserveVsResize) { // Easy: Understanding reserve vs resize - + std::vector vec1; vec1.reserve(5); - + // Q: How many Tracked objects were constructed after reserve(5)? // A: // R: - + EXPECT_EQ(vec1.size(), 0); EXPECT_GE(vec1.capacity(), 5); EXPECT_EQ(EventLog::instance().count_events("::ctor"), 0); - + EventLog::instance().clear(); - + std::vector vec2; vec2.resize(5, Tracked("Default")); - + // Q: How many Tracked objects were constructed after resize(5)? // A: // R: - + EXPECT_EQ(vec2.size(), 5); // One ctor for the default value, then copies/moves for the 5 elements EXPECT_GE(EventLog::instance().count_events("::ctor"), 1); @@ -122,29 +122,29 @@ TEST_F(ContainerInternalsTest, Vector_ReserveVsResize) TEST_F(ContainerInternalsTest, Deque_NoReallocation) { // Moderate: deque doesn't invalidate references on push - + std::deque deq; - + deq.push_back(Tracked("A")); deq.push_back(Tracked("B")); - + EventLog::instance().clear(); - + // Add many elements - no reallocation of existing elements for (int i = 0; i < 100; ++i) { deq.push_back(Tracked("Item")); } - + // Q: How many move operations occurred on existing elements A and B? // A: // R: - + // Verify A and B were not moved (deque uses chunked storage) size_t move_count = EventLog::instance().count_events("Tracked(A)::move"); move_count += EventLog::instance().count_events("Tracked(B)::move"); EXPECT_EQ(move_count, 0); - + // Q: How does deque achieve this without reallocation? // A: // R: @@ -153,20 +153,20 @@ TEST_F(ContainerInternalsTest, Deque_NoReallocation) TEST_F(ContainerInternalsTest, Deque_FrontAndBackInsertion) { // Easy: deque supports efficient insertion at both ends - + std::deque deq; - + deq.push_back(1); deq.push_front(0); deq.push_back(2); deq.push_front(-1); - + EXPECT_EQ(deq.size(), 4); EXPECT_EQ(deq[0], -1); EXPECT_EQ(deq[1], 0); EXPECT_EQ(deq[2], 1); EXPECT_EQ(deq[3], 2); - + // Q: What is the time complexity of push_front for vector vs deque? // A: // R: @@ -179,24 +179,24 @@ TEST_F(ContainerInternalsTest, Deque_FrontAndBackInsertion) TEST_F(ContainerInternalsTest, Map_OrderedIteration) { // Easy: std::map maintains sorted order - + std::map ordered_map; ordered_map[3] = "three"; ordered_map[1] = "one"; ordered_map[2] = "two"; - + std::vector keys; for (const auto& pair : ordered_map) { keys.push_back(pair.first); } - + EXPECT_EQ(keys, std::vector({1, 2, 3})); - + // Q: What data structure does std::map use internally? // A: // R: - + // Q: What is the time complexity of map::find? // A: // R: @@ -205,23 +205,23 @@ TEST_F(ContainerInternalsTest, Map_OrderedIteration) TEST_F(ContainerInternalsTest, UnorderedMap_HashBased) { // Moderate: std::unordered_map uses hash table - + std::unordered_map hash_map; hash_map[3] = "three"; hash_map[1] = "one"; hash_map[2] = "two"; - + // Iteration order is unspecified (based on hash) EXPECT_EQ(hash_map.size(), 3); - + // Q: What is the average time complexity of unordered_map::find? // A: // R: - + // Q: When would you choose map over unordered_map? // A: // R: - + // Check load factor float load = hash_map.load_factor(); EXPECT_GT(load, 0.0f); @@ -235,22 +235,22 @@ TEST_F(ContainerInternalsTest, UnorderedMap_HashBased) TEST_F(ContainerInternalsTest, Vector_ContiguousMemory) { // Moderate: vector guarantees contiguous storage - + std::vector vec = {1, 2, 3, 4, 5}; - + int* ptr = vec.data(); - + // Q: What does vec.data() return? // A: // R: - + // Verify contiguous memory for (size_t i = 0; i < vec.size(); ++i) { EXPECT_EQ(ptr[i], vec[i]); EXPECT_EQ(&ptr[i], &vec[i]); } - + // Q: Which other STL containers guarantee contiguous storage? // A: // R: @@ -259,28 +259,28 @@ TEST_F(ContainerInternalsTest, Vector_ContiguousMemory) TEST_F(ContainerInternalsTest, List_NodeBasedStorage) { // Easy: std::list uses node-based storage - + std::list lst; - + lst.push_back(Tracked("A")); lst.push_back(Tracked("B")); lst.push_back(Tracked("C")); - + EventLog::instance().clear(); - + // Insert in middle - no moves of existing elements auto it = lst.begin(); ++it; lst.insert(it, Tracked("Middle")); - + // Q: How many existing elements were moved during insert? // A: // R: - + EXPECT_EQ(EventLog::instance().count_events("Tracked(A)::move"), 0); EXPECT_EQ(EventLog::instance().count_events("Tracked(B)::move"), 0); EXPECT_EQ(EventLog::instance().count_events("Tracked(C)::move"), 0); - + // Q: What is the trade-off between list and vector for insertion? // A: // R: @@ -293,18 +293,18 @@ TEST_F(ContainerInternalsTest, List_NodeBasedStorage) TEST_F(ContainerInternalsTest, String_SmallStringOptimization) { // Hard: std::string may use SSO for small strings - + std::string small = "short"; std::string large = "this is a much longer string that exceeds SSO threshold"; - + // Q: What is Small String Optimization? // A: // R: - + // Q: Why does SSO improve performance for small strings? // A: // R: - + // Note: SSO threshold is implementation-defined (typically 15-23 bytes) // We can't directly test SSO, but can observe its effects } @@ -316,23 +316,23 @@ TEST_F(ContainerInternalsTest, String_SmallStringOptimization) TEST_F(ContainerInternalsTest, Container_ComplexityGuarantees) { // Moderate: Understanding time complexity of operations - + std::vector vec = {1, 2, 3}; std::list lst = {1, 2, 3}; std::deque deq = {1, 2, 3}; - + // Q: What is the complexity of vec.push_back() amortized? // A: // R: - + // Q: What is the complexity of lst.insert() at any position? // A: // R: - + // Q: What is the complexity of deq.push_front()? // A: // R: - + // Q: Which container provides O(1) random access? // A: // R: diff --git a/learning_stl/tests/test_iterator_invalidation.cpp b/learning_stl/tests/test_iterator_invalidation.cpp index b040610..8f88dac 100644 --- a/learning_stl/tests/test_iterator_invalidation.cpp +++ b/learning_stl/tests/test_iterator_invalidation.cpp @@ -2,15 +2,15 @@ // Estimated Time: 3 hours // Difficulty: Hard - #include "instrumentation.h" + +#include +#include #include -#include #include -#include #include #include -#include +#include class IteratorInvalidationTest : public ::testing::Test { @@ -28,30 +28,30 @@ class IteratorInvalidationTest : public ::testing::Test TEST_F(IteratorInvalidationTest, Vector_InvalidationOnReallocation) { // Hard: Vector iterators invalidate on reallocation - + std::vector vec; vec.reserve(2); - + vec.push_back(1); vec.push_back(2); - + auto it = vec.begin(); int* ptr = &vec[0]; - + EXPECT_EQ(*it, 1); EXPECT_EQ(*ptr, 1); - + // Force reallocation vec.push_back(3); - + // Q: Are 'it' and 'ptr' still valid after reallocation? // A: // R: - + // Q: How can you detect if reallocation occurred? // A: // R: - + // Safe: get new iterator auto new_it = vec.begin(); EXPECT_EQ(*new_it, 1); @@ -60,25 +60,25 @@ TEST_F(IteratorInvalidationTest, Vector_InvalidationOnReallocation) TEST_F(IteratorInvalidationTest, Vector_InvalidationOnErase) { // Moderate: Erase invalidates iterators at and after erase point - + std::vector vec = {1, 2, 3, 4, 5}; - + auto it1 = vec.begin(); - auto it2 = vec.begin() + 2; // Points to 3 - auto it5 = vec.begin() + 4; // Points to 5 - + auto it2 = vec.begin() + 2; // Points to 3 + auto it5 = vec.begin() + 4; // Points to 5 + EXPECT_EQ(*it2, 3); - + // Erase element at position 2 vec.erase(it2); - + // Q: Which iterators are invalidated after erasing position 2? // A: // R: - + // it1 still valid (before erase point) EXPECT_EQ(*it1, 1); - + // it2 and it5 are invalidated // Using them would be undefined behavior } @@ -86,20 +86,20 @@ TEST_F(IteratorInvalidationTest, Vector_InvalidationOnErase) TEST_F(IteratorInvalidationTest, Vector_InvalidationOnInsert) { // Moderate: Insert may invalidate all iterators - + std::vector vec = {1, 2, 3}; - vec.reserve(10); // Ensure no reallocation - + vec.reserve(10); // Ensure no reallocation + auto it = vec.begin() + 1; EXPECT_EQ(*it, 2); - + // Insert without reallocation vec.insert(vec.begin(), 0); - + // Q: Is 'it' still valid after insert (no reallocation)? // A: // R: - + // Even without reallocation, insert invalidates all iterators // because elements shift } @@ -111,40 +111,40 @@ TEST_F(IteratorInvalidationTest, Vector_InvalidationOnInsert) TEST_F(IteratorInvalidationTest, List_StableIterators) { // Easy: List iterators remain valid on insert/erase - + std::list lst; lst.push_back(Tracked("A")); lst.push_back(Tracked("B")); lst.push_back(Tracked("C")); - + auto it_a = lst.begin(); auto it_b = std::next(lst.begin()); auto it_c = std::next(lst.begin(), 2); - + EXPECT_EQ(it_a->name(), "A"); EXPECT_EQ(it_b->name(), "B"); EXPECT_EQ(it_c->name(), "C"); - + EventLog::instance().clear(); - + // Insert in middle lst.insert(it_b, Tracked("Middle")); - + // Q: Are it_a, it_b, it_c still valid after insert? // A: // R: - + EXPECT_EQ(it_a->name(), "A"); EXPECT_EQ(it_b->name(), "B"); EXPECT_EQ(it_c->name(), "C"); - + // Erase middle element lst.erase(it_b); - + // Q: Which iterators are invalidated after erasing it_b? // A: // R: - + EXPECT_EQ(it_a->name(), "A"); EXPECT_EQ(it_c->name(), "C"); // it_b is now invalid @@ -157,29 +157,29 @@ TEST_F(IteratorInvalidationTest, List_StableIterators) TEST_F(IteratorInvalidationTest, Deque_PartialInvalidation) { // Hard: Deque has complex invalidation rules - + std::deque deq = {1, 2, 3, 4, 5}; - + auto it_middle = deq.begin() + 2; EXPECT_EQ(*it_middle, 3); - + // Insert at front deq.push_front(0); - + // Q: Is it_middle still valid after push_front? // A: // R: - + // Insert at back deq.push_back(6); - + // Q: Is it_middle still valid after push_back? // A: // R: - + // Insert in middle deq.insert(deq.begin() + 3, 99); - + // Q: Is it_middle still valid after insert in middle? // A: // R: @@ -192,35 +192,35 @@ TEST_F(IteratorInvalidationTest, Deque_PartialInvalidation) TEST_F(IteratorInvalidationTest, Map_StableIterators) { // Moderate: Map iterators remain valid except for erased elements - + std::map map; map[1] = "one"; map[2] = "two"; map[3] = "three"; - + auto it1 = map.find(1); auto it2 = map.find(2); auto it3 = map.find(3); - + EXPECT_EQ(it2->second, "two"); - + // Erase element 2 map.erase(it2); - + // Q: Are it1 and it3 still valid after erasing it2? // A: // R: - + EXPECT_EQ(it1->second, "one"); EXPECT_EQ(it3->second, "three"); - + // Insert new element map[4] = "four"; - + // Q: Are it1 and it3 still valid after insert? // A: // R: - + EXPECT_EQ(it1->second, "one"); EXPECT_EQ(it3->second, "three"); } @@ -232,28 +232,28 @@ TEST_F(IteratorInvalidationTest, Map_StableIterators) TEST_F(IteratorInvalidationTest, UnorderedMap_RehashInvalidation) { // Hard: Unordered map invalidates on rehash - + std::unordered_map map; - map.reserve(3); // Prevent rehash initially - + map.reserve(3); // Prevent rehash initially + map[1] = "one"; map[2] = "two"; - + auto it1 = map.find(1); EXPECT_EQ(it1->second, "one"); - + float initial_load = map.load_factor(); - + // Add many elements to trigger rehash for (int i = 10; i < 100; ++i) { map[i] = "value"; } - + // Q: Is it1 still valid after potential rehash? // A: // R: - + // Q: What operation triggers rehash in unordered containers? // A: // R: @@ -266,19 +266,19 @@ TEST_F(IteratorInvalidationTest, UnorderedMap_RehashInvalidation) TEST_F(IteratorInvalidationTest, SafeErasure_EraseRemoveIdiom) { // Moderate: Erase-remove idiom for safe removal - + std::vector vec = {1, 2, 3, 2, 4, 2, 5}; - + // Remove all 2s auto new_end = std::remove(vec.begin(), vec.end(), 2); vec.erase(new_end, vec.end()); - + EXPECT_EQ(vec, std::vector({1, 3, 4, 5})); - + // Q: Why is this pattern called "erase-remove idiom"? // A: // R: - + // Q: What does std::remove actually do to the elements? // A: // R: @@ -287,24 +287,24 @@ TEST_F(IteratorInvalidationTest, SafeErasure_EraseRemoveIdiom) TEST_F(IteratorInvalidationTest, SafeErasure_EraseInLoop) { // Hard: Safe erasure while iterating - + std::vector vec = {1, 2, 3, 4, 5}; - + // TODO: Erase all even numbers safely - for (auto it = vec.begin(); it != vec.end(); ) + for (auto it = vec.begin(); it != vec.end();) { if (*it % 2 == 0) { - it = vec.erase(it); // erase returns next valid iterator + it = vec.erase(it); // erase returns next valid iterator } else { ++it; } } - + EXPECT_EQ(vec, std::vector({1, 3, 5})); - + // Q: Why must we use it = vec.erase(it) instead of vec.erase(it); ++it? // A: // R: @@ -313,10 +313,10 @@ TEST_F(IteratorInvalidationTest, SafeErasure_EraseInLoop) TEST_F(IteratorInvalidationTest, SafeErasure_ListErase) { // Moderate: List erase returns next iterator - + std::list lst = {1, 2, 3, 4, 5}; - - for (auto it = lst.begin(); it != lst.end(); ) + + for (auto it = lst.begin(); it != lst.end();) { if (*it % 2 == 0) { @@ -327,10 +327,10 @@ TEST_F(IteratorInvalidationTest, SafeErasure_ListErase) ++it; } } - + std::vector result(lst.begin(), lst.end()); EXPECT_EQ(result, std::vector({1, 3, 5})); - + // Q: Does list::erase have the same invalidation rules as vector::erase? // A: // R: @@ -343,19 +343,19 @@ TEST_F(IteratorInvalidationTest, SafeErasure_ListErase) TEST_F(IteratorInvalidationTest, Invalidation_ContainerComparison) { // Hard: Understanding invalidation rules across containers - + // Q: Which container has the most stable iterators? // A: // R: - + // Q: Which container invalidates iterators most aggressively? // A: // R: - + // Q: When do map iterators get invalidated? // A: // R: - + // Q: When do unordered_map iterators get invalidated? // A: // R: diff --git a/learning_stl/tests/test_iterators.cpp b/learning_stl/tests/test_iterators.cpp index 703078f..da88cd0 100644 --- a/learning_stl/tests/test_iterators.cpp +++ b/learning_stl/tests/test_iterators.cpp @@ -2,13 +2,13 @@ // Estimated Time: 3 hours // Difficulty: Moderate - #include "instrumentation.h" + +#include #include -#include -#include #include -#include +#include +#include class IteratorsTest : public ::testing::Test { @@ -26,30 +26,30 @@ class IteratorsTest : public ::testing::Test TEST_F(IteratorsTest, IteratorCategories_Hierarchy) { // Easy: Understanding the five iterator categories - + std::vector vec = {1, 2, 3}; std::list lst = {1, 2, 3}; - + auto vec_it = vec.begin(); auto lst_it = lst.begin(); - + // Q: What iterator category does std::vector::iterator provide? // A: // R: - + // Q: What iterator category does std::list::iterator provide? // A: // R: - + // Random access iterator supports arithmetic vec_it += 2; EXPECT_EQ(*vec_it, 3); - + // Bidirectional iterator requires increment/decrement ++lst_it; ++lst_it; EXPECT_EQ(*lst_it, 3); - + // Q: Can you do lst_it += 2? Why or why not? // A: // R: @@ -58,19 +58,19 @@ TEST_F(IteratorsTest, IteratorCategories_Hierarchy) TEST_F(IteratorsTest, IteratorTraits_CompileTimeQuery) { // Moderate: Using iterator_traits to query iterator properties - + using VecIter = std::vector::iterator; using ListIter = std::list::iterator; - + using VecCategory = typename std::iterator_traits::iterator_category; using ListCategory = typename std::iterator_traits::iterator_category; - + static_assert(std::is_same_v, "vector iterator should be random access"); - + static_assert(std::is_same_v, "list iterator should be bidirectional"); - + // Q: Why do algorithms need to query iterator categories? // A: // R: @@ -80,8 +80,7 @@ TEST_F(IteratorsTest, IteratorTraits_CompileTimeQuery) // Custom Iterator Implementation // ============================================================================ -template -class RangeIterator +template class RangeIterator { public: using iterator_category = std::forward_iterator_tag; @@ -89,33 +88,35 @@ class RangeIterator using difference_type = std::ptrdiff_t; using pointer = T*; using reference = T&; - - explicit RangeIterator(T value) - : current_(value) + + explicit RangeIterator(T value) : current_(value) { EventLog::instance().record("RangeIterator::ctor"); } - - T operator*() const { return current_; } - + + T operator*() const + { + return current_; + } + RangeIterator& operator++() { ++current_; return *this; } - + RangeIterator operator++(int) { RangeIterator temp = *this; ++current_; return temp; } - + bool operator==(const RangeIterator& other) const { return current_ == other.current_; } - + bool operator!=(const RangeIterator& other) const { return current_ != other.current_; @@ -132,18 +133,18 @@ class RangeIterator TEST_F(IteratorsTest, CustomIterator_ForwardIterator) { // Hard: Implementing a custom forward iterator - + RangeIterator begin(0); RangeIterator end(5); - + std::vector result; for (auto it = begin; it != end; ++it) { result.push_back(*it); } - + EXPECT_EQ(result, std::vector({0, 1, 2, 3, 4})); - + // Q: Can RangeIterator be used with std::sort? Why or why not? // A: // R: @@ -156,21 +157,21 @@ TEST_F(IteratorsTest, CustomIterator_ForwardIterator) TEST_F(IteratorsTest, ReverseIterator_Adaptor) { // Easy: std::reverse_iterator adapts bidirectional iterators - + std::vector vec = {1, 2, 3, 4, 5}; - + std::vector reversed; for (auto it = vec.rbegin(); it != vec.rend(); ++it) { reversed.push_back(*it); } - + EXPECT_EQ(reversed, std::vector({5, 4, 3, 2, 1})); - + // Q: What does rbegin() return in terms of regular iterators? // A: // R: - + // Q: Can you use reverse_iterator with std::list? // A: // R: @@ -179,18 +180,18 @@ TEST_F(IteratorsTest, ReverseIterator_Adaptor) TEST_F(IteratorsTest, BackInserter_OutputIterator) { // Moderate: std::back_inserter creates output iterator - + std::vector source = {1, 2, 3}; std::vector dest; - + std::copy(source.begin(), source.end(), std::back_inserter(dest)); - + EXPECT_EQ(dest, source); - + // Q: What does back_inserter do differently than assigning to dest.begin()? // A: // R: - + // Q: What happens if you use dest.begin() instead of back_inserter with empty dest? // A: // R: @@ -203,20 +204,20 @@ TEST_F(IteratorsTest, BackInserter_OutputIterator) TEST_F(IteratorsTest, IteratorDistance_Complexity) { // Moderate: std::distance complexity varies by iterator category - + std::vector vec = {1, 2, 3, 4, 5}; std::list lst = {1, 2, 3, 4, 5}; - + auto vec_dist = std::distance(vec.begin(), vec.end()); auto lst_dist = std::distance(lst.begin(), lst.end()); - + EXPECT_EQ(vec_dist, 5); EXPECT_EQ(lst_dist, 5); - + // Q: What is the time complexity of std::distance for random access iterators? // A: // R: - + // Q: What is the time complexity of std::distance for bidirectional iterators? // A: // R: @@ -225,24 +226,24 @@ TEST_F(IteratorsTest, IteratorDistance_Complexity) TEST_F(IteratorsTest, IteratorAdvance_Optimization) { // Moderate: std::advance optimizes based on iterator category - + std::vector vec = {1, 2, 3, 4, 5}; - + auto it = vec.begin(); std::advance(it, 3); - + EXPECT_EQ(*it, 4); - + // Q: How does std::advance optimize for random access iterators? // A: // R: - + std::list lst = {1, 2, 3, 4, 5}; auto lst_it = lst.begin(); std::advance(lst_it, 3); - + EXPECT_EQ(*lst_it, 4); - + // Q: How does std::advance work for bidirectional iterators? // A: // R: @@ -255,21 +256,21 @@ TEST_F(IteratorsTest, IteratorAdvance_Optimization) TEST_F(IteratorsTest, Iterator_ValidityAfterModification) { // Hard: Understanding when iterators remain valid - + std::vector vec = {1, 2, 3}; auto it = vec.begin(); - + EXPECT_EQ(*it, 1); - + // Modify without reallocation vec[0] = 10; - EXPECT_EQ(*it, 10); // Iterator still valid - + EXPECT_EQ(*it, 10); // Iterator still valid + // Q: When does vector invalidate iterators? // A: // R: - - vec.reserve(100); // May reallocate + + vec.reserve(100); // May reallocate // Q: Is 'it' still valid after reserve? // A: // R: @@ -282,22 +283,22 @@ TEST_F(IteratorsTest, Iterator_ValidityAfterModification) TEST_F(IteratorsTest, ConstIterator_Immutability) { // Easy: const_iterator prevents modification - + std::vector vec = {1, 2, 3}; - + std::vector::iterator it = vec.begin(); - *it = 10; // OK + *it = 10; // OK EXPECT_EQ(vec[0], 10); - + std::vector::const_iterator cit = vec.cbegin(); // *cit = 20; // Compile error - + EXPECT_EQ(*cit, 10); - + // Q: Can you convert iterator to const_iterator? // A: // R: - + // Q: Can you convert const_iterator to iterator? // A: // R: diff --git a/learning_templates/tests/test_function_class_templates.cpp b/learning_templates/tests/test_function_class_templates.cpp index 85072e0..0f69e6e 100644 --- a/learning_templates/tests/test_function_class_templates.cpp +++ b/learning_templates/tests/test_function_class_templates.cpp @@ -2,12 +2,12 @@ // Estimated Time: 3 hours // Difficulty: Easy to Moderate +#include "instrumentation.h" #include -#include "instrumentation.h" -#include #include #include +#include class FunctionClassTemplatesTest : public ::testing::Test { @@ -22,8 +22,7 @@ class FunctionClassTemplatesTest : public ::testing::Test // TEST 1: Basic Function Templates - Easy // ============================================================================ -template -T max_value(T a, T b) +template T max_value(T a, T b) { EventLog::instance().record("max_value called"); return (a > b) ? a : b; @@ -54,8 +53,7 @@ TEST_F(FunctionClassTemplatesTest, BasicFunctionTemplates) // TEST 2: Template Type Deduction - Moderate // ============================================================================ -template -void process_value(T value) +template void process_value(T value) { if constexpr (std::is_pointer_v) { @@ -101,17 +99,18 @@ TEST_F(FunctionClassTemplatesTest, TemplateTypeDeduction) // TEST 3: Basic Class Templates - Easy // ============================================================================ -template -class Box +template class Box { public: - explicit Box(T value) - : value_(value) + explicit Box(T value) : value_(value) { EventLog::instance().record("Box::ctor"); } - T get() const { return value_; } + T get() const + { + return value_; + } void set(T value) { @@ -150,18 +149,22 @@ TEST_F(FunctionClassTemplatesTest, BasicClassTemplates) // TEST 4: Template with Multiple Type Parameters - Moderate // ============================================================================ -template -class Pair +template class Pair { public: - Pair(K key, V value) - : key_(key), value_(value) + Pair(K key, V value) : key_(key), value_(value) { EventLog::instance().record("Pair::ctor"); } - K key() const { return key_; } - V value() const { return value_; } + K key() const + { + return key_; + } + V value() const + { + return value_; + } private: K key_; @@ -194,8 +197,7 @@ TEST_F(FunctionClassTemplatesTest, MultipleTypeParameters) // TEST 5: Non-Type Template Parameters - Moderate // ============================================================================ -template -class FixedArray +template class FixedArray { public: FixedArray() @@ -203,10 +205,19 @@ class FixedArray EventLog::instance().record("FixedArray::ctor size=" + std::to_string(N)); } - T& operator[](size_t index) { return data_[index]; } - const T& operator[](size_t index) const { return data_[index]; } + T& operator[](size_t index) + { + return data_[index]; + } + const T& operator[](size_t index) const + { + return data_[index]; + } - constexpr size_t size() const { return N; } + constexpr size_t size() const + { + return N; + } private: T data_[N]; @@ -263,17 +274,18 @@ TEST_F(FunctionClassTemplatesTest, DISABLED_TemplateDefaultParameters) // TEST 7: Template Argument Deduction (CTAD C++17) - Moderate // ============================================================================ -template -class Wrapper +template class Wrapper { public: - explicit Wrapper(T value) - : value_(value) + explicit Wrapper(T value) : value_(value) { EventLog::instance().record("Wrapper::ctor"); } - T get() const { return value_; } + T get() const + { + return value_; + } private: T value_; @@ -311,14 +323,12 @@ class Container EventLog::instance().record("Container::ctor"); } - template - void add(T value) + template void add(T value) { EventLog::instance().record("Container::add<" + std::string(typeid(T).name()) + ">"); } - template - T get() const + template T get() const { EventLog::instance().record("Container::get<" + std::string(typeid(T).name()) + ">"); return T{}; diff --git a/learning_templates/tests/test_practical_metaprogramming.cpp b/learning_templates/tests/test_practical_metaprogramming.cpp index 5731c21..8875091 100644 --- a/learning_templates/tests/test_practical_metaprogramming.cpp +++ b/learning_templates/tests/test_practical_metaprogramming.cpp @@ -2,14 +2,14 @@ // Estimated Time: 4 hours // Difficulty: Hard +#include "instrumentation.h" +#include #include -#include "instrumentation.h" -#include -#include #include +#include +#include #include -#include class PracticalMetaprogrammingTest : public ::testing::Test { @@ -24,20 +24,17 @@ class PracticalMetaprogrammingTest : public ::testing::Test // TEST 1: Compile-Time Fibonacci - Moderate // ============================================================================ -template -struct Fibonacci +template struct Fibonacci { static constexpr int value = Fibonacci::value + Fibonacci::value; }; -template<> -struct Fibonacci<0> +template <> struct Fibonacci<0> { static constexpr int value = 0; }; -template<> -struct Fibonacci<1> +template <> struct Fibonacci<1> { static constexpr int value = 1; }; @@ -68,26 +65,21 @@ TEST_F(PracticalMetaprogrammingTest, CompileTimeFibonacci) // TEST 2: Type List Manipulation - Hard // ============================================================================ -template -struct TypeList +template struct TypeList { static constexpr size_t size = sizeof...(Types); }; -template -struct TypeListSize; +template struct TypeListSize; -template -struct TypeListSize> +template struct TypeListSize> { static constexpr size_t value = sizeof...(Types); }; -template -struct PushFront; +template struct PushFront; -template -struct PushFront> +template struct PushFront> { using type = TypeList; }; @@ -115,7 +107,7 @@ TEST_F(PracticalMetaprogrammingTest, TypeListManipulation) // TEST 3: Tuple Utilities with Metaprogramming - Hard // ============================================================================ -template +template auto tuple_to_string_impl(const Tuple& t, std::index_sequence) -> std::string { std::string result; @@ -123,8 +115,7 @@ auto tuple_to_string_impl(const Tuple& t, std::index_sequence) -> std::st return result; } -template -auto tuple_to_string(const std::tuple& t) -> std::string +template auto tuple_to_string(const std::tuple& t) -> std::string { return tuple_to_string_impl(t, std::index_sequence_for{}); } @@ -154,36 +145,35 @@ TEST_F(PracticalMetaprogrammingTest, TupleUtilitiesWithMetaprogramming) // TEST 4: Tag Dispatching - Moderate // ============================================================================ -template -void advance_impl(T& iter, int n, std::random_access_iterator_tag) +template void advance_impl(T& iter, int n, std::random_access_iterator_tag) { EventLog::instance().record("advance: random_access"); iter += n; } -template -void advance_impl(T& iter, int n, std::bidirectional_iterator_tag) +template void advance_impl(T& iter, int n, std::bidirectional_iterator_tag) { EventLog::instance().record("advance: bidirectional"); if (n >= 0) { - for (int i = 0; i < n; ++i) ++iter; + for (int i = 0; i < n; ++i) + ++iter; } else { - for (int i = 0; i > n; --i) --iter; + for (int i = 0; i > n; --i) + --iter; } } -template -void advance_impl(T& iter, int n, std::forward_iterator_tag) +template void advance_impl(T& iter, int n, std::forward_iterator_tag) { EventLog::instance().record("advance: forward"); - for (int i = 0; i < n; ++i) ++iter; + for (int i = 0; i < n; ++i) + ++iter; } -template -void advance_custom(T& iter, int n) +template void advance_custom(T& iter, int n) { advance_impl(iter, n, typename std::iterator_traits::iterator_category{}); } @@ -233,8 +223,7 @@ TEST_F(PracticalMetaprogrammingTest, DISABLED_CompileTimeStringHashing) // TEST 6: Perfect Forwarding with Variadic Templates - Hard // ============================================================================ -template -std::unique_ptr make_unique_custom(Args&&... args) +template std::unique_ptr make_unique_custom(Args&&... args) { EventLog::instance().record("make_unique_custom: " + std::to_string(sizeof...(Args)) + " args"); return std::unique_ptr(new T(std::forward(args)...)); @@ -283,8 +272,7 @@ TEST_F(PracticalMetaprogrammingTest, PerfectForwardingWithVariadicTemplates) // TEST 7: Compile-Time Conditional Execution - Moderate // ============================================================================ -template -void process_type() +template void process_type() { if constexpr (std::is_integral_v) { @@ -330,8 +318,7 @@ TEST_F(PracticalMetaprogrammingTest, CompileTimeConditionalExecution) // TEST 8: Type-Based Function Dispatch - Hard // ============================================================================ -template -void serialize(const T& value, std::string& output) +template void serialize(const T& value, std::string& output) { if constexpr (std::is_arithmetic_v) { @@ -349,7 +336,8 @@ void serialize(const T& value, std::string& output) output += "["; for (size_t i = 0; i < value.size(); ++i) { - if (i > 0) output += ","; + if (i > 0) + output += ","; output += std::to_string(value[i]); } output += "]"; diff --git a/learning_templates/tests/test_sfinae.cpp b/learning_templates/tests/test_sfinae.cpp index 478a42b..40392ee 100644 --- a/learning_templates/tests/test_sfinae.cpp +++ b/learning_templates/tests/test_sfinae.cpp @@ -2,12 +2,12 @@ // Estimated Time: 4 hours // Difficulty: Hard +#include "instrumentation.h" #include -#include "instrumentation.h" +#include #include #include -#include class SFINAETest : public ::testing::Test { @@ -22,17 +22,13 @@ class SFINAETest : public ::testing::Test // TEST 1: Basic SFINAE with enable_if - Moderate // ============================================================================ -template -typename std::enable_if::value, T>::type -process(T value) +template typename std::enable_if::value, T>::type process(T value) { EventLog::instance().record("process: integral"); return value * 2; } -template -typename std::enable_if::value, T>::type -process(T value) +template typename std::enable_if::value, T>::type process(T value) { EventLog::instance().record("process: floating_point"); return value * 3.0; @@ -64,14 +60,12 @@ TEST_F(SFINAETest, BasicSFINAEWithEnableIf) // TEST 2: SFINAE with enable_if_t (C++14) - Moderate // ============================================================================ -template::value, int> = 0> -void handle(T value) +template ::value, int> = 0> void handle(T value) { EventLog::instance().record("handle: pointer"); } -template::value, int> = 0> -void handle(T value) +template ::value, int> = 0> void handle(T value) { EventLog::instance().record("handle: non-pointer"); } @@ -102,16 +96,21 @@ TEST_F(SFINAETest, SFINAEWithEnableIfT) // TEST 3: SFINAE for Member Detection - Hard // ============================================================================ -template -struct has_size : std::false_type {}; +template struct has_size : std::false_type +{ +}; -template -struct has_size().size())>> : std::true_type {}; +template struct has_size().size())>> : std::true_type +{ +}; class WithSize { public: - size_t size() const { return 42; } + size_t size() const + { + return 42; + } }; class WithoutSize @@ -146,15 +145,13 @@ TEST_F(SFINAETest, SFINAEForMemberDetection) // TEST 4: SFINAE with Return Type Deduction - Hard // ============================================================================ -template -auto get_size(T& container) -> decltype(container.size()) +template auto get_size(T& container) -> decltype(container.size()) { EventLog::instance().record("get_size: has_size"); return container.size(); } -template -auto get_size(T (&arr)[N]) -> size_t +template auto get_size(T (&arr)[N]) -> size_t { EventLog::instance().record("get_size: array"); return N; @@ -208,22 +205,21 @@ TEST_F(SFINAETest, DISABLED_SFINAEForIteratorDetection) // TEST 6: SFINAE vs if constexpr - Moderate // ============================================================================ -template +template void print_size_sfinae(const T& container, typename std::enable_if>::value>::type* = nullptr) { EventLog::instance().record("print_size: SFINAE vector"); } -template +template void print_size_sfinae(const T& container, typename std::enable_if::value>::type* = nullptr) { EventLog::instance().record("print_size: SFINAE string"); } -template -void print_size_if_constexpr(const T& container) +template void print_size_if_constexpr(const T& container) { if constexpr (std::is_same_v>) { @@ -266,8 +262,7 @@ TEST_F(SFINAETest, SFINAEVsIfConstexpr) // TEST 7: SFINAE with Expression SFINAE - Hard // ============================================================================ -template -auto add_if_possible(T a, T b) -> decltype(a + b) +template auto add_if_possible(T a, T b) -> decltype(a + b) { EventLog::instance().record("add_if_possible: success"); return a + b; diff --git a/learning_templates/tests/test_template_specialization.cpp b/learning_templates/tests/test_template_specialization.cpp index 4d3b0df..66d1321 100644 --- a/learning_templates/tests/test_template_specialization.cpp +++ b/learning_templates/tests/test_template_specialization.cpp @@ -2,12 +2,12 @@ // Estimated Time: 3 hours // Difficulty: Moderate to Hard +#include "instrumentation.h" #include -#include "instrumentation.h" #include -#include #include +#include class TemplateSpecializationTest : public ::testing::Test { @@ -22,8 +22,7 @@ class TemplateSpecializationTest : public ::testing::Test // TEST 1: Full Template Specialization - Easy // ============================================================================ -template -class TypeName +template class TypeName { public: static std::string name() @@ -33,8 +32,7 @@ class TypeName } }; -template<> -class TypeName +template <> class TypeName { public: static std::string name() @@ -44,8 +42,7 @@ class TypeName } }; -template<> -class TypeName +template <> class TypeName { public: static std::string name() @@ -82,8 +79,7 @@ TEST_F(TemplateSpecializationTest, FullTemplateSpecialization) // TEST 2: Partial Template Specialization - Moderate // ============================================================================ -template -class Container +template class Container { public: void add(T value) @@ -92,14 +88,16 @@ class Container data_.push_back(value); } - size_t size() const { return data_.size(); } + size_t size() const + { + return data_.size(); + } private: std::vector data_; }; -template -class Container +template class Container { public: void add(T* value) @@ -108,7 +106,10 @@ class Container data_.push_back(value); } - size_t size() const { return data_.size(); } + size_t size() const + { + return data_.size(); + } private: std::vector data_; @@ -146,8 +147,7 @@ TEST_F(TemplateSpecializationTest, PartialTemplateSpecialization) // TEST 3: Specialization for const and reference types - Moderate // ============================================================================ -template -class TypeTraits +template class TypeTraits { public: static constexpr bool is_const = false; @@ -155,8 +155,7 @@ class TypeTraits static constexpr bool is_pointer = false; }; -template -class TypeTraits +template class TypeTraits { public: static constexpr bool is_const = true; @@ -164,8 +163,7 @@ class TypeTraits static constexpr bool is_pointer = false; }; -template -class TypeTraits +template class TypeTraits { public: static constexpr bool is_const = false; @@ -173,8 +171,7 @@ class TypeTraits static constexpr bool is_pointer = false; }; -template -class TypeTraits +template class TypeTraits { public: static constexpr bool is_const = false; @@ -206,20 +203,17 @@ TEST_F(TemplateSpecializationTest, SpecializationForConstAndReference) // TEST 4: Function Template Specialization - Moderate // ============================================================================ -template -void print_type(T value) +template void print_type(T value) { EventLog::instance().record("print_type: generic"); } -template<> -void print_type(int value) +template <> void print_type(int value) { EventLog::instance().record("print_type: int specialization"); } -template<> -void print_type(const char* value) +template <> void print_type(const char* value) { EventLog::instance().record("print_type: const char* specialization"); } @@ -268,8 +262,7 @@ TEST_F(TemplateSpecializationTest, DISABLED_SpecializationForStdVector) // TEST 6: Specialization Resolution Order - Hard // ============================================================================ -template -class Selector +template class Selector { public: static std::string select() @@ -279,8 +272,7 @@ class Selector } }; -template -class Selector +template class Selector { public: static std::string select() @@ -290,8 +282,7 @@ class Selector } }; -template -class Selector +template class Selector { public: static std::string select() @@ -329,8 +320,7 @@ TEST_F(TemplateSpecializationTest, SpecializationResolutionOrder) // TEST 7: Specialization for Multiple Parameters - Hard // ============================================================================ -template -class Pair +template class Pair { public: static std::string type() @@ -340,8 +330,7 @@ class Pair } }; -template -class Pair +template class Pair { public: static std::string type() @@ -351,8 +340,7 @@ class Pair } }; -template -class Pair +template class Pair { public: static std::string type() @@ -362,8 +350,7 @@ class Pair } }; -template<> -class Pair +template <> class Pair { public: static std::string type() diff --git a/learning_templates/tests/test_type_traits.cpp b/learning_templates/tests/test_type_traits.cpp index 7d4733d..9d3bc41 100644 --- a/learning_templates/tests/test_type_traits.cpp +++ b/learning_templates/tests/test_type_traits.cpp @@ -2,13 +2,13 @@ // Estimated Time: 3 hours // Difficulty: Moderate to Hard +#include "instrumentation.h" +#include #include -#include "instrumentation.h" +#include #include #include -#include -#include class TypeTraitsTest : public ::testing::Test { @@ -80,15 +80,16 @@ TEST_F(TypeTraitsTest, TypeTransformations) // TEST 3: Custom Type Traits - Moderate // ============================================================================ -template -struct is_container : std::false_type {}; +template struct is_container : std::false_type +{ +}; -template -struct is_container().begin()), - decltype(std::declval().end()), - typename T::value_type ->> : std::true_type {}; +template +struct is_container< + T, std::void_t().begin()), decltype(std::declval().end()), typename T::value_type>> + : std::true_type +{ +}; TEST_F(TypeTraitsTest, CustomTypeTraits) { @@ -115,17 +116,15 @@ TEST_F(TypeTraitsTest, CustomTypeTraits) // TEST 4: Type Traits for Function Selection - Moderate // ============================================================================ -template -std::enable_if_t, void> -copy_data(T* dest, const T* src, size_t count) +template +std::enable_if_t, void> copy_data(T* dest, const T* src, size_t count) { EventLog::instance().record("copy_data: memcpy"); std::memcpy(dest, src, count * sizeof(T)); } -template -std::enable_if_t, void> -copy_data(T* dest, const T* src, size_t count) +template +std::enable_if_t, void> copy_data(T* dest, const T* src, size_t count) { EventLog::instance().record("copy_data: element-wise"); for (size_t i = 0; i < count; ++i) @@ -183,14 +182,12 @@ TEST_F(TypeTraitsTest, DISABLED_IsCallableTrait) // TEST 6: Compile-Time Type Selection - Moderate // ============================================================================ -template -struct conditional_type +template struct conditional_type { using type = T; }; -template -struct conditional_type +template struct conditional_type { using type = F; }; @@ -218,14 +215,15 @@ TEST_F(TypeTraitsTest, CompileTimeTypeSelection) // TEST 7: Type Traits Composition - Hard // ============================================================================ -template -struct is_const_pointer : std::false_type {}; +template struct is_const_pointer : std::false_type +{ +}; -template -struct is_const_pointer : std::true_type {}; +template struct is_const_pointer : std::true_type +{ +}; -template -constexpr bool is_const_pointer_v = is_const_pointer::value; +template constexpr bool is_const_pointer_v = is_const_pointer::value; TEST_F(TypeTraitsTest, TypeTraitsComposition) { diff --git a/learning_templates/tests/test_variadic_templates.cpp b/learning_templates/tests/test_variadic_templates.cpp index 56bf46c..ac3016b 100644 --- a/learning_templates/tests/test_variadic_templates.cpp +++ b/learning_templates/tests/test_variadic_templates.cpp @@ -2,11 +2,11 @@ // Estimated Time: 4 hours // Difficulty: Hard +#include "instrumentation.h" #include -#include "instrumentation.h" -#include #include +#include #include #include @@ -23,8 +23,7 @@ class VariadicTemplatesTest : public ::testing::Test // TEST 1: Basic Variadic Templates - Moderate // ============================================================================ -template -void log_args(Args... args) +template void log_args(Args... args) { EventLog::instance().record("log_args: " + std::to_string(sizeof...(Args)) + " arguments"); } @@ -61,8 +60,7 @@ void print_impl() EventLog::instance().record("print_impl: base case"); } -template -void print_impl(T first, Args... rest) +template void print_impl(T first, Args... rest) { EventLog::instance().record("print_impl: recursive"); print_impl(rest...); @@ -93,24 +91,20 @@ TEST_F(VariadicTemplatesTest, RecursiveVariadicExpansion) // TEST 3: Fold Expressions (C++17) - Moderate // ============================================================================ -template -auto sum_fold(Args... args) +template auto sum_fold(Args... args) { EventLog::instance().record("sum_fold"); return (args + ...); } -template -auto sum_recursive(Args... args); +template auto sum_recursive(Args... args); -template<> -auto sum_recursive() +template <> auto sum_recursive() { return 0; } -template -auto sum_recursive(T first, Args... rest) +template auto sum_recursive(T first, Args... rest) { return first + sum_recursive(rest...); } @@ -140,8 +134,7 @@ TEST_F(VariadicTemplatesTest, FoldExpressions) // TEST 4: Variadic Class Templates - Moderate // ============================================================================ -template -class Tuple +template class Tuple { public: Tuple() @@ -149,7 +142,10 @@ class Tuple EventLog::instance().record("Tuple::ctor " + std::to_string(sizeof...(Types)) + " types"); } - static constexpr size_t size() { return sizeof...(Types); } + static constexpr size_t size() + { + return sizeof...(Types); + } }; TEST_F(VariadicTemplatesTest, VariadicClassTemplates) @@ -178,8 +174,7 @@ TEST_F(VariadicTemplatesTest, VariadicClassTemplates) // TEST 5: Parameter Pack Expansion Patterns - Hard // ============================================================================ -template -void process_all(Args... args) +template void process_all(Args... args) { EventLog::instance().record("process_all: start"); (EventLog::instance().record("arg: " + std::to_string(args)), ...); @@ -228,8 +223,7 @@ TEST_F(VariadicTemplatesTest, DISABLED_VariadicMakeUnique) // TEST 7: Variadic Template with Type Constraints - Hard // ============================================================================ -template -auto sum_if_numeric(Args... args) +template auto sum_if_numeric(Args... args) { static_assert((std::is_arithmetic_v && ...), "All arguments must be numeric"); return (args + ...); @@ -260,14 +254,12 @@ TEST_F(VariadicTemplatesTest, VariadicTemplateWithTypeConstraints) // TEST 8: Index Sequence and Parameter Pack Indexing - Hard // ============================================================================ -template -void print_tuple_impl(const Tuple& t, std::index_sequence) +template void print_tuple_impl(const Tuple& t, std::index_sequence) { ((EventLog::instance().record("tuple[" + std::to_string(Is) + "]")), ...); } -template -void print_tuple(const std::tuple& t) +template void print_tuple(const std::tuple& t) { print_tuple_impl(t, std::index_sequence_for{}); } diff --git a/profile_showcase/tests/profile_showcase.cpp b/profile_showcase/tests/profile_showcase.cpp index da03d05..1917c2b 100644 --- a/profile_showcase/tests/profile_showcase.cpp +++ b/profile_showcase/tests/profile_showcase.cpp @@ -1,28 +1,28 @@ /* * PROFILE SHOWCASE * ================ - * + * * This file demonstrates the behavioral differences between all five Socratic learning profiles * in the AI teaching system. Each profile adapts its teaching style to match different skill levels: - * + * * - Junior (SWE I): Beginner-friendly with term definitions and gentle guidance * - Intermediate (SWE II): Concept connections and reasoning-focused questions * - Senior (SWE III): Precise technical depth with falsifiable claims * - Staff (SWE IV): Adversarial questioning about failure modes (Default) * - Principal (SWE V): Architecture-level with pathological edge cases - * + * * PURPOSE: * -------- * Use this file to: * 1. See how the AI adapts its teaching style to your skill level * 2. Choose which profile best matches your learning needs * 3. Understand what to expect from each profile's question depth and response style - * + * * STRUCTURE: * ---------- * All five TEST_F blocks use the SAME scenario (construct, copy, move) but ask DIFFERENT questions * and provide DIFFERENT response styles. This makes the profile differences immediately visible. - * + * * TO ACTIVATE A PROFILE: * ---------------------- * In Cursor chat, state: "profile: junior" (or intermediate, senior, staff, principal) @@ -30,24 +30,22 @@ */ #include "move_instrumentation.h" + #include #include class RuleOfFive { public: - explicit RuleOfFive(const std::string& name) - : tracked_(name) + explicit RuleOfFive(const std::string& name) : tracked_(name) { } - RuleOfFive(const RuleOfFive& other) - : tracked_(other.tracked_) + RuleOfFive(const RuleOfFive& other) : tracked_(other.tracked_) { } - RuleOfFive(RuleOfFive&& other) noexcept - : tracked_(std::move(other.tracked_)) + RuleOfFive(RuleOfFive&& other) noexcept : tracked_(std::move(other.tracked_)) { } @@ -114,23 +112,27 @@ TEST_F(ProfileShowcaseTest, JuniorProfile) // A: // R: Correct. A copy constructor creates a new object as a copy of an existing object. // R: It's called when you initialize a new object from an existing one, like `RuleOfFive obj4 = obj1`. - // R: In this case, obj4's copy constructor copies the MoveTracked member from obj1, creating an independent duplicate. - // R: Think of it like making a photocopy of a document—you have two separate documents with the same content. + // R: In this case, obj4's copy constructor copies the MoveTracked member from obj1, creating an independent + // duplicate. R: Think of it like making a photocopy of a document—you have two separate documents with the same + // content. // Q: What happens to obj2 after you write `std::move(obj2)`? // A: - // R: Good start. std::move doesn't actually move anything by itself—it just tells the compiler "it's okay to move from this object." - // R: However, when the move constructor runs, obj2.tracked_ gets moved into obj5.tracked_, leaving obj2.tracked_ in a "moved-from" state. - // R: Think of it like transferring money between bank accounts: the money moves from obj2's account to obj5's account, leaving obj2's account empty. - // R: obj2 still exists as a variable, but its internal MoveTracked member is now empty (name_ is cleared, moved_from_ flag is set). + // R: Good start. std::move doesn't actually move anything by itself—it just tells the compiler "it's okay to move + // from this object." R: However, when the move constructor runs, obj2.tracked_ gets moved into obj5.tracked_, + // leaving obj2.tracked_ in a "moved-from" state. R: Think of it like transferring money between bank accounts: the + // money moves from obj2's account to obj5's account, leaving obj2's account empty. R: obj2 still exists as a + // variable, but its internal MoveTracked member is now empty (name_ is cleared, moved_from_ flag is set). // Q: Why do we need both copy and move operations in the same class? // A: // R: Not quite. Let's break it down step by step: - // R: Copy is used when you want to keep the original object and create a duplicate. Both objects remain valid and independent. - // R: Move is used when you don't need the original object anymore. It's more efficient because it "steals" the resources instead of duplicating them. - // R: In RuleOfFive, copy means creating a new MoveTracked with the same name, while move means transferring ownership of the internal state. - // R: Real-world analogy: Copying is like duplicating a file (original stays intact), moving is like renaming/relocating a file (original location is empty). + // R: Copy is used when you want to keep the original object and create a duplicate. Both objects remain valid and + // independent. R: Move is used when you don't need the original object anymore. It's more efficient because it + // "steals" the resources instead of duplicating them. R: In RuleOfFive, copy means creating a new MoveTracked with + // the same name, while move means transferring ownership of the internal state. R: Real-world analogy: Copying is + // like duplicating a file (original stays intact), moving is like renaming/relocating a file (original location is + // empty). auto events = EventLog::instance().events(); EXPECT_EQ(EventLog::instance().count_events("copy_ctor"), 1); @@ -158,23 +160,28 @@ TEST_F(ProfileShowcaseTest, IntermediateProfile) // Q: How does the copy constructor affect the EventLog? What events get recorded? // A: - // R: Correct. The copy constructor triggers a "copy_ctor" event in EventLog because RuleOfFive's copy constructor calls MoveTracked's copy constructor. - // R: This demonstrates how copy semantics propagate through composition—when you copy the containing object (RuleOfFive), it automatically copies its members (MoveTracked). - // R: The connection to understand: EventLog is instrumented at the MoveTracked level, so every RuleOfFive operation shows up as a MoveTracked operation in the log. + // R: Correct. The copy constructor triggers a "copy_ctor" event in EventLog because RuleOfFive's copy constructor + // calls MoveTracked's copy constructor. R: This demonstrates how copy semantics propagate through composition—when + // you copy the containing object (RuleOfFive), it automatically copies its members (MoveTracked). R: The connection + // to understand: EventLog is instrumented at the MoveTracked level, so every RuleOfFive operation shows up as a + // MoveTracked operation in the log. // Q: What's the relationship between std::move() and the move constructor being called? // A: - // R: Good reasoning about std::move casting to an rvalue reference. However, the move constructor invocation is more nuanced. - // R: std::move(obj2) casts obj2 to RuleOfFive&& (rvalue reference), which makes the compiler prefer the move constructor over the copy constructor during overload resolution. - // R: How does this relate to performance? The move constructor can "steal" resources instead of duplicating them, which is why it's more efficient than copying. - // R: Think about the chain: std::move → rvalue cast → compiler selects move constructor → MoveTracked's state is transferred. + // R: Good reasoning about std::move casting to an rvalue reference. However, the move constructor invocation is + // more nuanced. R: std::move(obj2) casts obj2 to RuleOfFive&& (rvalue reference), which makes the compiler prefer + // the move constructor over the copy constructor during overload resolution. R: How does this relate to + // performance? The move constructor can "steal" resources instead of duplicating them, which is why it's more + // efficient than copying. R: Think about the chain: std::move → rvalue cast → compiler selects move constructor → + // MoveTracked's state is transferred. // Q: After the move, obj2 is in a moved-from state. What operations are still safe to perform on it? // A: // R: Not quite. The C++ standard guarantees that moved-from objects are in a "valid but unspecified state." - // R: The key is understanding what "valid" means: you can safely call obj2's destructor, and you can assign a new value to obj2. - // R: However, trying to use obj2's current value (like reading obj2.name()) is unsafe because its internal MoveTracked has been moved-from. - // R: Try reasoning about this: Why does the standard allow destructors but not value access? What invariant must remain even after a move? + // R: The key is understanding what "valid" means: you can safely call obj2's destructor, and you can assign a new + // value to obj2. R: However, trying to use obj2's current value (like reading obj2.name()) is unsafe because its + // internal MoveTracked has been moved-from. R: Try reasoning about this: Why does the standard allow destructors + // but not value access? What invariant must remain even after a move? auto events = EventLog::instance().events(); EXPECT_EQ(EventLog::instance().count_events("copy_ctor"), 1); @@ -202,24 +209,28 @@ TEST_F(ProfileShowcaseTest, SeniorProfile) // Q: What invariant must the copy constructor maintain regarding the MoveTracked member's state? // A: - // R: Correct. The invariant is that the copied object (obj4.tracked_) must have the same logical state as the source (obj1.tracked_). - // R: Specifically: obj4.tracked_.name() == obj1.tracked_.name() and both are non-moved-from (moved_from_ flag is false). - // R: Observable signal: EventLog shows exactly one "copy_ctor" event, confirming the copy occurred at the MoveTracked level. - // R: The mechanism preserves deep copy semantics—obj4.tracked_ is a distinct object from obj1.tracked_, not a shared reference. + // R: Correct. The invariant is that the copied object (obj4.tracked_) must have the same logical state as the + // source (obj1.tracked_). R: Specifically: obj4.tracked_.name() == obj1.tracked_.name() and both are non-moved-from + // (moved_from_ flag is false). R: Observable signal: EventLog shows exactly one "copy_ctor" event, confirming the + // copy occurred at the MoveTracked level. R: The mechanism preserves deep copy semantics—obj4.tracked_ is a + // distinct object from obj1.tracked_, not a shared reference. // Q: Walk through the move operation line-by-line. At what point does obj2.tracked_ become moved-from? // A: - // R: Correct that it occurs during MoveTracked's move constructor execution. However, your reasoning about the timing assumes the compiler's implementation details. - // R: What would falsify this claim? If obj2.tracked_.name() were still valid after the move, but EventLog shows "move_ctor" was called. - // R: The precise moment: Inside MoveTracked::MoveTracked(MoveTracked&& other), when `name_ = std::move(other.name_)` executes and `other.moved_from_ = true` is set. - // R: Before this line, obj2.tracked_ is valid. After this line, obj2.tracked_ is moved-from. The RuleOfFive move constructor simply delegates to this mechanism. + // R: Correct that it occurs during MoveTracked's move constructor execution. However, your reasoning about the + // timing assumes the compiler's implementation details. R: What would falsify this claim? If obj2.tracked_.name() + // were still valid after the move, but EventLog shows "move_ctor" was called. R: The precise moment: Inside + // MoveTracked::MoveTracked(MoveTracked&& other), when `name_ = std::move(other.name_)` executes and + // `other.moved_from_ = true` is set. R: Before this line, obj2.tracked_ is valid. After this line, obj2.tracked_ is + // moved-from. The RuleOfFive move constructor simply delegates to this mechanism. // Q: What observable signal in EventLog confirms that no unnecessary copies occurred? // A: - // R: This contradicts the observable behavior. EventLog shows exactly 1 "copy_ctor" and 1 "move_ctor" event, not zero copies. - // R: The evidence: obj4 = obj1 triggers copy_ctor (necessary), obj5 = std::move(obj2) triggers move_ctor (not a copy). - // R: What mechanism produces this signal? The instrumentation in MoveTracked logs each constructor call, allowing us to verify copy/move semantics at runtime. - // R: The key insight: "No unnecessary copies" means the count matches expectations (1 copy, 1 move), not that copies don't exist. + // R: This contradicts the observable behavior. EventLog shows exactly 1 "copy_ctor" and 1 "move_ctor" event, not + // zero copies. R: The evidence: obj4 = obj1 triggers copy_ctor (necessary), obj5 = std::move(obj2) triggers + // move_ctor (not a copy). R: What mechanism produces this signal? The instrumentation in MoveTracked logs each + // constructor call, allowing us to verify copy/move semantics at runtime. R: The key insight: "No unnecessary + // copies" means the count matches expectations (1 copy, 1 move), not that copies don't exist. auto events = EventLog::instance().events(); EXPECT_EQ(EventLog::instance().count_events("copy_ctor"), 1); @@ -245,30 +256,36 @@ TEST_F(ProfileShowcaseTest, StaffProfile) RuleOfFive obj4 = obj1; RuleOfFive obj5 = std::move(obj2); - // Q: What invariant breaks if the copy constructor throws after partially copying the MoveTracked member? Walk through the failure mode. - // A: - // R: Correct. The invariant is exception safety: either the object is fully constructed or it doesn't exist at all (strong guarantee). - // R: Now consider: what happens if MoveTracked's copy constructor throws after allocating memory but before setting name_? The partially-constructed obj4 would have invalid state. - // R: The failure mode cascade: (1) MoveTracked copy ctor throws, (2) RuleOfFive copy ctor propagates exception, (3) obj4 is never fully constructed, (4) no destructor runs for obj4. - // R: What invariant prevents this? MoveTracked's copy constructor must be exception-safe—if it throws, no resources are leaked because nothing was allocated yet. - // R: Observable signal: EventLog would show "copy_ctor" start but no corresponding destruction, indicating the failure point. - - // Q: Consider the ownership graph after `obj5 = std::move(obj2)`. Which objects hold valid MoveTracked instances, and what happens if obj2's destructor runs? - // A: - // R: You've identified the states correctly. However, the failure mode is more subtle: obj2's destructor will definitely run when it goes out of scope. - // R: Ownership graph: obj1.tracked_ (valid), obj2.tracked_ (moved-from), obj3.tracked_ (valid), obj4.tracked_ (valid copy of obj1), obj5.tracked_ (valid, stolen from obj2). - // R: What happens to obj2.tracked_'s destructor? It runs safely because MoveTracked's destructor handles moved-from state (empty name_, no resources to release). - // R: The critical invariant: moved-from objects must remain destructible. If obj2.tracked_'s destructor tried to release non-existent resources, it would crash. - // R: Observable signal: EventLog shows 5 construction events (3 initial + 1 copy + 1 move) and eventually 5 destructor calls when all objects go out of scope. - - // Q: What observable signal distinguishes a moved-from MoveTracked from a default-constructed one? Why does this distinction matter for lifetime correctness? - // A: - // R: This reasoning doesn't account for the failure mode when code tries to use the object's value. - // R: Walk through the signals: (1) Moved-from MoveTracked has is_moved_from() == true, default-constructed has is_moved_from() == false. (2) Moved-from has name() == "", default may have name() == "". - // R: The distinction matters because moved-from state indicates "this object was valid but got pillaged"—you know it had a value that was stolen. - // R: Default-constructed means "this object was never given a meaningful value"—it's empty by design, not because something was taken. - // R: What observable signal confirms this? EventLog shows the history: moved-from objects have a "move_ctor" in their past, default-constructed objects have only "::ctor" with default args. - // R: Why lifetime correctness? If you accidentally use a moved-from object thinking it's default-constructed, you violate the class's contract. The flag prevents this confusion. + // Q: What invariant breaks if the copy constructor throws after partially copying the MoveTracked member? Walk + // through the failure mode. A: R: Correct. The invariant is exception safety: either the object is fully + // constructed or it doesn't exist at all (strong guarantee). R: Now consider: what happens if MoveTracked's copy + // constructor throws after allocating memory but before setting name_? The partially-constructed obj4 would have + // invalid state. R: The failure mode cascade: (1) MoveTracked copy ctor throws, (2) RuleOfFive copy ctor propagates + // exception, (3) obj4 is never fully constructed, (4) no destructor runs for obj4. R: What invariant prevents this? + // MoveTracked's copy constructor must be exception-safe—if it throws, no resources are leaked because nothing was + // allocated yet. R: Observable signal: EventLog would show "copy_ctor" start but no corresponding destruction, + // indicating the failure point. + + // Q: Consider the ownership graph after `obj5 = std::move(obj2)`. Which objects hold valid MoveTracked instances, + // and what happens if obj2's destructor runs? A: R: You've identified the states correctly. However, the failure + // mode is more subtle: obj2's destructor will definitely run when it goes out of scope. R: Ownership graph: + // obj1.tracked_ (valid), obj2.tracked_ (moved-from), obj3.tracked_ (valid), obj4.tracked_ (valid copy of obj1), + // obj5.tracked_ (valid, stolen from obj2). R: What happens to obj2.tracked_'s destructor? It runs safely because + // MoveTracked's destructor handles moved-from state (empty name_, no resources to release). R: The critical + // invariant: moved-from objects must remain destructible. If obj2.tracked_'s destructor tried to release + // non-existent resources, it would crash. R: Observable signal: EventLog shows 5 construction events (3 initial + 1 + // copy + 1 move) and eventually 5 destructor calls when all objects go out of scope. + + // Q: What observable signal distinguishes a moved-from MoveTracked from a default-constructed one? Why does this + // distinction matter for lifetime correctness? A: R: This reasoning doesn't account for the failure mode when code + // tries to use the object's value. R: Walk through the signals: (1) Moved-from MoveTracked has is_moved_from() == + // true, default-constructed has is_moved_from() == false. (2) Moved-from has name() == "", default may have name() + // == "". R: The distinction matters because moved-from state indicates "this object was valid but got pillaged"—you + // know it had a value that was stolen. R: Default-constructed means "this object was never given a meaningful + // value"—it's empty by design, not because something was taken. R: What observable signal confirms this? EventLog + // shows the history: moved-from objects have a "move_ctor" in their past, default-constructed objects have only + // "::ctor" with default args. R: Why lifetime correctness? If you accidentally use a moved-from object thinking + // it's default-constructed, you violate the class's contract. The flag prevents this confusion. auto events = EventLog::instance().events(); EXPECT_EQ(EventLog::instance().count_events("copy_ctor"), 1); @@ -294,38 +311,56 @@ TEST_F(ProfileShowcaseTest, PrincipalProfile) RuleOfFive obj4 = obj1; RuleOfFive obj5 = std::move(obj2); - // Q: What implicit contract does noexcept on the move constructor establish between RuleOfFive and std::vector's reallocation strategy? What pathological scenario violates this contract? - // A: - // R: Correct. The contract noexcept establishes is: "std::vector can safely use move semantics during reallocation without exception safety concerns." - // R: In pathological case where move throws: std::vector is reallocating from old buffer to new buffer. If the 5th element's move constructor throws, elements 1-4 are in the new buffer, 5-10 are in the old buffer. Strong exception guarantee is violated—you can't restore the original state. - // R: Observable signals if contract is violated: (1) std::is_nothrow_move_constructible::value would be false, (2) std::vector would fall back to copy constructor (EventLog would show copy_ctor instead of move_ctor during reallocation), (3) performance degrades from O(n) moves to O(n) copies. - // R: The systemic consequence: Without noexcept, every std::vector operation becomes pessimistically safe, copying instead of moving. This cascades through the entire codebase—any container holding RuleOfFive pays the copy penalty. - // R: Architecture-level invariant: Move constructors that manage resources must either be noexcept OR must restore source object state on throw. RuleOfFive delegates to MoveTracked, which is noexcept, preserving this contract. - - // Q: If MoveTracked's move constructor threw an exception mid-operation, how would the RuleOfFive's move constructor restore exception safety? What systemic lifetime hazards emerge if it fails? - // A: - // R: Surface-level correct. Architecture-level: the hazard is more insidious than just "exception propagates." - // R: Consider the pathological scenario: RuleOfFive move constructor calls `tracked_(std::move(other.tracked_))`. If this throws after MoveTracked has transferred name_ but before setting moved_from_ flag, what state is other.tracked_ in? - // R: Systemic lifetime hazards: (1) other.tracked_ has invalid state (name_ is moved but flag is unset), (2) other's destructor still runs because other exists on the stack, (3) destructor operates on inconsistent state (thinks it owns resources that were stolen), (4) double-free or use-after-free depending on resource type. - // R: What happens when std::vector holds 10,000 RuleOfFive objects and the 5,000th move throws? First 4,999 are moved (sources are zombies), element 5,000 is partially moved (source is corrupted), elements 5,001-10,000 are untouched. Cleanup is impossible without explicit exception handling in the move constructor. - // R: Observable signals that would expose this: (1) EventLog shows "move_ctor" start but no completion, (2) Destructor runs on corrupted object (moved_from_ == false but name_.empty() == true), (3) Sanitizers would detect use-after-move if the contract is violated. - // R: The implicit contract RuleOfFive can't restore on throw: it has no way to "unmove" tracked_ once std::move() succeeds. This is why move constructors MUST be noexcept for types managing resources. - - // Q: Consider aliasing: if RuleOfFive stored `MoveTracked* tracked_` instead of `MoveTracked tracked_`, what architecture-level ownership invariant breaks during move assignment, and what observable signals would expose dangling pointers? - // A: - // R: This overlooks the systemic hazard of pointer-based ownership with move semantics. - // R: Pathological case with `MoveTracked* tracked_`: Move assignment `obj5 = std::move(obj2)` would transfer the pointer: `obj5.tracked_ = obj2.tracked_; obj2.tracked_ = nullptr;`. - // R: What implicit contract breaks? Lifetime coupling: obj2's destructor runs `delete tracked_`, but tracked_ is nullptr (safe). obj5's destructor also runs `delete tracked_`, deleting the actual object. So far, correct. - // R: But consider the failure mode: What if obj5 already pointed to a MoveTracked before the move assignment? The old obj5.tracked_ is leaked because we overwrote the pointer without deleting it first. - // R: Walk through the lifetime graph: (1) obj5 initially holds MoveTracked("Object5"), tracked_ points to heap allocation A. (2) Move assignment: obj5.tracked_ = obj2.tracked_ (now points to allocation B), allocation A is leaked. (3) obj2.tracked_ = nullptr. (4) Both destructors run: obj5 deletes B (correct), obj2 deletes nullptr (safe), but A is never deleted. - // R: Observable signals exposing this: (1) EventLog shows "::ctor" for Object5 but no corresponding "::dtor" (allocation A leaked), (2) Valgrind reports "definitely lost" memory, (3) ASAN detects the leak at program exit, (4) use_count would be wrong if using shared_ptr semantics. - // R: Architecture-level invariant violated: RAII requires destructor to release resources. With raw pointer members, move assignment must explicitly `delete tracked_` before overwriting. By-value members (MoveTracked tracked_) handle this automatically via compiler-generated destructors—this is why composition is safer than pointer ownership. + // Q: What implicit contract does noexcept on the move constructor establish between RuleOfFive and std::vector's + // reallocation strategy? What pathological scenario violates this contract? A: R: Correct. The contract noexcept + // establishes is: "std::vector can safely use move semantics during reallocation without exception safety + // concerns." R: In pathological case where move throws: std::vector is reallocating from old buffer to new buffer. + // If the 5th element's move constructor throws, elements 1-4 are in the new buffer, 5-10 are in the old buffer. + // Strong exception guarantee is violated—you can't restore the original state. R: Observable signals if contract is + // violated: (1) std::is_nothrow_move_constructible::value would be false, (2) std::vector would fall + // back to copy constructor (EventLog would show copy_ctor instead of move_ctor during reallocation), (3) + // performance degrades from O(n) moves to O(n) copies. R: The systemic consequence: Without noexcept, every + // std::vector operation becomes pessimistically safe, copying instead of moving. This cascades through the entire + // codebase—any container holding RuleOfFive pays the copy penalty. R: Architecture-level invariant: Move + // constructors that manage resources must either be noexcept OR must restore source object state on throw. + // RuleOfFive delegates to MoveTracked, which is noexcept, preserving this contract. + + // Q: If MoveTracked's move constructor threw an exception mid-operation, how would the RuleOfFive's move + // constructor restore exception safety? What systemic lifetime hazards emerge if it fails? A: R: Surface-level + // correct. Architecture-level: the hazard is more insidious than just "exception propagates." R: Consider the + // pathological scenario: RuleOfFive move constructor calls `tracked_(std::move(other.tracked_))`. If this throws + // after MoveTracked has transferred name_ but before setting moved_from_ flag, what state is other.tracked_ in? R: + // Systemic lifetime hazards: (1) other.tracked_ has invalid state (name_ is moved but flag is unset), (2) other's + // destructor still runs because other exists on the stack, (3) destructor operates on inconsistent state (thinks it + // owns resources that were stolen), (4) double-free or use-after-free depending on resource type. R: What happens + // when std::vector holds 10,000 RuleOfFive objects and the 5,000th move throws? First 4,999 are moved (sources are + // zombies), element 5,000 is partially moved (source is corrupted), elements 5,001-10,000 are untouched. Cleanup is + // impossible without explicit exception handling in the move constructor. R: Observable signals that would expose + // this: (1) EventLog shows "move_ctor" start but no completion, (2) Destructor runs on corrupted object + // (moved_from_ == false but name_.empty() == true), (3) Sanitizers would detect use-after-move if the contract is + // violated. R: The implicit contract RuleOfFive can't restore on throw: it has no way to "unmove" tracked_ once + // std::move() succeeds. This is why move constructors MUST be noexcept for types managing resources. + + // Q: Consider aliasing: if RuleOfFive stored `MoveTracked* tracked_` instead of `MoveTracked tracked_`, what + // architecture-level ownership invariant breaks during move assignment, and what observable signals would expose + // dangling pointers? A: R: This overlooks the systemic hazard of pointer-based ownership with move semantics. R: + // Pathological case with `MoveTracked* tracked_`: Move assignment `obj5 = std::move(obj2)` would transfer the + // pointer: `obj5.tracked_ = obj2.tracked_; obj2.tracked_ = nullptr;`. R: What implicit contract breaks? Lifetime + // coupling: obj2's destructor runs `delete tracked_`, but tracked_ is nullptr (safe). obj5's destructor also runs + // `delete tracked_`, deleting the actual object. So far, correct. R: But consider the failure mode: What if obj5 + // already pointed to a MoveTracked before the move assignment? The old obj5.tracked_ is leaked because we overwrote + // the pointer without deleting it first. R: Walk through the lifetime graph: (1) obj5 initially holds + // MoveTracked("Object5"), tracked_ points to heap allocation A. (2) Move assignment: obj5.tracked_ = obj2.tracked_ + // (now points to allocation B), allocation A is leaked. (3) obj2.tracked_ = nullptr. (4) Both destructors run: obj5 + // deletes B (correct), obj2 deletes nullptr (safe), but A is never deleted. R: Observable signals exposing this: + // (1) EventLog shows "::ctor" for Object5 but no corresponding "::dtor" (allocation A leaked), (2) Valgrind reports + // "definitely lost" memory, (3) ASAN detects the leak at program exit, (4) use_count would be wrong if using + // shared_ptr semantics. R: Architecture-level invariant violated: RAII requires destructor to release resources. + // With raw pointer members, move assignment must explicitly `delete tracked_` before overwriting. By-value members + // (MoveTracked tracked_) handle this automatically via compiler-generated destructors—this is why composition is + // safer than pointer ownership. auto events = EventLog::instance().events(); EXPECT_EQ(EventLog::instance().count_events("copy_ctor"), 1); EXPECT_EQ(EventLog::instance().count_events("move_ctor"), 1); } - - - -