Journal Articles

CVu Journal Vol 27, #2 - May 2015 + Programming Topics
Browse in : All > Journals > CVu > 272 (9)
All > Topics > Programming (877)
Any of these categories - All of these categories

Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.

Title: Writing Good C++ APIs

Author: Martin Moene

Date: 07 May 2015 14:21:03 +01:00 or Thu, 07 May 2015 14:21:03 +01:00

Summary: Tom Björkholm examines some common pitfalls that make code hard to use.

Body: 

In my view (although every programmer may have a different view on this subject) a good C++ API is an API that guides the user into writing good application code. In particular, it should be:

Over the years a fair number of programmers have appreciated my APIs, so I guess there is some merit to my view of what constitutes a good API.

One question I have asked myself is if the Standard C++ library serves as a good example for API design. Admittedly the Standard C++ library APIs are clever and offer a very effective programming interface. However, it is often designed in a way that is possible to misunderstand. That is OK for the Standard C++ library as there are lots of books out there to explain the correct usage of the API. Also, as it is the Standard C++ library every serious programmer will spend time learning the correct way to use it. That is a luxury that we mortal API developers do not have. Nobody is going to write a book about how to use our APIs, and even if someone wrote such a book, no programmer would find time to read it. We need to define our APIs with more focus on ease of use and making it ‘impossible’ to use incorrectly.

Naturally, the Standard C++ library has introduced a number of concepts, that are now familiar to programmers. Concepts like iterators going from begin() to end(), where end() is past the last valid element, are familiar concepts. Building our APIs on familiar concepts like that makes it easier for programmers to learn our APIs.

Stating what constitutes a good API like this is the easy part. Writing code to create a good API is a lot harder. Naturally, we can keep the view of what constitutes a good API in our mind when creating APIs, but this is still kind of abstract. For this article I will take another route and have a look at some common pitfalls, and try to suggest ways to improve the APIs.

SQL API example and RAII

As the first example we will take a look at a piece of simple code that uses the Oracle C++ database access library OCCI. Such code might look like Listing 1.

void occiUse(const LoginInfo & loginInfo)
{
    oracle::occi::Environment * envP = 
        ::oracle::occi::Environment::createEnvironment();
    oracle::occi::Connection * conP = envP->createConnection(
        loginInfo.username, loginInfo.password(), loginInfo.alias);
        
    const std::string query = "select count(*) from user_tables";
    oracle::occi::Statement * stmtP = conP->createStatement(query);
    oracle::occi::ResultSet * resP = stmtP->executeQuery();
    
    if (resP-> next())
    {
        doSomeThing("occiUse", resP-> getInt(1));
    }
    
    stmtP->closeResultSet(resP);
    conP->terminateStatement(stmtP);
    envP->terminateConnection(conP);
    ::oracle::occi::Environment::terminateEnvironment(envP);
}
Listing 1

I guess that you immediately spotted the problems with this code: If doSomeThing() throws an exception, this code will leak a ResultSet, a Statement, a Connection and an Environment. The functions executeQuery(), createStatement() and createConnection() calls may also throw exceptions leading to resource leaks. You could argue that the programmer who wrote this code was criminally incompetent to ignore RAII (Resource Acquisition Is Initialization), but I claim that it is the design of the API that has tricked the programmer into ignoring RAII. The code is written in the style that naturally fits the design of the API. Unfortunately many other database APIs are just as bad as the Oracle OCCI API. I ended up writing a wrapper library for OCCI just to make it ‘RAII compatible’. Using this wrapper library the code for the same operation would look like Listing 2.

void myDbUse(
    const LoginInfo & loginInfo, std::shared_ptr< MyLib::LogBase> log)
{
    MyDb::Connection con(log, loginInfo);
    const std::string query = "select count(*) from user_tables";
    MyDb::Statement stmt(con, query);
    MyDb::ResultSet res(stmt);
    if (res.next())
    {
        doSomeThing("myDbUse", res.getInt(1));
    }
}
Listing 2

Please ignore log for a moment, I will get back to it. Here you can see that the API clearly guides the application programmer into writing code that uses RAII. Connection, Statement and ResultSet are all created using the constructor and destroyed using the destructor. (If you wonder what happened to the Environment, it has now become a member of the Connection.)

This API is still not perfect. I am not happy with the fact that the newly created ResultSet is positioned before the first row in the result. This seems a bit odd in C++. On the other hand this is the way most SQL APIs distinguish between an empty ResultSet and one with rows in it, so if the application programmer is used to other SQL APIs this seems natural. I am playing with the idea of modelling the result set as an input iterator, but that idea has not yet moulded into a final design. Also I am not happy with the way getSomething(1) is used to get the first column. C++ uses the convention to let index 0 denote the first element, whereas SQL uses index 1 for the first element. Either scheme of indexing is likely to fool some users, because we are mixing C++ and SQL. I am playing with the idea of using the input operator >> to get the next column of the ResultSet, which would circumvent any confusion about indexing altogether.

Context neutral

A good API should be context neutral. We want to be able to use the same API in many different contexts. The context dependent parts should be factored out. I will use the previous database API example to illustrate this.

If we write an API for database access we should not limit its usage to only a specific context, like a daemon (server/service) program, a command line utility, a graphical user interface application, or a library that will be wrapped in JNI, or...whatever. We want our API to be usable in all foreseeable contexts. For some of these contexts we might want to do some things in different ways.

One thing that is present in almost all bigger software products is logging. This is sometimes called trace logging, sometimes debug logging, and on some UNIX daemons it might be referred to as ‘syslogging’. The idea is to output information about the internal state and the decisions taken in the program flow, to enable the support technician to understand what went wrong when the customer complaints arrive. It is easy to see the generalized functionality: the programmer prints information and when printing it mentions what type of information it is. The log class (or framework) will then look at runtime configuration to determine if the log printout shall be carried out or if the output shall be suppressed. Logging an error shall not be confused with handling the error. The logging is only done to help technicians locate the fault.

Logging is something that is typically handled differently in different contexts. For instance a database problem might cause an error message to be written to the standard error stream if it is a command line utility, but it should be logged to a log file when the API is used in a daemon and result in a status pane update if it is used in GUI. By defining a log base class that has pure virtual methods for logging, and by accepting a reference to such a log base class the API has been decoupled from the decision of where the log ends up. The user of the API might choose one of a number of predefined derived log classes, or the user of the API might derive her own log class. This is the log argument in the database API example. In that example there is an additional twist to it. The problem that should be reported might actually happen in the destructor. To have a derived log class object accessible in the destructor, it is actually passed in to the constructor as a std::shared_ptr and stored in the object for later use.

Finding context dependent parts to be factored out is not limited to logging. The challenge here is to imagine all the possible contexts where the API might be used, and to recognize and factor out the parts that are specific to some contexts. Still we need to keep the API small and tidy, so that it is easy to understand.

Ways to pass arguments and return values

Let’s look at a function declaration as it might appear in some API.

  XY_Receipt * XY_sendData(XY_Data * datap, XY_Flags * flagsp);

Although APIs like this are quite common, I think that it is a scary example of a bad API. The problem is that the usage remains unclear to the application programmer:

With a function declared like this, the user has to rely on documentation and comments to try to understand how to use it. Relying on something that is not in the code itself is not good. Too often the code deviates from the comments and documentation. They might match when the code is initially written, but then there will be a number of bug fixes and change requests in hectic ‘survival of the company’ projects and the code just starts to deviate from the comments.

It is much better if as much as possible can be communicated from the API programmer to the application programmer in the code itself. Let’s fix the obvious flaws in the above function API. We will then get:

  XY_Receipt XY_sendData(const XY_Data & data, const XY_Flags & flags);

This function declaration makes it much clearer how to use it:

I prefer to use the return values to pass values from a function to the caller. This is much cleaner and easier to communicate than to modify non-const reference arguments. In C++11 (and C++14) we have move constructors, and decent compilers use RVO (return value optimization). The result is that it is usually cheap to return even large values (like conglomerates of std::map, std::vector and std::string). In my view there is no longer any good case to use non-const reference (or pointer) arguments for passing out values from a function.

When it comes to arguments I favour const references. Admittedly some objects (like ints) might be faster to copy than to pass by const reference. Still I feel that consistent use of const reference communicates with the user of the API (the application programmer) in an easy to understand way. (Especially as the compiler does not differentiate between const A a and A a in function declarations. I found that many users of my APIs get confused if they see func(const A a) in the header file, but get a compiler error message about func(A a).)

When modifying data we have the option of modifying the argument, or keeping the argument unchanged and returning the changed data. Let’s take a function that capitalizes all words in a string as an example

  void capitalizeAllWords(std::string & s);
  std::string capitalizeAllWords(const std::string & s);

Here you can argue that there is a performance cost of using a return value instead of modifying the argument. Still in most cases I prefer the version with return value. The code using it becomes much easier to read. (And please, never provide both the above versions as overloads. That would definitely be confusing for the user.)

I guess the readers of this magazine already know everything about const correctness. I will not dwell on it now. If there is enough interest that might be the scope of another article.

Unintended API consequences

As my next example I will use a class that stores a parsed C++ source file. (It might be part of a C++ IDE or it might be part of a tool like Doxygen.) With some designers this class might have an interface that includes:

  class CppFile {
  public:
    // ...
    unsigned int & numberOfFunctions();
  };

The interface actually allows the class to be used like this:

  void g(CppFile & cpp) {
    cpp.numberOfFunctions() = 0;
    // remove all functions
    
    cpp.numberOfFunctions() = 5000;
    // create some other functions
    //...
 }

Unless you consider this to be legitimate usage, the member function should not return a non-const reference. Here I cannot see how the class implementation could know what code to create as the source code of these created functions, so I think the interface should not return a non-const reference. Having access methods that return non-const references is useful for container classes where we have well-defined semantics for accessing and changing the contained elements. If the class is a wrapper around another class (like MyDb::Connection wrapping oracle::occi::Connection) there is also a legitimate case for having a member function that returns a non-const reference to the wrapped object. In most other cases I feel that returning a non-const reference just creates trouble.

Call-backs – when action is needed from the application

Sometimes an interface wants to allow the calling code to react to some events. I prefer to use call-backs for this. Call-backs are also useful if the interface implementation would like to let the calling code make decisions about how to handle some situations.

Call-backs can be of three different styles:

Often I prefer to use the abstract base class style call-backs. I find that these allow the API designer to impose structure and communicate to the application designer what events the application code is supposed to (or allowed to) handle. To make this more concrete and easier to understand, I will demonstrate it using a small example (see Listing 3) with a simplified SMS sending API. (SMS is another name the short text messages sent over the mobile telephony network. SMSC is the telephony network server you connect to in order to send a message. PoR stands for ‘Proof of Receipt’ and is either a positive or negative acknowledgement to a sent message.)

class SMS_Data
{
    /* ... */
};
class SMS_Address
{
    /* ... */
};
class SMS_Channel
{
public:
    class Cb;
    class SendCb;
    SMS_Channel() = delete;
    explicit SMS_Channel(Cb & cb);
    
    enum class SendError
    {
        OK,
        NoSmscConnection,
        sizeLimitExceeded
    };
    
    struct SendResult
    {
        unsigned long long id;
        SendError sendError;
    };
    
    SendResult send(
        const SMS_Data & data,
        const SMS_Address & address,
        std::shared_ptr< SendCb> cb);
    // ...
};

class SMS_Channel::Cb
{
public:
    virtual void lostSmscConnection() = 0;
    virtual void dataReceived(
        const SMS_Data & data,
        const SMS_Address & to,
        const SMS_Address & from) = 0;
    // ...
};

class SMS_Channel::SendCb
{
public:
    virtual void okPoR(
        unsigned long long id,
        const SMS_Data & sentData,
        const SMS_Address & wasTo) = 0;
    
    virtual void errorPoR(
        unsigned long long id,
        const SMS_Data & sentData,
        const SMS_Address & wasTo,
        const SMS_Data & responseData) = 0;
    // ...
};
Listing 3

The abstraction captured in this API is about sending data, receiving data and reacting to events.

When we have a connection to an SMSC we might at any time receive a message, or lose the connection. Things like these, which are not related to any specific message we send, are handled in the SMS_Channel::Cb call-back that is passed to the constructor. The SMS_Channel class might cooperate with some event-dispatcher framework, or it might have an internal thread that listens to input from the network. In either case the call-back is called when there is an event that the application code needs to handle. With these call-back base classes it is easy for the API designer to tell the application programmer what events the application needs to handle.

In a similar way the send member function takes a call-back object, that is called to handle events that are caused specifically by sending the message. This can be the reception of a positive or negative acknowledgement ‘PoR’. As the application code has probably exited the local function scope where the send member function was called long before the call-back is called, the call-back is this time passed as a std::shared_ptr. Without a call-back class like this, it is quite hard to communicate to the application programmer what events the application code needs to handle.

The SMS_Channel::Cb is passed as non-const reference. This is not an obvious choice. The non-const reference prevents the user from creating a temporary object in the call to the SMS_Channel constructor (which can be seen as a very desirable thing to do). On the other hand, a const reference would require all the call-back member functions to be const, and thus prevent the call-back object from changing state. I made the judgement that the possibility to change state and keep state between call-back invocations is more valuable.

Allowed order to call function in an interface

If an interface has several functions, the application programmer should be allowed to call them in any order that is allowed by the syntax. Remember, it should be ‘impossible’ to use the interface incorrectly. This can be a real challenge for the API designer.

One way to solve this is to use the C++ syntax rules to limit the possible order that functions may be called in. If we look back at the database API example the Statement constructor needs a Connection as an argument. This is an effective way of specifying the order. It does also specify the order of destructions (and the API user will do it correctly without any focus on order requirements).

Conclusions

It is hard to write a good C++ API. There are many pitfalls. By keeping an open eye for the pitfalls and by thinking about the API from the viewpoint of how we would like to use it as application programmers, I think that we can improve our API designs. Whenever an application programmer asks questions about an API that I wrote, I think that it is important to not only answer the question, but also to analyse if the question is a sign that the API design should be improved. I hope that some of the readers have found some useful hints in this article, and that maybe some other readers have silently nodded and recognized pitfalls and good practices from their own experience.

Notes: 

More fields may be available via dynamicdata ..