Wednesday, October 22, 2014

C Programming: Encapsulation

Encapsulation is perhaps the most valuable aspect of object oriented programming.  Without encapsulation, object orientation would not have been useful enough to become popular, in a large part because most of the other useful aspects of object orientation rely heavily on encapsulation.  Encapsulation is the intuitive grouping of related data into coherent blocks.  In C, the most common form of encapsulation is grouping related functions into libraries.  This only brushes the surface of the possibilities though, because a library is only a single instance of an encapsulated entity, and additional instances cannot be made.  In object oriented programming, encapsulation is typically used to group related variables together with functions used to manipulate them.  Encapsulation's strength is that it can be used to logically order data and operations in a way that is easy to understand and remember.

Encapsulation is not inherently beneficial in programming.  It does not improve program efficiency, and it often harms it.  Encapsulation can improve development speed and make program source code easier to understand.  This can substantially reduce the time required to create a program, and it can also make maintenance far easier.  Object oriented programming languages and styles are primarily popular because encapsulation increases development speed, thus increasing potential profits.  The benefits of encapsulation are primarily business benefits, not benefits to the program itself.

Encapsulation comes at a heavy cost.  While it may be worth the cost to gain the benefits, it is still important to be aware of the costs.  Encapsulation almost always results in both memory and performance costs.  In object oriented languages, objects must store function pointers in addition to data.  These function pointers take up memory, and large numbers of objects or objects with many functions can take up substantial amounts of memory.  Contrasted with purely procedural languages, where each function call is explicitly stored in the code only once, object orientation can be very inefficient in memory usage.  To add to this, many object oriented languages store additional information about objects, such as object type, even in the compiled program, which uses even more memory.  This, however, is not the worst part.

Encapsulation changes how data is stored in memory.  On older computers and embedded systems, this effect may be negligible, but on modern systems with advanced memory caching, it is very important.  Processor caches are used to avoid slow memory access by storing frequently used data on very fast memory on the processor.  This memory is called the cache.  Modern processors use additional techniques to further improve cache performance.  One of these is to load memory into the cache in chunks.  These chunks will always contain the memory that is being accessed, but they also contain nearby sections of memory that are likely to be used by the program in the near future.  Since modern processors are many times faster than memory, caching is important to get the full performance of the processor.  However, how data is used in a program dramatically affects how efficiently the processor cache is used.  A program that jumps around memory, accessing data from widely separated areas, will force the cache to load new data from memory (discarding the old data) very frequently.  When data is not found in the cache and must be loaded from memory, this is called a cache miss.  Cache misses take a long time, during which the processor will either be idle or executing a different program, reducing the performance of the program waiting for the data to be loaded.  When a program loops through a contiguous array of data, cache misses are very infrequent, and performance is maximized.  When a program jumps around, performance is dramatically reduced.  This is important because organization of data in object oriented programs is different from how data is typically organized in procedural programs.  Each object stores its data in a single location.  An array of objects, even in contiguous memory (most OOP languages store objects on the heap, which is not guaranteed to be contiguous), that is looped through to access only one or two member variables will still load the entire objects into the cache.  This causes a lot of unused data to cycle through the cache, displacing data which would have been used.  The result is a dramatic increase in cache misses, which substantially reduces performance.  A similar procedural program might store the "object" data in multiple arrays, only accessing (and thus caching) the data in the arrays that are actually used.  This adds another benefit.  Processor caches are divided into sections, where each section of the cache holds some section of memory.  These sections are rotated through (typically the least recently accessed is overwritten by new data being loaded).  Looping through two arrays at once can take advantage of this when one cache section holds part of one array and another holds part of another array.  Using encapsulation, an array of objects will likely use only one cache section at a time, constantly missing and loading more data.  Now, we have only looked at how encapsulation affects cache usage when an array of objects is contiguous in memory.  This is not typical.  Most object oriented languages dynamically allocate memory for objects, and dynamically allocated memory is often not contiguous.  Looping through non-contiguous memory will cause cache misses at almost every iteration, causing severe performance loss.  The costs of encapsulation are sometimes justified, but it is important to be aware of them when making design decisions.  When using object oriented programming, it is important to understand that OOP is a human invention designed to making interfacing with computers on a low level easier.  Computers do not "think" in objects, so there will always be costs to the translation between human ideas of objects and how computers actually work.  Understanding the underlying architecture can help mitigate the costs of encapsulation, but it cannot eliminate them entirely.

Now we can discuss how to use encapsulation in C.  C already has primitive support for encapsulation, which we can leverage to implement full encapsulation.  Note that this is going to be ugly and unwieldy, and it should typically be avoided wherever possible.  As an exercise in understanding the C language, however, this may be very useful.

C has a meta data type called a struct.  Structs are used to create composite data types that essentially encapsulate data.  Structs can contain only data.  They cannot contain functions or code.  Following is a simple example of a struct.

struct item {
    int id;
    int price;
    int count;
    char* name;
};

This code defines a struct of type "item".  An instance of this struct could be created and manipulated with the following.

struct item can;
can.id = 1;
can.price = 120;
can.count = 12;
The name element would be assigned a pointer to a CString.  This struct might be a data type for an inventory system, where id is the inventory id number, price is the price in cents (since it is an int), count is the number of items in stock, and name is a pointer to a character string containing the name of the item.  This struct could be passed to and returned from functions like any other variable.  We could make an array of this new type to hold all of the different items the store carries.  Unlike most object oriented languages, a statically created array of these structs would be contiguous in memory (though, the character arrays pointed to might not be contiguous, and would certainly not be contiguous within the array).  This helps keep our data coherent and understandable by human standards.  Without the struct, we might instead create a price array, a count array, and a name array, and the ids could be the array indices for each item (we might even avoid storing ids this way, reducing memory costs).  For the struct model, if we only looped through the array when we needed to access every element in the struct, we would not get any performance benefits from separate arrays, but since this is unlikely, we will be paying for coherency in performance (in an inventory system, where performance is not that important, this is probably justified).

Now that we have a struct for our inventory items, we might want to reuse this idea in our POS system.  We might want a struct for transactions.  A transaction might contain a pointer to an array of items (we cannot make the array static, since it may have a different size for each transaction).  We could use the previous struct for items, but we would use the count element as the quantity purchased instead of inventory.  When finalizing a transaction, we might need to calculate sales tax, so a transaction will need a sales tax as well as a function for calculating tax and setting the variable in the struct.  Normally, we would just write a function that takes the struct and modifies it.  Maybe we want to be able to use different algorithms for sales tax depending on the customer (business customers might have a lower tax rate, while out of state and government customers might be exempt), but we want to be able to treat all transactions the same.  This gets into very basic polymorphism, which we will discuss in depth in a different article.  For now, however, we need our transaction to use a different tax algorithm for different customers, and we do not want to have to keep track of tons of information to accomplish this.  We want to be able to populate most of a transaction, then send it to a function to finalize it that can use the same procedure on all transactions.

While structs cannot contain functions, they can contain function pointers.  So long as the functions pointed to have the same signatures (return values and argument lists), they can easily be called in another function that knows how to use them.  We could create several functions for calculating tax, and then we could store a pointer to the appropriate function in the struct.  Later, when we finalize the transaction, we can just call the function pointed to by the transaction, and it will calculate tax for us.  We do not have to care which algorithm is used during finalization.  Here is a very simple example of this.

struct transaction {
    int pretax;
    int tax;
    void (*gettax)(struct transaction*);
};
There is the struct.  The pretax element will contain the sum of the prices of items being purchased (in real life, we would have an array of items, and the finalizer would calculate the pretax total from those).  The tax element begins empty, and it will be populated by the function pointed to by the gettax element.  The gettax element can point to any function that returns void and takes a single argument that is a pointer to a transaction (a pointer because we need to change the original).  Now we need some tax functions.

void normalTax(struct transaction *t) {
    (*t).tax = (*t).pretax * 0.05;
}

void exemptTax(struct transaction *t) {
    (*t).tax = 0;
}

These two functions return void and take transaction pointers, just like the function pointer in the struct definition.  The first calculates a 5% sales tax, while the other sets tax to 0 for tax exempt customers.  Now, we want to create a transaction.  This will be a normal customer, with normal sales tax, who is making a purchase that totals to $10.00 (since we are using ints to store cents, it will be 1000).

struct transaction t;
t.pretax = 1000;        // $10.00
t.tax = 0;              // Initialize to 0
t.gettax = *normalTax;  // Normal customer

There is our transaction.  If we were serving a tax exempt customer, we could set gettax to *exemptTax instead.  Now we are ready to calculate tax.  This can be done with the following line, regardless of what tax algorithm we are using.
(*t.gettax)(&t);
That will call whichever tax algorithm we selected earlier, passing the transaction in by pointer, so we can set the tax element appropriately.  After running the above, we will find that t.tax is equal to 50, which means $0.50.  If we had used the tax exempt algorithm, the tax would have been 0.

This example is obviously contrived, and in real life we probably would have explicitly called a different function for each tax mode.  In fact, this would probably be a better way to do it for this situation, but this sort of encapsulation has its strong points in many other applications.


Again, this is an ugly and unwieldy way of implementing encapsulation.  If you really need encapsulation, it would probably be better to use an object oriented language like C++.  In the rare situation where that is not an option or where you only need very basic encapsulation and only to a very limited degree, this might be appropriate.  If you are considering doing this, first ask yourself why you are using C in the first place.  It is very likely that the reason you are using C is because object oriented programming is much more expensive in memory and performance.  If this is the case, you should probably find a more efficient way of solving your problem.


Here is the source code for a simple C program that implements and uses the transaction example from above:

#include <stdio.h>

struct transaction {
    int pretax;
    int tax;
    void (*gettax)(struct transaction*);
};

void normalTax(struct transaction *t);
void exemptTax(struct transaction *t);

void main() {
    struct transaction t;
    t.pretax = 1000;        // $10.00
    t.tax    = 0;
    t.gettax = *normalTax;  // *exemptTax for no tax

    (*t.gettax)(&t);

    printf("Price: %i\n", t.pretax);
    printf("Tax:   %i\n", t.tax);
}

# For normal 5% sales tax
void normalTax(struct transaction *t) {
    (*t).tax = (*t).pretax * 0.05;
}

# For tax exempt customers
void exemptTax(struct transaction *t) {
    (*t).tax = 0;
}