Skip to content

Commit 53af80d

Browse files
committed
Make behaviour of reference counting of games consistent.
The behaviour of reference counting of games has to date not been well-defined. An object or variable holding a reference to an individual game element (player, outcome, etc.) was not enough to extend the lifetime of a game object if the last user-held reference to the game went out of scope (in either C++ or Python). In contrast, a collection object (e.g. the set of players) did hold a shared reference to the underlying game and therefore extended the lifetime of the game. * Standardise on the semantics that holding a reference to any component element or elements of a game, or another indirect reference to a game via e.g. a profile, extends the lifespan of the underlying game. (This is the same semantics as was used in the old Gambit Command Language.) * Add a section in the `pygambit` user guide briefly explaining this. Closes #537.
1 parent a72ead3 commit 53af80d

3 files changed

Lines changed: 91 additions & 2 deletions

File tree

doc/pygambit.user.rst

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,46 @@ Games stored in existing Gambit savefiles can be loaded using :meth:`.read_efg`
334334
cd ../../doc
335335
336336
337+
Lifetime of a game object and its elements
338+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
339+
340+
A game is only deallocated when all variables referring to the game either directly
341+
or indirectly have gone out of scope. Indirect references to games include objects such
342+
as :py:class:`~pygambit.gambit.MixedStrategyProfile` or :py:class:`~pygambit.gambit.MixedBehaviorProfile`,
343+
or variables referring to individual elements of a game.
344+
345+
So for example, the following sequence of operations is valid:
346+
347+
.. ipython:: python
348+
:suppress:
349+
350+
cd ../contrib/games
351+
352+
353+
.. ipython:: python
354+
355+
g = gbt.read_efg("e02.efg")
356+
p = g.players[0]
357+
print(p)
358+
g = gbt.read_efg("poker.efg")
359+
print(p)
360+
print(g)
361+
362+
.. ipython:: python
363+
:suppress:
364+
365+
cd ../../doc
366+
367+
The variable `p` refers to a player in the game read from ``e02.efg``.
368+
So, when ``poker.efg`` is read and assigned to the variable `g`, the game from
369+
``e02.efg`` is still referred to indirectly via `p`. The game object from the
370+
first game can therefore still be obtained from the object referring to the
371+
player:
372+
373+
.. ipython:: python
374+
375+
print(p.game)
376+
337377
338378
Computing Nash equilibria
339379
~~~~~~~~~~~~~~~~~~~~~~~~~

src/games/game.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ class GameActionRep : public std::enable_shared_from_this<GameActionRep> {
175175
void Invalidate() { m_valid = false; }
176176

177177
int GetNumber() const { return m_number; }
178+
Game GetGame() const;
178179
GameInfoset GetInfoset() const;
179180

180181
const std::string &GetLabel() const { return m_label; }
@@ -305,6 +306,8 @@ class GameStrategyRep : public std::enable_shared_from_this<GameStrategyRep> {
305306
/// Sets the text label associated with the strategy
306307
void SetLabel(const std::string &p_label) { m_label = p_label; }
307308

309+
/// Returns the game on which the strategy is defined
310+
Game GetGame() const;
308311
/// Returns the player for whom this is a strategy
309312
GamePlayer GetPlayer() const;
310313
/// Returns the index of the strategy for its player
@@ -940,6 +943,9 @@ inline void GameOutcomeRep::SetPayoff(const GamePlayer &p_player, const Number &
940943
}
941944

942945
inline GamePlayer GameStrategyRep::GetPlayer() const { return m_player->shared_from_this(); }
946+
inline Game GameStrategyRep::GetGame() const { return m_player->GetGame(); }
947+
948+
inline Game GameActionRep::GetGame() const { return m_infoset->GetGame(); }
943949

944950
inline Game GameInfosetRep::GetGame() const { return m_game->shared_from_this(); }
945951
inline GamePlayer GameInfosetRep::GetPlayer() const { return m_player->shared_from_this(); }

src/games/gameobject.h

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727

2828
namespace Gambit {
2929

30+
class GameRep;
31+
using Game = std::shared_ptr<GameRep>;
32+
3033
/// An exception thrown when attempting to dereference an invalidated object
3134
class InvalidObjectException : public Exception {
3235
public:
@@ -35,18 +38,40 @@ class InvalidObjectException : public Exception {
3538
const char *what() const noexcept override { return "Dereferencing an invalidated object"; }
3639
};
3740

41+
/// A handle object for referring to elements of a game
42+
///
43+
/// This class provides the facilities for safely referencing the objects representing
44+
/// elements of a game (players, outcomes, nodes, and so on).
45+
///
46+
/// This addresses two issues:
47+
/// * Holding a reference to some element of a game counts as holding a reference to the
48+
/// whole game. This encapsulates such reference counting such that a game is only
49+
/// deallocated when all references to any part of it are deallocated.
50+
/// * Because games are mutable, the element of the game referred to by this class
51+
/// may be removed from the game. When an element is removed from the game, it is
52+
/// marked as no longer valid. This class automates the checking of validity when
53+
/// dereferencing the object, and raising an exception when appropriate.
3854
template <class T> class GameObjectPtr {
3955
std::shared_ptr<T> m_rep;
56+
Game m_game;
4057

4158
public:
4259
GameObjectPtr() = default;
43-
GameObjectPtr(std::nullptr_t r) : m_rep(r) {}
44-
GameObjectPtr(std::shared_ptr<T> r) : m_rep(r) {}
60+
GameObjectPtr(std::nullptr_t r) : m_rep(r), m_game(nullptr) {}
61+
GameObjectPtr(std::shared_ptr<T> r) : m_rep(r), m_game((r) ? r->GetGame() : nullptr) {}
4562
GameObjectPtr(const GameObjectPtr<T> &) = default;
4663
~GameObjectPtr() = default;
4764

4865
GameObjectPtr<T> &operator=(const GameObjectPtr<T> &) = default;
4966

67+
/// Access the shared pointer to the object representing the game element
68+
///
69+
/// Returns the shared pointer to the game element. Checks for the validity of the
70+
/// game element held by this object and throws an exception if the object is the
71+
/// null object, or is no longer valid (has been removed from the game)
72+
///
73+
/// @exception NullException if the object holds a reference to a null element
74+
/// @exception InvalidObjectException if the element referred to has been deleted from its game
5075
std::shared_ptr<T> operator->() const
5176
{
5277
if (!m_rep) {
@@ -57,6 +82,15 @@ template <class T> class GameObjectPtr {
5782
}
5883
return m_rep;
5984
}
85+
86+
/// Access the shared pointer to the object representing the game element
87+
///
88+
/// Returns the shared pointer to the game element. Checks for the validity of the
89+
/// game element held by this object and throws an exception if the object is the
90+
/// null object, or is no longer valid (has been removed from the game)
91+
///
92+
/// @exception NullException if the object holds a reference to a null element
93+
/// @exception InvalidObjectException if the element referred to has been deleted from its game
6094
std::shared_ptr<T> get_shared() const
6195
{
6296
if (!m_rep) {
@@ -67,6 +101,15 @@ template <class T> class GameObjectPtr {
67101
}
68102
return m_rep;
69103
}
104+
105+
/// Access the raw pointer to the object representing the game element
106+
///
107+
/// Returns the raw pointer to the game element. Checks for the validity of the
108+
/// game element held by this object and throws an exception if the object is the
109+
/// null object, or is no longer valid (has been removed from the game)
110+
///
111+
/// @exception NullException if the object holds a reference to a null element
112+
/// @exception InvalidObjectException if the element referred to has been deleted from its game
70113
T *get() const
71114
{
72115
if (!m_rep) {

0 commit comments

Comments
 (0)