There’s a phenomenon I think of as the “evolution of objects” and it impacts novice programmers. They start by having piles of variables named things like userName0
, userName1
, accountNum0
, accountNum1
, etc. This is awkward and cumbersome, and then they discover arrays. string* userNames
, int[] accountNums
. This is also awkward and cumbersome, and then they discover hash maps, and can do something like Map<string, string>* users
. Most programmers go on to discover “wait, objects do that!”
Not so Brian’s co-worker, Dagny. Dagny wanted to write some C++, but didn’t want to learn that pesky STL or have to master templates. Dagny also considered themselves a “performance junkie”, so they didn’t want to bloat their codebase with peer-reviewed and optimized code, and instead decided to invent that wheel themselves.
Thus was born CHashMap
. Now, Brian didn’t do us the favor of including any of the implementation of CHashMap
, claiming he doesn’t want to “subject the readers to the nightmares that would inevitably arise from viewing this horror directly”. Important note for submitters: we want those nightmares.
Instead, Brian shares with us how the CHashMap
is used, and from that we can infer a great deal about how it was implemented. First, let’s simply look at some declarations:
CHashMap bills;
CHashMap billcols;
CHashMap summary;
CHashMap billinfo;
Note that CHashMap
does not take any type parameters. This is because it’s “type ignorant”, which is like being type agnostic, but with more char*
. For example, if you want to get, say, the “amount_due” field, you might write code like this:
double amount = 0;
amount = Atof(bills.Item("amount_due"));
Yes, everything, keys and values, is simply a char*
. And, as a bonus, in the interests of code clarity, we can see that Dagny didn’t do anything dangerous, like overload the []
operator. It would certainly be confusing to be able to index the hash map like it were any other collection type.
Now, since everything is stored as a char*
, it’s onto you to convert it back to the right type, but since char
s are just byte
s if you don’t look too closely… you can store anything at that pointer. So, for example, if you wanted to get all of a user’s billing history, you might do something like this…
CHashMap bills;
CHashMap billcols;
CHashMap summary;
CHashMap billinfo;
int nbills = dbQuery (query, bills, billcols);
if (nbills > 0) {
// scan the bills for amounts and/or issues
double amount;
double amountDue = 0;
int unresolved = 0;
for (int r=0; r<nbills; r++) {
if (Strcmp(bills.Item("payment_status",r),BILL_STATUS_REMITTED) != 0) {
billinfo.Clear();
amount = Atof(bills.Item("amount_due",r));
if (amount >= 0) {
amountDue += amount;
if (Strcmp(bills.Item("status",r),BILL_STATUS_WAITING) == 0) {
unresolved += 1;
billinfo.AddItem ("duedate", FormatTime("YYYY-MM-DD hh:mm:ss",cvtUTC(bills.Item("due_date",r))));
billinfo.AddItem ("biller", bills.Item("account_display_name",r));
billinfo.AddItem ("account", bills.Item("account_number",r));
billinfo.AddItem ("amount", amount);
}
}
else {
amountDue += 0;
unresolved += 1;
billinfo.AddItem ("duedate", FormatTime("YYYY-MM-DD hh:mm:ss",cvtUTC(bills.Item("due_date",r))));
billinfo.AddItem ("biller", bills.Item("account_display_name",r));
billinfo.AddItem ("account", bills.Item("account_number",r));
billinfo.AddItem ("amount", "???");
}
summary.AddItem ("", &billinfo);
}
}
}
Look at that summary.AddItem ("", &billinfo)
line. Yes, that is an empty key. Yes, they’re pointing it at a reference to the billinfo
(which also gets Clear()
ed a few lines earlier, so I have no idea what’s happening there). And yes, they’re doing this assignment in a loop, but don’t worry! CHashMap
allows multiple values per key! That ""
key will hold everything.
So, you have multi-value keys which can themselves point to nested CHashMap
s, which means you don’t need any complicated JSON or XML classes, you can just use CHashMap as your hammer/foot-gun.
//code which fetches account details from JSON
CHashMap accounts;
CHashMap details;
CHashMap keys;
rc = getAccounts (userToken, accounts);
if (rc == 0) {
for (int a=1; a<=accounts.Count(); a++) {
cvt.jsonToKeys (accounts.Item(a), keys);
rc = getAccountDetails (userToken, keys.Item("accountId"), details);
}
}
// Details of getAccounts
int getAccounts (const char * user, CHashMap& rows) {
// <snip>
AccountClass account;
for (int a=1; a<=count; a++) {
// Populate the account class
// <snip>
rows.AddItem ("", account.jsonify(t));
}
With this kind of versatility, is it any surprise that pretty much every function in the application depends on a CHashMap
somehow? If that doesn’t prove its utility, I don’t know what will. How could you do anything better? Use classes? Don’t make me laugh!
As a bonus, remember this line above? billinfo.AddItem ("duedate", FormatTime("YYYY-MM-DD hh:mm:ss",cvtUTC(bills.Item("due_date",r))))
? Well, Brian has this to add:
it’s worth mentioning that our DB stores dates in the typical format: “YYYY-MM-DD hh:mm:ss”. cvtUTC is a function that converts a date-time string to a time_t value, and FormatTime converts a time_t to a date-time string.