But there's a problem. Like all new tools, there is a certain class of people who believe that this tool should be used everywhere. These people idolize the tool, believing that the tool holds no wrong and should be used wherever possible. And even some places that it shouldn't be possible.
The key to taking C++ to the Limit is not just knowing when to use something; it's knowing when not to.
So, we're going to talk about how to properly use OOP. Or, more accurately, why it shouldn't be the default state.
First, a clarification. When I say "use OOP", I mean to derive a class from a base class for the purpose of using polymorphism to cause something to happen. Any use of classes (or structs) is not OOP; it doesn't become OOP unless you're using a class hierarchy and taking advantage of polymorphism. A class hierarchy alone isn't enough; that's merely a way of sharing interfaces. It is when you have a class hierarchy and you're using polymorphism to treat the derived class like a regular base class instance that you're truly being polymorphic.
Basically, the question when you're designing an object with relationships to other objects is this: is it "is a" or "has a"?
"Is a" means exactly what it says: Object B is an Object A. That is, it is reasonable to take a pointer to Object B and cast it into a pointer to Object A. If this is the kind of relationship you want between them, then you're using OOP.
"Has a" is likewise rather self-explanatory: Object B has an Object A. That is, it stores internally a pointer to Object A. Here, you're not using OOP.
However, even with this distinction in relationships, we need to go deeper to get at what the ramifications of either choice are.
Using "Is a" nets you some advantages.
It means that the public interface of Object A (the parent) are immediately usable by users of Object B. Object B also gets to use the protected interface of Object A. Basic inheritance.
Second, it gives you polymorphism: the ability to make code that used Object A now use Object B while redefining some of Object A's behavior, all without rewriting that code.
Basic inheritance may not really be what you want. Object A may have some public interface that Object B wants, but it may not want all of it. So users of Object B will have extra functions that they can use.
Object B has a 1:1 relationship with Object A. That is, there can't be two Object A instances that Object B has a relationship with.
If there are implementation details in Object A that are of no value to Object B, then Object B instances will have a lot of dead weight.
Notice something about those limitations? They all happen only when you want to acknowledge that Object B exists! That is, when you're using the derived class explicitly. Whether this was through the use of a cast (static_cast, dynamic_cast, or C-standard cast) or something else, it doesn't matter. The limitations come into play when the derived class needs to be used frequently. As long as the code is treating Object B as through it were Object A, and only talking to it through members, everything pretty much works.
The take-home point is this: only use "Is a", only use OOP, when your specific intent is to only (or, at least, most of the time) access the object through the base class interface. If you honestly need to look at an Object B as an Object B in a significant portion of the applicable code, it's probably not a good idea to make Object B an actual Object A.
In short, only use OOP for code that explicitly exhibits polymorphism.
Why? Well, if you don't, what will happen is that you'll create some object hierarchy. And then you're going to want to work around one of those limitations. So, you're going to do something ugly. Whether it's create a fat interface (bubble up virtual methods to the common base class even when they only matter for a derived classes), or other coding pathology.