Introduction
C++ is well-known for efficiency. On the other hand, there are some traps which annoy developers. As you know,
wild pointers and memory leaks are problems which happen to us most.
Those problems make some developers desperate sometimes and makes using C++
difficult. Actually there are some methods to avoid those problems. Here, I'd like to share my experience
dealing with those problems.
Smart Pointers
Most C++ developers know about smart pointers. We can use them with boost or
C++ 11. There are lots of articles that introduce smart pointers but less of those clarify how to use
them right.
Unfortunately there are some opinions to be ignored when using smart pointers. To some extent, that's right. Using smart pointers improperly may bring bad results,
even memory leaks. So let's talk about the rules of using smart pointers.
Generally there are three kinds of smart pointers:
- Scope Pointer: Scope pointer will auto release the object when the scope pointer exits
a code statement. This is a so easy, one that we nearly will not make mistakes.
So we will talk more about it.
- Shared pointer. Object will not be released until a non-shared pointer has the reference. This will be very useful to avoid wild pointers.
- Weak pointer. This must be used by combining with a shared pointer. Weak Pointer has a reference to
a shared pointer. Weak pointer can be converted to a shared pointer.
But if a shared pointer has released the object, the weak pointer will auto release the reference.
Mgr class to use shared pointer
We like to use a class like xx_mgr
to manage objects. There are some codes
using the game server model as an example:
class palyer_t
{
public:
int id();
};
typedef shared_ptr<palyer_t> palyer_ptr_t;
class player_mgr_t
{
public:
int add(palyer_ptr_t player_)
{
m_players[player_->id()] = player_;
return 0;
}
palyer_ptr_t get(int id_)
{
map<int, palyer_ptr_t>::iterator it = m_players.find(id_);
if (it != m_players.end())
{
return it->second;
}
return NULL;
}
int del(int id_)
{
m_players,erase(id_);
return 0;
}
protected:
map<int, palyer_ptr_t> m_players;
};
player_mgr_t
controls the life cycle of player_t
. We must make sure only
player_mgr_t
owns the player_t
shared pointer. We can use
a temporary variable of the player_t
shared pointer which will be auto released when
the function exits. So when the player gets offline, we delete it from player_mgr_t
so that this player_t
object will be auto released. That's what we expect.
Recourse object to use shared pointer
We are all familiar with the situation that we new
an object or buffer to put contents read from
a file or database. I recently solved a memory leak in my project that was caused by
a MySQL query. The MySQL API of mysql_fetch_result
returns an object that needs to free even though
a zero row is included. For example:
struct db_data_t{
strint data;
};
typedef shared_ptr<db_data_t> db_data_ptr_t;
db_data_ptr_t load_data(const string& sql_){
return new db_data_t();
}
int process_1(db_data_ptr_t data_);
int process_2(db_data_ptr_t data_);
int process_3(db_data_ptr_t data_);
int process(db_data_ptr_t data_){
db_data_ptr_t data = load_data(sql);
if (process_1(data)){
return -1;
}
if (process_2(data)){
return -1;
}
if (process_3(data)){
return -1;
}
}
As you can see, whenever we quit the process, the object of db_data
will be deconstructed. That makes sure
db_data_t
will not cause a memory leak.
Property object to use shared pointer
This situation happens when two objects have the owner relationship. E.g.,
player_t
may have a weapon, or may not. Once we allocate a weapon object to
player_t
,
the weapon will not be destroyed until player_t
is destroyed. Let's see
an example code:
class weapon_t;
typedef shared_ptr<weapon_t> weapon_ptr_t;
class palyer_t
{
public:
int id();
weapon_ptr_t get_weapon() { return m_weapon; }
void set_weapon(weapon_ptr_t weapon_) { m_weapon = weapon_; }
protected:
weapon_ptr_t m_weapon;
};
Reference relationship to use weak pointer
I use weak pointers besides the situations mentioned above. Using a game server model
is an example. Monster will auto attack players trying to approach it. So class
monster_t
has an interface lock_target()
which will
be auto invoked by the AI system. We should set the target player of the monster to
null
if the target player gets offline. That is annoying. That is where we regularly make mistakes. And weak pointer fits here very much.
class monster_t{
public:
void set_target(shared_ptr<player_t> player_){
m_target_player = player_;
}
shared_ptr<player_t> get_target() { return m_target_player.lock(); }
protected:
weak_ptr<player_t> m_target_player;
};
m_target_player
will be auto set to null
if the real player gets offline. Do you
remember the example of the mgr
object? If we combine them together, it's easy to manage memory allocation.
Object Counter
Though we use smart pointers, we likely make mistakes that make
program memory increase all the way. For example, we forget to delete player_t
from
player_mgr_t
when the player gets offline in some situations; that will not generate
a memory leak. Another example, our program will sometimes increase memory faster than at other times. But which object causes these memory
allocations? That usually can't be tested in the developing environment. So we have a need
to know object numbers anytime. Generally we need to dump the data of object numbers to
a file as time goes on. Here is a simple implementation:
#include <stdio.h>
#include "base/fftype.h"
using namespace ff;
class foo_t: public ffobject_count_t<foo_t>
{
};
class dumy_t: public foo_t, ffobject_count_t<dumy_t>
{
};
int main(int argc, char* argv[])
{
foo_t foo;
dumy_t dumy;
map<string, long> data = singleton_t<obj_summary_t>::instance().get_all_obj_num();
printf("foo_t=%ld, dumy_t=%ld\n", data["foo_t"], data["dumy_t"]);
return 0;
}
output:
foo_t=2, dumy_t=1
I will upload the files of the implementation code. I just posted some key codes. First,
ffobject_count_t
will auto increase the number of objects during construction, and will auto decrease
the number during destruction.
class obj_counter_i
<pre>{
public:
obj_counter_i():m_ref_count(0){}
virtual ~ obj_counter_i(){}
void inc(int n) { (void)__sync_add_and_fetch(&m_ref_count, n); }
void dec(int n) { __sync_sub_and_fetch(&m_ref_count, n); }
long val() const{ return m_ref_count; }
virtual const string& get_name() { static string ret; return ret; }
protected:
volatile long m_ref_count;
};
template<typename T>
class obj_counter_t: public obj_counter_i
{
public:
obj_counter_t()
{
singleton_t<obj_summary_t>::instance().reg(this);
}
virtual const string& get_name() { return TYPE_NAME(T); }
};
template<typename T>
class ffobject_count_t
{
public:
ffobject_count_t()
{
singleton_t<obj_counter_t<T> >::instance().inc(1);
}
virtual ~ ffobject_count_t()
{
singleton_t<obj_counter_t<T> >::instance().dec(1);
}
};
If we dump the data of object number to a file in a timely fashion, we will get such
a file:
obj,num,20120606-17:01:41
dumy,1111
foo,222
obj,num,20120606-18:01:41
dumy,11311
foo,2422
obj,num,20120606-19:01:41
dumy,41111
foo,24442
It's easy to write a tool to analyze the data. For example, we can use highchart (js lib) to draw lines. I
have uploaded my implementation of the tool. The picture generated is like this:
Summary
- We should be careful with the C++ memory allocation.
- Smart pointers help us deal with memory allocations more easily.
- In my opinion, we should use Smart pointers always but properly.
- We should know when to use a shared pointer and when to use a weak pointer. If you make mistakes, that makes
the situation worse than the original pointer.
- An object counter should be part of the infrastructure of our C++ program. That helps us analyze
memory allocation as time goes.
- More codes available here: Github:
https://github.com/fanchy/RedRabbit/blob/gh-pages/fflib/base/fftype.h