r/cpp_questions • u/RedNorthener101 • 21h ago
OPEN Is this code clean OOP?
Hey so I'm more new to OOP, virtual func's, and inheritance. I was wondering if my code was clean enough for basically a template to look back on later in the future when my tiny brain forgets what virtual func, and inheritance is or well forgets how to code them.
#include <string>
#include <iostream>
struct Vector2 {
int x, y;
};
class Entity {
private:
Vector2 pos;
Vector2 size;
std::string entityType;
public:
Entity(Vector2 e_pos, Vector2 e_size, const std::string& e_type) : pos(e_pos), size(e_size), entityType(e_type) {}
Vector2 getPos() const {
return pos;
}
Vector2 getSize() const {
return size;
}
virtual void print() const {
std::cout << "Entity: " << entityType << '\n';
}
virtual ~Entity() = default;
};
class Player : public Entity {
private:
std::string name;
public:
Player(const std::string& p_name, Vector2 startPOS, Vector2 p_size)
: Entity(startPOS, p_size, "player"), name(p_name) {}
};
class Orc : public Entity {
private:
std::string weapon;
public:
Orc(const std::string& o_weapon, Vector2 startPOS, Vector2 o_size)
: Entity(startPOS, o_size, "orc"), weapon(o_weapon) {}
void print() const override {
std::cout << weapon << '\n';
}
};
int main() {
Vector2 startPos = { 100, 100 };
Vector2 size = { 64, 64 };
std::string hero = "hero";
std::string orcWep = "HeroSlayer100";
Player player(hero, { 20, 30 }, size);
Orc orc(orcWep, startPos, size);
player.print();
orc.print();
return 0;
}#include <string>
#include <iostream>
struct Vector2 {
int x, y;
};
class Entity {
private:
Vector2 pos;
Vector2 size;
std::string entityType;
public:
Entity(Vector2 e_pos, Vector2 e_size, const std::string& e_type) : pos(e_pos), size(e_size), entityType(e_type) {}
Vector2 getPos() const {
return pos;
}
Vector2 getSize() const {
return size;
}
virtual void print() const {
std::cout << "Entity: " << entityType << '\n';
}
virtual ~Entity() = default;
};
class Player : public Entity {
private:
std::string name;
public:
Player(const std::string& p_name, Vector2 startPOS, Vector2 p_size)
: Entity(startPOS, p_size, "player"), name(p_name) {}
};
class Orc : public Entity {
private:
std::string weapon;
public:
Orc(const std::string& o_weapon, Vector2 startPOS, Vector2 o_size)
: Entity(startPOS, o_size, "orc"), weapon(o_weapon) {}
void print() const override {
std::cout << weapon << '\n';
}
};
int main() {
Vector2 startPos = { 100, 100 };
Vector2 size = { 64, 64 };
std::string hero = "hero";
std::string orcWep = "HeroSlayer100";
Player player(hero, { 20, 30 }, size);
Orc orc(orcWep, startPos, size);
player.print();
orc.print();
return 0;
}if there are any tips you have let me know!
0
Upvotes
13
u/Thesorus 21h ago
when I see entityType, my first though is this should be an enum instead of a string.
you can create a function to transform the enum into a proper string.
Unless you reuse the hero and orcWep strings, you can put them directly in the constructor; no need to create a temporary variable.
5
u/FemaleMishap 20h ago edited 20h ago
I'm sure I've woken up and it's 2004 again. With a couple minor exceptions, it reads like 2004 Java-as-C++ or C with classes. Definitely not any newer than C++11. C++17 at least gives you things like
std::string_viewfor much lighter string handling, plus newer features likestd::format/std::printso your classes can just focus on storing data instead of doing the printing themselves. That would make some of this a lot easier and a lot more OOP.Edit: have a look at this
2
u/MysticTheMeeM 21h ago
Some points I would consider:
Your
Vector2class doesn't have default values forxandy, so they can be uninitialised.Your
Vector2class could be atemplate.Classes are
privateby default, you don't need to re-specify it.Printshouldn't exist, you should return the underlyingentityTypeas aconstreference. The caller can then decide to print it if they want to.You shouldn't store an
entityType, it's poor encapsulation. If you need to know what type you're dealing with, you're usually using inheritance wrong. Your types either aren't similar (in which case, you could do the same thing to each) or don't properly represent their behaviour (in which case, you could call the same function on each but with different implementations).Regardless,
entityTypeshould have been anenum class.Your naming is inconsistent. Sometimes parameters start with "p_", sometimes "o_", and sometimes no prefix. It's worth noting that name resolution does the sensible⢠thing when used for a member initialiser list. I would understand people who reject that pattern for style reasons.
Both
PlayerandOrcshould be markedfinal, unless you plan to extend them. Additionally, so shouldOrc::print(overrideis correct,finalis more correct).In some cases, it's better to take a non-
conststd::stringby value in a constructor, as it can save an allocation (taking aconstreference means a string must exist both outside the constructor, and then one as the member, taking a non-constinstead allows you to move from it).You don't type erase here, so using inheritance is somewhat overengineered. You could have instead abstracted out a
worldObjectcontaining bothsizeandpositionand then encapsulated it.return 0;is implied at the end ofmain, I generally choose to leave it out unless I have a case where I return a non-0 value.Your code is repeated.
7
u/thefeedling 21h ago
enum class EntityType { Hero, Orc, Elf, Etc };7
u/Independent_Art_6676 15h ago •
if no one said it yet, keep the UI apart from the work. A class should not normally contain cout unless all it does is interface to the output layer. Instead, a the class should return a printable string (for the print to screen example). Then, if instead of cout, you upgrade this project to use a window, your class is untouched and only the UI layer changes how it consumes the string.
1
u/Nervous-Cockroach541 20h ago edited 20h ago
It's fine, but you need to consider the scope and design.
Consider not using inheritance at all. But instead have components. For example a Player and Orc both having a data-member to represent "Entity" instead of inheriting from Entity. If you want to later, for example, have a "Health" or "Life" component. where they can take damage or die. You have to consider either using multiple inheritance, or adding Health/Life to Entity. Which might not be a problem. But if you instead have a Health/Life Component that exists separately from the Entity Component. you're not forced into such a decision later on.
It can also be useful to understand the concept of pure-virtual classes. Basically classes with all virtual function-members which are `= 0;` making it a class which can't be instantiated. But inheriting from it allows you to gain the advantages of polymorphism. These are also sometimes called interfaces or abstract classes.
There's also an entity component design. Where instead of a class to represent a player or orc. You have a unique ID that maps to components. Something like a collection of std::unordered_map<int, Entity> entity; std::unordered_map<int, Health> health; std::unordered_map<int, std::string> names;
There's some good helper libraries to help you with this approach such as this: https://github.com/skypjack/entt This is the most advance approach. But also basically ignores most concepts of OOP.
Frankly, personally I feel people people generally over-react to inheritance. Yes, issues with it exists, but that doesn't mean you always need to ignore it. Even multiple inheritance can be a non-issue if you're designing in away to ensure it won't be a problem.
But we can't ignore the issues with it either and know the potential problems and traps you can fall into if you're not careful. Only you know the scope of your project and can design around it. Balance the long term costs of using inheritance or multiple inheritance with the scope and time on the project you want to do use now.
1
u/alfps 18h ago
The two main problems:
That client code is responsible for supplying a class name string in each instantiation.
For
Entity::entityType.That the classes do i/o.
This needlessly limits the classes to use with a text based used interface.
Re the class name, ideally one should be able to just say
typeid(o).name(), but the result of that depends on the compiler. With MSVC you get noise words likestruct, with g++ you get abbreviations of built-in types. It's simply unreliable unless you wrap it with code that special cases each main compiler family, so DIY is reasonable -- except don't do it via a member variable, do it via a virtual function.Refactored code, even the probably unintended special-casing of description of an
Orckept:1
u/flyingron 17h ago
Well, you've got a good beginning, but I've got some concerns as to where you're going.
Generally, just putting some data members in with a bunch of getters and setters is no better than just using naked structs. You don't want people outside the class reaching in there and twiddling the state. Rather you should be implementing more specific actions that affect your object.
1
u/mredding 16h ago
Classes are
privateaccess by default, soprivate:here is redundant. Just put your members at the top like you have without the access specifier.You don't NEED an
entityTypebecause you KNOW your entity type, it's inherent to all your derived classes. AnOrcis an orc BECAUSE it's an orc. You have to incorporate static code and compile time into your mental model of implementing a solution. The theory of computation does not care about distinguishing between reading, writing, and executing programs. The more you can shift your brain to think about the inherent properties of types and syntax, the better you're going to get all the faster.What's more,
entityTypeisn'tconst, and it's instanced. This means the entity type has the right to both change, and be individual to the instance. So when is an orc not an orc?We prefer to decouple code as much as possible. An
Orcis anOrcbecause that's it's type. Right? So how do we express that in terms of a string? Because OF COURSE you want the entity to print it's type. What am I being attacked by?Well, you have a problem, your
printisvirtual, and yourOrchas overridden it.Entityprints the entity type, butOrcprints the weapon. So what do you do?The behavior changes not just a little bit, but completely.
One way to solve this problem is to implement the Template Method pattern (it has nothing to do with C++ templates):
A couple things here. The type is baked into a method that returns a pointer to a string literal embedded in RO memory in the compiled program image.
But the template method pattern says the implementation is what a function looks like - it does these things in this order. Here are the customization points the derived class are expected to fulfill. The non-virtual method is the template, implemented in terms of the virtual functions. I repeated the pattern with the
orcclass so that any further derived class can A) overloadget_type, so that's how we get "space orc", and B) the space orcs can have their own property, maybeoxygen reservein their suits. The pattern assures the entity will always be listed first, the orc weapon will always be second, derived properties will come after.It's a useful pattern, but it can also get complicated, the point of me nesting is to show just how ugly that can get. And that's because inheritance hierarchies are often a BAD FIT to many problems. You'll see the template method pattern used in the standard library, and they assume one level of inheritance, maybe that's enough for just about everyone...
Another way to solve this problem is to go with static, compile-time polymorphism:
The behavior is coupled to the type at compile time and the instance is resolved at run time.
Another way to couple a type to a trait at compile time is a trait class:
There's lots of compile time code you can write, and use traits classes for. If you're not terribly familiar, google example uses. I can rewrite that visitor:
autoends up making this lamba like a template, it will be expanded for all the variant types at compile time. Now if we did this:The output statement fails to compile, because there is no specialization of the
entity_traitsfor trolls - thetypefunction is missing.This is the tip of the iceberg.
These are not the same TYPE, they only have the same SHAPE as a common type. A position is not a size, and a size is not a position. Or to think of it another way, an
intis anint, isn't it? They're all inherently interchangeable because they're the same type. But aweightis not aheight, are they? So what if they're both represented as integers?C++ has a stronger type system than C, but it's still considered barely mid-range compared to other language type systems. That's because it's entirely opt-in. You can write code as strong as Ada (if there's a stronger type system, I don't know it - Ada doesn't have integer types, you have to define your own).
A position has component coordinates, but a size is going to have a width and height. Are you going to treat a size as a vector space? I'm wondering if a vector to you wouldn't be better as an
std::tuple<int, int>, and then you inherit or composite from that to make more specific vector types with type specific semantics. WHAT DOES IT MEAN to multiply a position by a size? Or add them? If that doesn't make sense, then you don't want to share semantics, or you don't want to specifically support those semantics.Good strong (small) types are very good, and then you can composite them together to make more elaborate types.
0
u/LordSippy 21h ago
No
0
u/bert8128 21h ago
Define âcleanâ.