06 Sep, 2007, David Haley wrote in the 41st comment:
Votes: 0
Indeed, many of these questions about memory are empirical.

Justice said:
The TST will vary quite a bit.


Indeed; tries were in fact designed, as I understand it, to take advantage of prefix compression. In fact, there are even neater versions that have prefix and suffix compression, but those are much, much harder to update at run-time than the prefix-only versions.

Justice said:
The hashtable will tend to degrade in lookup performance the closer it is to being full.

I'm not sure how you define 'full' for a hash table, rather I think it's more precise to speak of average bucket size. A hash table's average-case lookup is O(b) where b is the average bucket size. Worst-case is O© if you let c be the largest bucket. Both of those being a function of the number of buckets and the hash function, naturally.

Justice said:
(although there was no noticeable degradation between 100k and 1m strings).

Well, of course:
log_2 100k ~= 20
log_2 1m ~= 23.something
The difference in time of 3 extra hops isn't very big. Unless you compound it over very very very many lookups.

Justice said:
Given that in the event it does occur, there are other options available… what exactly does the abstract parent do for you?

Personal satisfaction. :smile: I'm not saying it's inherently better than the other options. But I would argue that it's not worse, either.

Justice said:
As for the clarity, I don't see what you're talking about. My code has exactly 3 methods. I've pasted both my declaration and yours (stripped of comments). Which is clearer?

Yours is shorter, but I don't really consider either to be clearer. Your solution imposes an implementation, and since it comes first in the file (well this would be true even had it come second) I think that, to some extent, takes away from the clarity of the interface. But I'm not claiming that what I say has objective value. (I would argue that it is objectively clearer to put public members first, though, because those are what users of the class most care about. But, the marginal gain is admittedly minimal and can be solved with good IDEs anyhow…)
08 Sep, 2007, Justice wrote in the 42nd comment:
Votes: 0
DavidHaley said:
Justice said:
The hashtable will tend to degrade in lookup performance the closer it is to being full.

I'm not sure how you define 'full' for a hash table, rather I think it's more precise to speak of average bucket size. A hash table's average-case lookup is O(b) where b is the average bucket size. Worst-case is O© if you let c be the largest bucket. Both of those being a function of the number of buckets and the hash function, naturally.


David, I thought we were discussing the theoretical performance of a hash table, not the limitations of your implementation. A hash table is considered "full" when each of it's buckets have at least 1 value in them. When more than 1 key generates the same hash, they are said to "collide". Most efficient hash tables I'm familiar with have a load factor, that allows the table to rebuild as it fills up. Rehashing of course, being an expensive operation causes these implementations to have inconsistent insertion performance. The java implementation for example, can insert instantly or take some time depending on whether an insert triggers a rehash. Although in my testing it has very good lookup performance, it could take several seconds to populate. (Those tests also used 1million values). If the table was properly sized before populating, it reduced the population time considerably, clearly demonstrating the overhead of rehashing.

Anyway, here's what Wiki has to say on the matter:
Wikipedia
Quote
A good hash function is essential for good hash table performance. A poor choice of a hash function is likely to lead to clustering, in which probability of keys mapping to the same hash bucket (i.e. a collision) is significantly greater than would be expected from a random function. A nonzero collision probability is inevitable in any hash implementation, but usually the number of operations required to resolve a collision scales linearly with the number of keys mapping to the same bucket, so excess collisions will degrade performance significantly.


Quote
With a good hash function, a hash table can typically contain about 70%–80% as many elements as it does table slots and still perform well. Depending on the collision resolution mechanism, performance can begin to suffer either gradually or dramatically as more elements are added.


DavidHaley said:
Your solution imposes an implementation, and since it comes first in the file (well this would be true even had it come second) I think that, to some extent, takes away from the clarity of the interface.


How does it impose an implementation? I'm talking about an informal interface. I'll try to make this simple to avoid any further misunderstandings since you're apparently getting hung up on the fact that I posted an implementation. Basically, I have 3 methods…
const std::string *get_ref(const std::string &str);
unsigned int count_ref(const std::string &str);
unsigned int remove_ref(const std::string &str);


It doesn't matter what the class of the object I define is, if I write code that looks like…
mngr.get_ref(str);


This is a core concept when working with the STL. You might notice that my string_compare object doesn't have a parent. Does this prevent me from using it with the std::map? Not really, it wouldn't prevent me from using it with the existing sort algorithms either. Nor would it prevent me from using it anywhere the standard library needs a comparison. You see, in this case who the parents are doesn't really matter, its what you can do that matters. The "composition" of the object.


Oh yeah, and for those that are interested. I've completed some real world tests on my version and am starting to use it on my actual code. The area files in a stock SmaugFUSS provide it with a little more than 8700 strings. Doing a retrieval test (string exists in table, real strings), it was able to execute a little over 1m times (each string tested 115 times), in .8s, compared to 7.7 seconds for a worst case scenario (long virtually identical strings that don't exist in the table incurring both a lookup and insert).
08 Sep, 2007, David Haley wrote in the 43rd comment:
Votes: 0
Justice said:
David, I thought we were discussing the theoretical performance of a hash table, not the limitations of your implementation.

We are… I'm not sure how what I said in the piece you quote is specific to limitations of my implementation or even my implementation at all. I talked about hash table lookup costs, based on bucket sizes; those notions apply to all hash tables. (Well, ok: you can change the bucket search method to improve using BSTs or whatever. The point is that the average-case time is proportional to average bucket size. O(log n) is still O(n) after all.)

Justice said:
A hash table is considered "full" when each of it's buckets have at least 1 value in them.

I maintain that it makes more sense to speak of average-case lookup time based on average bucket size, keeping in mind that you should also be looking at bucket size disparity.

Justice said:
Although in my testing it has very good lookup performance, it could take several seconds to populate. (Those tests also used 1million values). If the table was properly sized before populating, it reduced the population time considerably, clearly demonstrating the overhead of rehashing.

Well, it also demonstrates the importance of knowing what you're doing ahead of time. If you know you're about to insert 1m values, you should size the table ahead of time. If you don't know how many you'll be adding, and suspect it's a small number, you shouldn't reserve space so as to not waste memory.

Justice said:
How does it impose an implementation?

Well, the class provides an implementation. If I want to subclass, so that I can have two manager classes in parallel, the other one inherits the map it doesn't necessarily use.

It occurs to me that one problem is that you don't seem to care about having an interface specified, whereas I do: you say "just use templates then". I think it's just a "clashing of philosophies". I'm not sure how important this particular thread of debate is, so if you don't mind I'd like to submit a motion to move on to something else. :wink:
10 Sep, 2007, Justice wrote in the 44th comment:
Votes: 0
Here's the latest version of my shared string manager with supporting code. I'll post it as a snippet later with the various supporting functions. It should be noted that as a result of this thread, all the supporting functions support n instances over n implementations. With the exception of ManagedStringP which handles a single instance, but could be setup to handle multiple implementations.

Anyway, after actually using it, I found the interface was flawed. Setting the reference took 1 line, removing a reference took 2 lines, changing a reference took 3. Because managed strings used "const std::string*" instead of "const std::string&", some conversion was required.

The remove_ref method now returns a NULL const std::string*. This allows you to clear the old pointer in the same line you remove the reference (which may delete the string).
The get_ref method now takes the old pointer as a second parameter. This allows it to handle the cleanup for you. Also, get_ref has been overloaded to accept "const std::string *", allowing managed strings to be used without conversion.

Additionally, Samson requested various statistics be kept on how the string manager operates. I implemented StringManagerStats as a mixin to allow for the possibility of multiple interfaces.

Finally, I've implemented 2 types of managed string wrappers. ManagedStringR maintains a reference to it's string manager. It will maintain it's own string if it has no manager, and can move this string into a manager, or move a reference between managers as necessary. ManagedStringP uses a provider object that gives it a reference to it's manager. While limited to a single instance, it allows string management to be completely automatic and doesn't need to maintain a reference to a manager.

So far the StringManager class is being used to maintain strings for obj_data and obj_index_data. obj_data is using raw pointers (const std::string*), while obj_index_data is using ManagedStringP.

util.h
class string_compare
{
public:
string_compare(void);
bool operator()(const std::string *left, const std::string *right);
};


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// String Manager Stats
// Maintain various performance statistics
// – Justice
class StringManagerStats
{
protected:
unsigned int m_total_string;
unsigned int m_total_insert;
unsigned int m_total_delete;
unsigned int m_total_refs;
unsigned int m_total_chars;
unsigned int m_total_saved_chars;

StringManagerStats(void)
{
m_total_string = 0;
m_total_insert = 0;
m_total_delete = 0;
m_total_refs = 0;
m_total_chars = 0;
m_total_saved_chars = 0;
}

inline void update_new_ref(unsigned int len)
{
m_total_refs++;
m_total_string++;
m_total_insert++;
m_total_chars += len;
}

inline void update_add_ref(unsigned int len)
{
m_total_refs++;
m_total_saved_chars += len;
}

inline void update_delete_ref(unsigned int len)
{
m_total_refs–;
m_total_string–;
m_total_delete++;
m_total_chars -= len;
}

inline void update_remove_ref(unsigned int len)
{
m_total_refs–;
m_total_saved_chars -= len;
}

public:
inline unsigned int total_strings(void)
{
return m_total_string;
}

inline unsigned int total_inserted(void)
{
return m_total_insert;
}

inline unsigned int total_deleted(void)
{
return m_total_delete;
}

inline unsigned int total_refs(void)
{
return m_total_refs;
}

inline unsigned int total_chars(void)
{
return m_total_chars;
}

inline unsigned int total_saved_chars(void)
{
return m_total_saved_chars;
}
};

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// String Manager
// Shared string management using a refcount table.
// – Justice
class StringManager : public StringManagerStats
{
protected:
typedef std::map<const std::string*, unsigned int, string_compare>::iterator ITERATOR;
typedef std::pair<const std::string*, unsigned int> PAIR;
std::map<const std::string*, unsigned int, string_compare> content;

public:
StringManager(void);
~StringManager(void);

// Get a string reference.
// str = string to reference
// old = previous reference (remove if exists)
const std::string *get_ref(const std::string &str, const std::string *old);
const std::string *get_ref(const std::string *str, const std::string *old);

// Count a string's references.
// str = string to lookup
unsigned int count_ref(const std::string &str);

// Count a string's references.
// str = string to remove
const std::string *remove_ref(const std::string *str);

// Pass each string/refcount to a supplied object.
// Traits: T
// bool operator()(const std::string*,unsigned int)
// if TRUE then break
template <class T>
void for_each(T value)
{
ITERATOR it, end;

it = content.begin();
end = content.end();

for (; it != end; it++)
if (value(it->first, it->second))
break;
}
};


util.c
bool string_compare::operator()(const std::string *left, const std::string *right)
{
return (left->compare(*right) < 0);
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// String Manager
StringManager::StringManager(void)
{
}

StringManager::~StringManager(void)
{
ITERATOR nxt,it,end;
const std::string *cur;

nxt = content.begin();
end = content.end();

while (nxt != end)
{
it = nxt++;
cur = it->first;

content.erase(it);
delete cur;
}
}

unsigned int StringManager::count_ref(const std::string &str)
{
ITERATOR fnd;

if ((fnd = content.find(&str)) == content.end())
return 0;
return fnd->second;
}

const std::string *StringManager::get_ref(const std::string &str, const std::string *old)
{
ITERATOR fnd;
PAIR tmp;

// Clear Reference
if (old)
remove_ref(old);

// Insert
if ((fnd = content.find(&str)) == content.end())
{
update_new_ref(str.length());

tmp.first = new std::string(str);
tmp.second = 1;
content.insert(tmp);
return tmp.first;
}

update_add_ref(str.length());

// Update
fnd->second++;
return fnd->first;
}

const std::string *StringManager::get_ref(const std::string *str, const std::string *old)
{
ITERATOR fnd;
PAIR tmp;

// Clear Reference
if (old)
remove_ref(old);

// Cannot insert null
if (!str)
return NULL;

// Insert
if ((fnd = content.find(str)) == content.end())
{
update_new_ref(str->length());

tmp.first = new std::string(*str);
tmp.second = 1;
content.insert(tmp);
return tmp.first;
}

update_add_ref(str->length());

// Update
fnd->second++;
return fnd->first;
}

const std::string *StringManager::remove_ref(const std::string *str)
{
ITERATOR fnd;
const std::string *tmp;

if (!str)
return NULL;

// Not Found
if ((fnd = content.find(str)) == content.end())
return NULL;

// Delete
if (–fnd->second == 0)
{
update_delete_ref(str->length());

tmp = fnd->first;
content.erase(fnd);
delete tmp;
return NULL;
}

// Remove
update_remove_ref(str->length());
return NULL;
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
// Managed String Wrappers
// Simplify string management tasks.

// Reference Managed String
// Maintains a reference to it's string manager.
// If no table has been supplied, maintain it's own string.
// Support moving between string managers.
//
// Traits: MANAGER
// const std::string *get_ref(const std::string &str, const std::string *old);
// const std::string *get_ref(const std::string *str, const std::string *old);
// const std::string *remove_ref(const std::string *str);
// – Justice
template <class MANAGER>
class ManagedStringR
{
protected:
MANAGER *table;
const std::string *data;
public:
ManagedStringR(void)
{
table = NULL;
data = NULL;
}

~ManagedStringR(void)
{
if (table)
{
if (data)
data = table->remove_ref(data);
table = NULL;
}

if (data)
delete data;
}

// String Access
inline const std::string &operator()(void)
{
return *data;
}

inline const std::string *value(void)
{
return data;
}

const char *c_str(void)
{
return data->c_str();
}

std::string::size_type size(void)
{
if (!data)
return 0;
return data->size();
}

// Set the manager.
void operator<<(MANAGER *tbl)
{
std::string buf;

if (data)
{
// Remember value
buf = *data;

// Maintain previous value
if (table)
data = table->remove_ref(data);
else
delete data;

// Update table
table = tbl;

// Update pointer
if (table)
data = table->get_ref(buf, data);
else
data = new std::string(buf);
return;
}

// Update table
table = tbl;
}

// Change value
void operator=(ManagedStringR<MANAGER> &str)
{
// No Table? Manual string handling.
if (!table)
{
if (data)
delete data;

data = new std::string(str());
return;
}

// Table string handling.
data = table->get_ref(str.value(), data);
}

// Change value
void operator=(const std::string &str)
{
// No Table? Manual string handling.
if (!table)
{
if (data)
delete data;

data = new std::string(str);
return;
}

// Table string handling.
data = table->get_ref(str, data);
}

// Change value
void operator=(const std::string *str)
{
// No Table? Manual string handling.
if (!table)
{
if (data)
delete data;

if (str)
data = new std::string(*str);
else
data = NULL;
return;
}

// Table managed strings
if (str)
data = table->get_ref(str, data);
// Null means clear reference
else
data = table->remove_ref(data);
}
};

// Provider Managed String
// Uses a provider to reference it's string manager.
// Assume table is never null.
//
// Traits: MANAGER
// const std::string *get_ref(const std::string &str, const std::string *old);
// const std::string *get_ref(const std::string *str, const std::string *old);
// const std::string *remove_ref(const std::string *str);
//
// Traits: PROVIDER
// MANAGER *manager(void);
// – Justice
template <class MANAGER, class PROVIDER>
class ManagedStringP
{
protected:
const std::string *data;
public:
ManagedStringP(void)
{
data = NULL;
}

~ManagedStringP(void)
{
PROVIDER p;
MANAGER *table;
table = p.manager();

if (data)
data = table->remove_ref(data);
}

// String Access
inline const std::string &operator()(void)
{
return *data;
}

inline const std::string *value(void)
{
return data;
}

const char *c_str(void)
{
return data->c_str();
}

std::string::size_type size(void)
{
if (!data)
return 0;
return data->size();
}

// Change value
void operator=(ManagedStringP<MANAGER,PROVIDER> &str)
{
PROVIDER p;
MANAGER *table;
table = p.manager();

// Table string handling.
data = table->get_ref(str.value(), data);
}

// Change value
void operator=(const std::string &str)
{
PROVIDER p;
MANAGER *table;
table = p.manager();

// Table string handling.
data = table->get_ref(str, data);
}

// Change value
void operator=(const std::string *str)
{
PROVIDER p;
MANAGER *table;
table = p.manager();

// Table managed strings
if (str)
data = table->get_ref(str, data);
// Null means clear reference
else
data = table->remove_ref(data);
}
};

Oh yeah, one final note, I've overloaded many of the string utilities to accept ManagedStringP and const std::string* as parameters. This is make the code cleaner by avoiding conversions. I'll post those later.
10 Sep, 2007, David Haley wrote in the 45th comment:
Votes: 0
Justice said:
Anyway, after actually using it, I found the interface was flawed.

Improving the interface is always good, but I'm curious about something. It seems that you expect the user to talk to the manager directly, and not the managed string classes. Is that correct? If so, isn't the whole point of managed strings that you don't ever have to worry about managing them with the manager, and that the string just takes care of itself?
I see that you have the two kinds of managed string classes; but you were talking about clearing strings as you delete a reference from the manager as if it was something you were doing a lot.

Justice said:
Additionally, Samson requested various statistics be kept on how the string manager operates. I implemented StringManagerStats as a mixin to allow for the possibility of multiple interfaces.

Why do you have the manager subclass the stats class? It seems to me that the manager owns/contains the stats, as opposed to being a "stats" object.


One other thing: please forgive the grammar nazi. But the possessive form is "its", not "it's". :evil: (Sorry, that's been nagging me for a while. Yeah, yeah, grammar nazi, I know.)
10 Sep, 2007, Justice wrote in the 46th comment:
Votes: 0
DavidHaley said:
Improving the interface is always good, but I'm curious about something. It seems that you expect the user to talk to the manager directly, and not the managed string classes. Is that correct? If so, isn't the whole point of managed strings that you don't ever have to worry about managing them with the manager, and that the string just takes care of itself?


It doesn't matter what I expect. The StringManager should have an interface that does it's job cleanly, correct? It stands to reason that when a pointer is managed, you will want to remove the old reference when you retrieve a new one. It also stands to reason that when you remove a reference you will want to clear the old pointer.

The use of a managed string wrapper is to allow string management to occur automatically. This merely offsets where management occurs. In fact, these also benefit from the improved interface making their code simpler and easier to work with.

Although I had started planning them much sooner, I waited until after I solidified the interface for StringManager by implementing it in the mud (for both object and their indexes). Currently, OBJ_INDEX_DATA is using the managed string wrappers, while OBJ_DATA uses direct pointers.

DavidHaley said:
I see that you have the two kinds of managed string classes; but you were talking about clearing strings as you delete a reference from the manager as if it was something you were doing a lot.


In practice? Yes, a large amount of code is spent working with the reference. When an area file is loaded, each string must be referenced. When any string is changed (in olc, mpxset, or various loading routines), each string must be referenced/changed. When an object is loaded in the game, each string must be referenced. When objects are disposed, each string must be dereferenced. It was something that happened regularly.

DavidHaley said:
Why do you have the manager subclass the stats class? It seems to me that the manager owns/contains the stats, as opposed to being a "stats" object.

There are a couple reasons why this is separated.

First… what exactly do statistics have to do with referencing and dereferencing a string? Absolutely nothing. These statistics merely display the usage characteristics of the application, and aren't even useful for comparing implementations against each other. Basically, these statistics estimate the overhead of the strings stored in the table. If you get excessive inserts/deletes you can look into the life cycle and possibly come up with a system to reduce them.

Second is… The statistics are part of a very narrowly scoped class. It is not dependent on any implementation, allowing it to be reused if it becomes necessary to have a second implementation.
11 Sep, 2007, Justice wrote in the 47th comment:
Votes: 0
I've come up with a few updates for the managed strings. Basically, added some proxy functions to emulate std::string better, some conversion operators, and a few null checks. The new conversion operators allow the managed strings to play nicely with const char* and std::string.

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
// Managed String Wrappers
// Simplify string management tasks.

// Reference Managed String
// Maintains a reference to it's string manager.
// If no table has been supplied, maintain it's own string.
// Support moving between string managers.
//
// Traits: MANAGER
// const std::string *get_ref(const std::string &str, const std::string *old);
// const std::string *get_ref(const std::string *str, const std::string *old);
// const std::string *remove_ref(const std::string *str);
// – Justice
template <class MANAGER>
class ManagedStringR
{
protected:
MANAGER *table;
const std::string *data;
public:
ManagedStringR(void)
{
table = NULL;
data = NULL;
}

~ManagedStringR(void)
{
if (table)
{
if (data)
data = table->remove_ref(data);
table = NULL;
}

if (data)
delete data;
}

// Conversions
operator const std::string*() {return data;}
operator const char*() {return c_str();}

// String Access
inline const std::string &operator()(void)
{
static std::string BLANK = "";
if (!data)
return BLANK;
return *data;
}

inline const std::string *value(void)
{
return data;
}

// is empty?
bool empty(void)
{
if (!data)
return TRUE;
return data->empty();
}

const char *c_str(void)
{
if (!data)
return NULL;
return data->c_str();
}

std::string::size_type size(void)
{
if (!data)
return 0;
return data->size();
}

// Set the manager.
void operator<<(MANAGER *tbl)
{
std::string buf;

if (data)
{
// Remember value
buf = *data;

// Maintain previous value
if (table)
data = table->remove_ref(data);
else
delete data;

// Update table
table = tbl;

// Update pointer
if (table)
data = table->get_ref(buf, data);
else
data = new std::string(buf);
return;
}

// Update table
table = tbl;
}

// Assign value
void operator=(const std::string &str)
{
// No Table? Manual string handling.
if (!table)
{
if (data)
delete data;

data = new std::string(str);
return;
}

// Table string handling.
data = table->get_ref(str, data);
}

// Assign value
void operator=(const std::string *str)
{
// No Table? Manual string handling.
if (!table)
{
if (data)
delete data;

if (str)
data = new std::string(*str);
else
data = NULL;
return;
}

// Table managed strings
if (str)
data = table->get_ref(str, data);
// Null means clear reference
else
data = table->remove_ref(data);
}

// Assign value: Cross implementation support
void operator=(ManagedStringR<MANAGER> &str)
{
// No Table? Manual string handling.
if (!table)
{
if (data)
delete data;

data = new std::string(str());
return;
}

// Table string handling.
data = table->get_ref(str.value(), data);
}
};

// Provider Managed String
// Uses a provider to reference it's string manager.
// Assume table is never null.
//
// Traits: MANAGER
// const std::string *get_ref(const std::string &str, const std::string *old);
// const std::string *get_ref(const std::string *str, const std::string *old);
// const std::string *remove_ref(const std::string *str);
//
// Traits: PROVIDER
// MANAGER *manager(void);
// – Justice
template <class MANAGER, class PROVIDER>
class ManagedStringP
{
protected:
const std::string *data;
public:
ManagedStringP(void)
{
data = NULL;
}

~ManagedStringP(void)
{
PROVIDER p;
MANAGER *table;
table = p.manager();

if (data)
data = table->remove_ref(data);
}

// Conversions
operator const std::string*() {return data;}
operator const char*() {return c_str();}

// String Access
inline const std::string &operator()(void)
{
static std::string BLANK = "";
if (!data)
return BLANK;
return *data;
}

// String Access
inline const std::string *value(void)
{
return data;
}

// is empty?
bool empty(void)
{
if (!data)
return TRUE;
return data->empty();
}

// char buffer
const char *c_str(void)
{
if (!data)
return NULL;
return data->c_str();
}

// size
std::string::size_type size(void)
{
if (!data)
return 0;
return data->size();
}

// assign value
void operator=(const std::string &str)
{
PROVIDER p;
MANAGER *table;
table = p.manager();

// Table string handling.
data = table->get_ref(str, data);
}

// assign value
void operator=(const std::string *str)
{
PROVIDER p;
MANAGER *table;
table = p.manager();

// Table managed strings
if (str)
data = table->get_ref(str, data);
// Null means clear reference
else
data = table->remove_ref(data);
}

void operator=(ManagedStringP<MANAGER,PROVIDER> &str)
{
PROVIDER p;
MANAGER *table;
table = p.manager();

// Table string handling.
data = table->get_ref(str.value(), data);
}
};
13 Sep, 2007, Justice wrote in the 48th comment:
Votes: 0
I've completed installing the managed strings into the major structs (rooms, objects, characters). Based on the experience, I've made a few more changes/additions to the wrappers.

The operator() is not operator*, I feel this is a cleaner way to handle it, and mimics standard behavior.

I've added operator! to return (!data). (if (!ch->name)) With that, I added bool exists(void){return (data != NULL);}. (if (ch->name) replacement).
To fill out the wrapper, I've added a length method that proxies data->length();

Overall, very happy with how it worked. About 70% of the conversion was dealing with printf statements. The majority of the rest was updating functions to take a const char* where applicable. This allows the conversion operators to handle these function calls automatically.
20 Oct, 2007, Noplex wrote in the 49th comment:
Votes: 0
Samson said:
I already did that to my IMC2 module using a function Noplex wrote for me way back when. I've already asked about it on smaugmuds.org but figured I'd post it here too to see what people thought. I've been advised it's a bit inefficient and needs improvement. It does work though.

Really, I wrote that? I always hated the damn C++ string library.

Wow, that must have been years ago. Heh.
40.0/49