SimplifyC++ Article
OOP in Modern C++: Own the Tools — Do Not Let the Tools Own You

One of the most common mistakes among some C++ programmers is treating Object-Oriented Programming as if it were the official, preferred, or only legitimate way to write good C++ software.
On the opposite side, another trend has emerged that rejects OOP almost entirely, as if using classes, inheritance, or polymorphism were automatically signs of outdated software design.
Both extremes unnecessarily restrict a language that was deliberately designed to offer far more freedom.
C++ is not only an OOP language. It is not only a functional language. It is not only a generic programming language, and it is certainly not merely an extended version of C.
C++ is a multi-paradigm programming language, and one of its greatest strengths is that it allows you to choose the programming style that best matches the problem instead of forcing the problem into one preferred paradigm.
The more important question is therefore not:
Should I use OOP or not?
A better question is:
Which design expresses this problem most clearly, with the least unnecessary complexity, while providing the required safety, performance, and maintainability?
OOP Is a Powerful Tool, Not a Programming Religion
When starting a new project, you should not immediately ask:
Where should I place the classes?
What should the base class be?
What should inherit from what?
Start with the problem itself.
You may simply need a function:
double calculate_tax(double value);
There may be absolutely no reason to transform it into:
class TaxCalculator {
public:
double calculate(double value);
};
If there is no state, invariant, ownership requirement, or meaningful abstraction that needs to be represented, the class may be little more than an unnecessary wrapper.
In another situation, however, you may have an abstraction that owns resources, maintains internal state, and must protect certain rules. In that case, a class may be exactly the right tool:
class File {
public:
explicit File(std::string_view path);
~File();
void write(std::span<const std::byte> data);
private:
// resource and invariant
};
Here, the class is not merely decorative OOP.
It represents resource ownership, lifetime, and usage constraints.
This is where one of the most powerful characteristics of C++ appears: the ability to combine abstraction, RAII, and deterministic resource management without sacrificing low-level control.
Understand OOP Even If You Decide Not to Use It Often
Freedom from mandatory OOP does not mean an advanced C++ programmer can afford to ignore it.
There is a major difference between:
A programmer who does not use OOP because they do not understand it well
and:
A programmer who understands OOP deeply and deliberately decides that the current problem does not require it.
The second programmer is making an engineering decision.
The first programmer may simply be constrained by the limits of their knowledge.
A serious C++ programmer should understand concepts such as:
- Encapsulation
- Abstraction
- Invariants
- Composition
- Inheritance
- Runtime Polymorphism
- Virtual Functions
- Interfaces
- Object Lifetime
- RAII
- Value Semantics
Then they can decide which tools are actually appropriate.
Knowing a tool does not mean you must use it.
But not knowing the tool means you have already lost an important design option.
The Real Power of C++ Is the Ability to Mix Paradigms
Consider a real-world project.
The same program can contain free functions for data transformation:
auto normalize(Data& data);
Simple data types:
struct Point {
double x;
double y;
};
Classes that protect invariants:
class DatabaseConnection {
// ...
};
Templates:
template<typename T>
void process(T&& value);
Algorithms:
std::ranges::sort(values);
Lambdas:
std::ranges::for_each(values, [](auto& x) {
// ...
});
And perhaps runtime polymorphism in one carefully selected part of the system:
class Renderer {
public:
virtual void render() = 0;
virtual ~Renderer() = default;
};
This is not necessarily a confused mixture of programming paradigms.
If the architecture is deliberate and disciplined, it represents exactly one of the major strengths of C++.
A project does not need to be an “OOP project,” a “functional project,” or a “generic programming project.”
It can simply be:
A well-designed C++ project.
Modern C++ Has Reduced the Need for Excessive Traditional OOP
Modern C++ provides many tools capable of solving problems that were once commonly addressed using large object hierarchies.
Today we have:
- Templates
- Concepts
- Lambdas
constexpr- Ranges
- Algorithms
- Type inference
- Smart pointers
- RAII
std::variantstd::optional- Function objects
- Compile-time polymorphism
- Value-oriented design
Therefore, not every relationship in a software system needs to become inheritance.
And not every variation in behavior requires a virtual function.
Sometimes:
template<typename Renderer>
void draw(Renderer& renderer);
may be better than introducing an entire class hierarchy.
In another situation:
virtual void draw() = 0;
may be exactly the correct design, especially when the concrete types are not known until runtime.
A good engineer does not belong ideologically to one approach.
A good engineer understands the difference between the problems.
Prefer Composition to Inheritance — But Do Not Turn That Into Another Dogma
One of the valuable lessons in modern software design is to avoid unnecessarily deep inheritance hierarchies.
A
└── B
└── C
└── D
└── E
Such a hierarchy does not automatically make the design more professional.
It may instead indicate that the system has become tightly coupled and increasingly difficult to understand, test, modify, and maintain.
In many cases, composition provides greater flexibility with less coupling.
But this should not become a blind prohibition against inheritance either.
When inheritance represents a real and stable abstraction, runtime polymorphism and class hierarchies remain powerful tools.
The problem is not inheritance itself.
The problem is using inheritance when the design does not genuinely need it.
Do Not Treat Design Patterns as Sacred Templates
Design patterns gave software engineering something extremely useful: a shared vocabulary for recurring design solutions.
Factory, Observer, Strategy, Adapter, Visitor, and many others are important concepts that advanced programmers should understand.
But understanding a pattern does not mean constantly searching for somewhere to insert it.
One of the worst side effects of studying design patterns superficially is that programmers can begin seeing every problem as an opportunity to apply another pattern:
Here we need a Factory.
Here an Abstract Factory.
Here a Strategy.
Here an Observer.
And perhaps another interface just in case we need it someday.
Suddenly, a problem that could have been solved clearly in 200 lines becomes an architecture requiring 2,000 lines.
A design pattern should emerge because the problem benefits from it, not because the programmer wants to prove familiarity with the pattern.
You Can Develop Your Own Design Style
This is an important stage in the maturity of a programmer.
After years of programming, you naturally begin to develop your own engineering personality.
You may discover preferred ways of organizing:
- Modules
- Interfaces
- Resources
- Error handling
- Ownership
- State
- Algorithms
- Data flow
Your architecture may not be a literal implementation of any famous design pattern.
That is perfectly acceptable.
In fact, it may be excellent.
Many well-known design patterns were not handed down as eternal rules. They were identified because engineers repeatedly observed certain useful solutions appearing in different software systems and eventually gave those recurring approaches names.
Therefore, there is nothing wrong with discovering your own architecture or idioms when they fit your project better.
But there is one essential condition:
Independent design does not mean inventing chaos.
Programming Personality Does Not Mean Writing Strange Code
Some programmers confuse independence with deliberately violating every familiar convention.
If you create an architecture that nobody except you can understand, that does not automatically make it innovative.
If you create unusual conventions simply to look different, that is not engineering personality either.
A good personal design style should be:
- Clear
- Explainable
- Technically justified
- Testable
- Maintainable
- Appropriate for the required performance
- Clear about ownership
- Clear about lifetime
- Clear about dependencies
If another engineer asks:
Why did you design the system this way?
your answer should be stronger than:
Because that is my style.
Instead, you should be able to explain:
I chose value semantics here to reduce shared mutable state.
Or:
I used runtime polymorphism here because implementations are loaded dynamically.
Or:
I did not use classes here because these operations are stateless and there are no invariants that require encapsulation.
At that point, your programming personality becomes more than personal preference.
It becomes engineering reasoning.
Freedom in C++ Does Not Mean Freedom From Discipline
C++ gives programmers enormous freedom.
That freedom is one of the reasons for its extraordinary power, but also one of the reasons the language demands experience and discipline.
You can write code that looks very close to C.
You can build heavily generic architectures.
You can create large object-oriented systems.
You can perform significant work at compile time.
And you can work extremely close to hardware and memory.
The greater the freedom, however, the greater the need for engineering discipline.
This is where the C++ Core Guidelines become especially valuable.
They provide professional guidance for writing modern C++ with greater safety, clarity, maintainability, and efficiency. They cover areas including interfaces, functions, classes, class hierarchies, resource management, templates, concurrency, performance, and general programming philosophy.
It is important, however, to distinguish between the ISO C++ Standard and the C++ Core Guidelines.
The ISO C++ Standard formally defines the language and its standard library. The C++ Core Guidelines, on the other hand, are professional engineering guidelines intended to help programmers use the language effectively. They are not themselves mandatory rules of the ISO standard.
This distinction fits perfectly with the philosophy discussed throughout this article:
Guidelines help you use freedom responsibly; they are not intended to eliminate freedom.
Do Not Create a Class Simply Because You Can
Before introducing a class, ask:
- What responsibility does this type represent?
- Is there an invariant that needs protection?
- Is there state that needs to be managed?
- Is there a resource whose lifetime should be tied to the object's lifetime?
- Is there a genuine abstraction here?
- Will encapsulation provide real value?
If the answer to these questions is largely no, a simple
struct, function, algorithm, or template may provide a cleaner design.
Do Not Use Virtual Functions Simply to Claim Polymorphism
C++ provides more than one form of polymorphism.
There is runtime polymorphism:
virtual void execute() = 0;
And there is compile-time polymorphism using templates and concepts.
There is no universal rule saying one approach is always superior.
If the relevant types are known during compilation and performance is important, templates may provide an excellent solution.
But if you are building systems such as:
- Plugin systems
- GUI frameworks
- Runtime-loaded components
- Hardware abstraction layers
- Extensible applications
runtime polymorphism may be the cleanest and most natural design.
The real question is:
What type of flexibility does the system actually require?
Sometimes the Best Design Is Simply a Set of Functions
Some programmers seem to regard free functions as less sophisticated than methods.
But:
auto calculate_checksum(std::span<const std::byte> data);
may be considerably clearer than forcing the same operation into a class that provides no useful state or abstraction.
Modern C++ has reinforced the importance of free functions, algorithms, ranges, and generic programming.
Not everything needs to be an object.
And not every operation needs to be a method.
On the Other Hand, Do Not Fear OOP When You Actually Need It
The opposite extreme is equally problematic.
After hearing repeated criticism of OOP, some programmers begin avoiding it even where it provides the most natural design.
This means voluntarily giving up one of the major strengths of C++.
Many systems become considerably clearer when important concepts are represented using types that provide:
- Encapsulated state
- Invariants
- Resource ownership
- Behavior
- Interfaces
- Controlled polymorphism
In such cases, C++ can provide extremely powerful object-oriented programming because OOP is combined with:
- RAII
- Deterministic destruction
- Templates
- Value semantics
- Precise memory control
- Compile-time abstractions
- Runtime abstractions
At that point, OOP is not merely a way to organize project files.
It becomes a powerful mechanism for creating high-level abstractions while preserving low-level control.
Do Not Start From the Paradigm — Start From the Problem
Instead of asking:
How do I design this using OOP?
Ask:
What is the actual nature of this problem?
If the problem is mostly about transforming data, algorithms and functions may be the right answer.
If the problem involves a stateful entity with internal rules, a class may be appropriate.
If the problem requires generic behavior across many types, templates and concepts may be better.
If behavior must change dynamically at runtime, runtime polymorphism may be appropriate.
If the application is data-intensive and sensitive to cache behavior, Data-Oriented Design may be better than constructing a large graph of objects.
And if the problem contains several of these characteristics, use several paradigms.
A multi-paradigm language exists precisely so that you can do this.
Invent Your Own Design — But Test It
There is nothing wrong with saying:
I have developed my own architecture.
But then you should challenge that architecture with serious questions:
- Are dependencies clear?
- Is ownership clear?
- Can one component change without destabilizing the entire system?
- Is the system easy to test?
- Are interfaces small and focused?
- Do the abstractions justify their cost?
- Are there unnecessary allocations?
- Is there unnecessary inheritance?
- Is object lifetime obvious?
- Can another programmer understand the architecture?
- Does measured performance match your assumptions?
Profilers, benchmarks, static analyzers, compiler warnings, tests, and code reviews are far more valuable than loyalty to any particular design paradigm.
The Beginner Searches for Rules — the Expert Searches for Reasons
At the beginning of our programming journey, rules are valuable:
Use OOP.
Prefer composition.
Avoid global state.
Use RAII.
Avoid rawnew.
Such rules help establish good foundations.
But as experience grows, the programmer should move from asking:
What does the rule say?
to:
Why does this rule exist?
and eventually:
Are the reasons behind this rule present in this particular case?
That is where real engineering judgment begins.
This does not mean ignoring good rules.
It means understanding them deeply enough to know when they apply and why.
The Best C++ Is Not the C++ With the Most Features
A strong programmer is not someone who can place:
Templates + Concepts + CRTP + Multiple Inheritance + TMP + Coroutines + Virtual Dispatch
inside the same program.
A strong programmer is someone who can solve the problem using the minimum complexity necessary to satisfy the real requirements.
If a function is enough, use a function.
If a struct is enough, use a struct.
If a class protects an invariant, use a class.
If polymorphism is necessary, choose the appropriate form of polymorphism.
If inheritance genuinely expresses the design, use inheritance.
And if none of these mechanisms is necessary, do not add them merely to make the architecture appear sophisticated.
Develop Your Own C++ Personality
Once you understand the language deeply, do not let a book, framework, design school, or famous programmer make every design decision on your behalf.
Learn from all of them.
Study OOP deeply.
Study generic programming.
Understand functional techniques.
Understand Data-Oriented Design.
Understand RAII and value semantics.
Read the C++ Core Guidelines.
Understand the Standard Library.
Learn how processors, memory, caches, and hardware behave when they matter to the problem.
Then develop your own engineering style.
One of your projects may rely heavily on classes.
Another may consist mostly of functions and templates.
A third may contain a low-level core that looks relatively close to C, modern abstractions above it, and a small set of object-oriented interfaces at the boundaries.
All of these can represent excellent C++.
Conclusion: Do Not Become a Follower of a Paradigm — Become an Engineer
OOP in modern C++ is not mandatory.
Nor is it a mistake that should be eliminated.
It is one of the most powerful tools available inside a large, multi-paradigm programming language.
Learn it deeply enough to understand both its strengths and its limits.
Then use it when it serves the problem.
Use only part of it when that is sufficient.
Leave it aside when it adds no value.
Combine it with functions, generic programming, algorithms, functional techniques, and Data-Oriented Design when that combination produces the best architecture.
Do not copy design patterns simply because they are famous.
Do not be afraid to develop architectures and idioms of your own that reflect your experience and the needs of your project.
But build that independence on knowledge, discipline, measurement, clarity, and engineering reasoning, not on being different merely for the sake of being different.
Use the C++ Core Guidelines and the accumulated experience of the C++ community as an engineering compass rather than as a mechanism that prevents you from thinking.
Ultimately, the objective is not to write:
Good OOP.
Nor:
Good Functional C++.
Nor is the objective to prove that you are using every modern language feature.
The objective is to write:
C++ that is clear, safe, efficient, maintainable, and designed specifically for the problem in front of you.
That is the real freedom C++ gives to a programmer who truly understands the language.
What did you think?
Sign in to react or comment.
Comments
0No comments yet.