Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 90

Modern C++ Refactoring Techniques

Recommended resources

Hello! Welcome to the final project module of our C++ course.

In the previous lessons, you've built a functional thread pool, tested it for correctness under stress, and analyzed its performance, identifying key bottlenecks like lock contention. You now have a working, and perhaps even a high-performance, piece of concurrent machinery.

This lesson is about transforming that functional prototype into a professional, portfolio-ready library. We will focus on the learning outcome: Refactor the codebase to improve modularity, readability, and adherence to modern C++ best practices. This is where we elevate your code from merely working to being well-engineered—easy to understand, maintain, and extend. We'll explore core software design principles, practical refactoring patterns, and project structuring techniques that distinguish professional code from a hobby project.

1. The Philosophy of Good Design: Designing for Change

Before we write any code, we must understand why we're doing this. A program that works today might be a nightmare to debug or extend tomorrow. The root cause of this is often poor dependency management.

To get a clear perspective on this, let's watch the opening of a classic talk on class design.

Back to Basics: Designing Classes (part 1 of 2) - Klaus Iglberger - CppCon 2021

Klaus Iglberger's CppCon talk, 'Back to Basics: Designing Classes', provides a foundational understanding of why good design is critical. He argues that the primary challenge in software development is 'change' and that managing 'dependencies' is the key to mastering it.

Please watch from 02:04 to 05:14. Focus on the core argument that your goal as a designer is to make your code easy to change and easy to extend.

This is the mindset to adopt for your thread pool project. Every design decision should be weighed against this question: "Does this make my code easier or harder to change in the future?"


2. Core Principles for Maintainable Code

So, how do we design for change? We can start with a set of time-tested principles. These are not rigid laws but powerful guidelines that help manage dependencies and complexity.

Back to Basics: Designing Classes (part 1 of 2) - Klaus Iglberger - CppCon 2021

In the same talk, Klaus Iglberger introduces three of the most important design principles: the Single Responsibility Principle (SRP), the Open/Closed Principle (OCP), and the Don't Repeat Yourself (DRY) principle.

Now watch from 15:08 to 18:35. Pay close attention to how he rephrases SRP from the vague 'do one thing' to the more actionable 'separate concerns to isolate and simplify change'.

Let's apply these directly to your thread pool:

  • Single Responsibility Principle (SRP): Your ThreadPool class should be responsible for managing the lifecycle of threads and the overall submission of tasks. It should not also be responsible for the details of how tasks are queued and scheduled. That's a separate concern. Similarly, a task queue class should only manage the queueing of tasks, not the execution logic.
  • Open/Closed Principle (OCP): Your thread pool should be open to extension but closed for modification. For example, you should be able to add a new scheduling strategy (like the work-stealing mechanism from our last lesson) without having to rewrite the core ThreadPool class or its public API.
  • Don't Repeat Yourself (DRY): If you find yourself writing similar synchronization or logging code around every task submission, that's a signal to abstract it into a single place.
Test your understanding!

Imagine your ThreadPool class currently has a single blocking queue inside it. You decide to add a work-stealing scheduler, which requires a vector of deques. To implement this, you add an if/else block inside every method of the ThreadPool class to check which scheduling mode is active. Which design principle does this approach most clearly violate, and why?

Show answer

This most clearly violates the Open/Closed Principle (OCP). By adding if/else checks everywhere, you are modifying the existing, working ThreadPool class to add new behavior. A better design, following OCP, would be to define a Scheduler interface and have different implementations (BlockingQueueScheduler, WorkStealingScheduler). The ThreadPool would then use one of these strategies without needing to know its internal details, allowing you to add new schedulers in the future without modifying the ThreadPool class at all.


3. Practical Refactoring: Patterns and Idioms

Principles are great, but how do we apply them? Design patterns provide reusable solutions to common problems. The NVI idiom is a C++ specific technique that helps enforce class invariants.

Back to Basics: Designing Classes (part 1 of 2) - Klaus Iglberger - CppCon 2021

Let's continue with Klaus Iglberger's talk to see how these principles translate into concrete patterns. He demonstrates the Strategy pattern and the Template Method pattern (in C++, often implemented as the Non-Virtual Interface idiom).

Watch these two segments: Strategy Pattern (19:54 - 25:51): See how he extracts the 'draw' behavior from a Shape class into a separate DrawStrategy. This is exactly how you could separate your scheduling logic from the ThreadPool class. Template Method / NVI (29:35 - 37:50): Understand how making public interface functions non-virtual (write) and private implementation functions virtual (do_write) allows you to separate concerns between the caller of the interface and the implementer of the derived class. This is perfect for adding logging, validation, or other boilerplate code without duplicating it.

Applying these to your project:

  • Strategy Pattern: Define an abstract IScheduler base class with virtual methods like schedule(Task t) and get_task(). Then, create concrete implementations: CentralQueueScheduler and WorkStealingScheduler. Your ThreadPool will hold a std::unique_ptr<IScheduler> and delegate the scheduling logic to it. This makes your design modular and compliant with OCP.
  • Non-Virtual Interface (NVI): Your public ThreadPool::submit(Task t) method can be non-virtual. Inside it, you can handle any logic that is common to all schedulers (e.g., wrapping the task, logging, checking if the pool is running). Then, it calls a private virtual method, p_scheduler->do_submit(wrapped_task), which contains the strategy-specific implementation. This enforces invariants and adheres to DRY.

4. Project-Level Structure and Readability

Good design isn't just about individual classes; it's also about how the entire project is organized. A clean, logical file structure and readable code are hallmarks of a professional project.

Project Modularity

Over-engineering a small project is a risk, but demonstrating that you understand how to structure a larger system is invaluable for your portfolio.

The Most Professional C++ Project I've Ever Reviewed // Code Review

The Cherno provides an excellent code review of a C++ project that, while simple, demonstrates professional structure. He discusses the value of modularity and critiques common architectural mistakes.

Watch from 10:04 to 22:07. Focus on: The rationale for splitting code into static libraries (core, render, main). The critique of putting too much logic in main.cpp and having functions with 'sub-loops'. The analysis of how poor architectural choices (like putting game logic in the UI) lead to messy code.

For your thread pool, this means:

  • Your core thread pool implementation should be self-contained, likely in a ThreadPool.hpp file (if header-only) within an include/ directory.
  • Your unit and stress tests should be in a separate tests/ directory.
  • Examples of how to use your library should be in an examples/ directory.
  • Your build system (e.g., CMake) should define separate targets for the library, tests, and examples.

Naming and Testability

Clean code is readable code. Spend time on good names. As Klaus Iglberger notes, is_empty() is better than empty(). A function get_num_threads() is clearer than threads().

Designing for testability is also crucial. Private functions are hard to test. This is often a sign that the logic in that private function is a separate concern that should be moved to its own class or into a free function in a private namespace, where it can be tested in isolation.

Back to Basics: Designing Classes (part 1 of 2) - Klaus Iglberger - CppCon 2021

Let's revisit Klaus Iglberger's talk one last time to hear his thoughts on naming and designing for testability.

Watch these final two clips: Naming (05:14 - 08:31): Internalize the importance of clear, intuitive names. Testability (38:17 - 44:58): Pay attention to the advice on testing private members: the best way is often to refactor so the logic is no longer private.


5. Case Study: A Professional Open-Source Thread Pool

Let's look at a real-world example that embodies these principles. BS::thread_pool is a popular, modern, header-only C++ thread pool library. Its documentation and design are excellent references for your own project.

BS::thread_pool Library

The README for BS::thread_pool on GitHub is a masterclass in API design and documentation. We will study it to see how a professional library presents itself.

Please spend about 15 minutes on the BS::thread_pool GitHub page. I want you to focus on two things: Skim the 'Overview of features' section. Notice the clean API (e.g., submit_task, submit_loop), the configurability (pausing, priorities), and the utility classes (synced_stream). Scroll down to the 'Complete library reference' section and look at the API for the BS::thread_pool class. How is it structured? What do the function names tell you? This serves as a great model for your own API documentation.

This library is an excellent example of:

  • A Clean and Expressive API: The purpose of functions like submit_loop and detach_task is immediately clear.
  • Modularity through Configuration: It uses template parameters to enable features like pausing (BS::pause_thread_pool) or priorities (BS::priority_thread_pool), so you only pay for what you use. This is a form of compile-time OCP.
  • Excellent Documentation: The README you just reviewed is a perfect example of what a potential employer would want to see for a portfolio project. It explains the why, the how, and provides a complete API reference.
  • Adherence to Modern C++: It uses lambdas, futures, C++20 modules, and other modern features effectively.

Conclusion

You are now equipped with the principles and patterns to refactor your thread pool into a truly professional piece of software. This is a critical step in building a strong portfolio. It's not enough to solve a hard problem; you must also present the solution in a clean, understandable, and maintainable way.

Key Takeaways:

  • Design for Change: The primary goal of good software design is to make future changes easier by managing dependencies.
  • Apply Core Principles: Use SRP, OCP, and DRY to guide your class and function design.
  • Use Patterns for Structure: The Strategy and NVI patterns are powerful tools for creating modular and robust class hierarchies.
  • Structure Your Project: Separate your library code, tests, and examples into a logical directory structure with a clean build system.
  • Document Thoroughly: A well-documented API and clear README are non-negotiable for a portfolio project. Look to high-quality open-source projects for inspiration.
  • Reference the Guidelines: For any C++ best practice, the C++ Core Guidelines are your ultimate source of truth. Bookmark this for future reference.

Next Lesson Preview:
This was the final implementation-focused lesson for your project! The next and final lesson in our course is: Prepare a presentation explaining the project's architecture, design patterns used, and technical challenges overcome. The work you do refactoring your code now will form the foundation of that presentation, allowing you to confidently explain your design choices.