About Value semantic and Entity semantic

When dealing with Object-oriented programming, whatever the langage we use, we generally face to two kind of objects which are conceptually different: objects which represent a Value, and objects which represent an Entity.

Value semantic

An object has a Value semantic if object is refered by its value (<=> value of its members). 2 instances of an object having the same values can be used indistincly.

For ex. all small mathematics objects like Vector, Matrix, Color, Point but also primitive types like int, float, double, … have a Value semantic.

Value semantic objects are more likely to be manipulated by value or by copy. So, its definition should respect the Rule of Tree/Five

Entity semantic

Object having Entity semantic are identified by their instance. In this case, we care more about the object itself than its actual value. As a consequence, these object might be references in several places in the code.

For ex. high level object like nodes in a scene, widgets in a GUI, character in a game

Entity semantic objects are more likely to be manipulated by references or pointer. It is a good idea to forbid copy class definition.

And so what ?

Although there is no need to explicitly describe your intention in the class documentation, its better to stay consistent for the guys who will follow you. In 3D world, Matrix are typical Value semantic objects. Manipulating Matrices by references might:

  • Create implicit state machine.
    Developers modifying a matrix in some place really don’t expect this value to be change in some totally unrelated location in code
  • Create potential memory leak or memory access violation
    Developers expect these objects to be stored by values, without potential circular reference or lifetime issues.

On the other hand, manipulating Entities by values might:

  • Break the Object Oriented approach:
    Mutating the object will only affect its local copy, whereas developer will expect its modification to affect all places where the object is used.
  • Generate memory consumption issues
    Instead of having one instance in memory, you will have lots of copies
  • May create memory violation issues
    Developers may create a pointer on a local copy thinking the object lifetime is much longer that the actual scope.

And concretely ?

Imagine we have a game and a class representing the player with its life. We have a GameEngine giving damage to the player and a GameRenderer taking care of rendering. We will now consider the player as a Value object and implement the must buggy game ever:

#include <iostream>

struct Player {
  int m_life = 100;
};

class GameEngine {
public:
  GameEngine(Player player) : m_player(player) {}
  void damagePlayer() { m_player.m_life--; }
  Player m_player;
};

class GameRenderer {
public:
  GameRenderer(Player player): m_player(player) {}
  void renderPlayer() { std::cout << m_player.m_life << std::endl; }
  Player m_player;
};

int main() {
  Player player;
  GameEngine engine(player);
  GameRenderer renderer(player);

  engine.damagePlayer();
  renderer.renderPlayer();

  return 0;
}

In this case, Player class has a semantic of Entity. There should be only one instance of Player and it should be referenced in class GameEngine and GameRenderer using smart pointers:

#include <memory>
#include <iostream>

struct Player {
  int m_life = 100;
};

class GameEngine {
public:
  GameEngine(std::shared_ptr<Player> player) : m_player(player) {}
  void damagePlayer() { m_player->m_life--; }
  std::shared_ptr<Player> m_player;
};

class GameRenderer {
public:
  GameRenderer(std::shared_ptr<Player> player): m_player(player) {}
  void renderPlayer() { std::cout << m_player->m_life << std::endl; }
  std::shared_ptr<Player> m_player;
};

int main() {
  std::shared_ptr<Player> player = std::make_shared<Player>();
  GameEngine engine(player);
  GameRenderer renderer(player);

  engine.damagePlayer();
  renderer.renderPlayer();

  return 0;
}

Let’s take another example where we will manipulate Value semantic objects with references:

#include <iostream>

class Point {
public:
  Point(int x, int y) : x(x), y(y) {}
  int x;
  int y;
};
 
class Rectangle {
public:
  Rectangle(const Point& lowerLeft, const Point& upperRight)
    : m_lowerLeft(lowerLeft)
    , m_upperRight(upperRight)
  {}
  const Point& m_lowerLeft;
  const Point& m_upperRight;
};
 
int main() {
  Point a(0, 0);
  Point b(5, 5);
 
  Rectangle rec1(a, b);
  a.x = 2;
  b.x = 10;
  Rectangle rec2(a, b);
 
  return 0;
}

In the above example, a developer working in the main() function wouldn’t expect lines 25-26 to have a side effect on rec1 position. The problem here comes from lines 16-17, where we store points as reference instead of value.

Home


Similar topics

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s