Skip to main content

C++ Developer Interfaces

I've been developing with C++ very extensively for the past year or so. Most of this development has been on heavily optimized codepaths, using modern C++17 practices. After creating a few developer interfaces, I realized that I was taking a similar general approach to interface architecture.

Above all else, I typically think of other developers first when programming. Even if I have a set of functionality requirements to meet, as I develop code I really do think a lot about what interfaces would make it as convenient as possible for other developers to use. Code is a UI/UX domain like any other.

Typically in modern C++, when designing a class interface you either make a concept interface (compile-time interface) or an abstract base class (with pure virtual methods, creating a runtime interface).

Concept (Compile-Time) Interfaces

I am eagerly awaiting concepts to finally land in the language (c'mon, C++20!). I think the largest advantage they will provide, besides easier up-front verification of concepts by the compiler, is incredibly more readable and understandable error messages. There are a lot of nice things targeting C++20, like the Ranges TS (already mostly implemented here), but concepts are going to make huge changes to developer quality of life and in general make templating an easier feature to use.

Wishes for the future aside, one might make a concept (whether verifiable by the compiler or not) for a particular interface, and make templates functions/classes which require that concept on the template type(s). Some advantages of templating are

  1. The concept may require certain features of a type which are not able to be captures as features of an abstract base class (as they can only specify method signatures).
  2. Templated types allow the compiler to have more total knowledge, which can be used to more aggressively optimize code.

So typically one might want to use a concept interface in code where the type itself may need a more detailed definition of what operations will be performed with it (beyond method calls), and/or when operations of the type may be performed often or in a tight loop.

You might be thinking: why not always use concepts? Well, that day may be here soon, but it's not quite here yet. Templated code can produce notoriously poor error messages. More recent GCC/clang compilers are getting better at sorting through those, but it's still rough going. There's something about wading through hundreds of lines of wrapping template types that puts a sour taste in developer's mouths :)

To the compiler's credit, the actual source of the error is always in that output, it's just a matter of parsing it mentally and, in some cases, decrypting the source of the error based on the final symptoms.

Abstract Base Class (Runtime) Interfaces

The other side of the coin for developer interfaces is runtime classes that use vtable lookups to get methods. Abstract base (pure virtual) classes are the more object-oriented interface, that are fairly equivalent to Interfaces in Java or C#. These interfaces can only define instance method signatures to be overridden, but the good news is that, since the dawn of C++ (approximately), the compiler can verify whether an implementer of the interface has done so correctly. Thus, errors in interface implementation are usually obvious, and easy to remedy. Likewise, code that consumes base class interfaces can be verified to ensure it's using precisely and exclusively the interface methods; with concepts as they currently are, a templated function/class may accidentally do things with a type that are not in its concept.

So the advantages of abstract base classes are mainly usability ones (which will be evened out when the concepts TS is in the language). The main disadvantage to abstract base classes is the level of indirection that is introduced by method lookup in a vtable.

By this, I don't mean the cost of an indirect function call; that really doesn't make a huge difference (yeah, you can measure some small difference if you're lucky). In my experience, the compiler optimizes code involving virtual function calls pretty well. However, if you have a virtual function call somewhere, that immediately acts as a barrier to further optimization based on the 'contents' of the call. In a lot of interface situations, this won't matter much. However, if your interface can potentially be offering somewhat trivial functionality (say, performing a cast), and will be chaining or composing with other interfaces, that can cause a big performance hit. Remember, if the code is templated, the compiler has total knowledge of the types, and in theory can optimize as if one had written specialized code for the task at hand.

Also, technically, a developer should only need to use an abstract base class if, at runtime, there is some functionality that can be changed, whether it be by user input (from configuration or otherwise) or to generalize operations across multiple types (without using templates).

A Happy Medium

So, this comes to the idiom I've been following lately. It's pretty simple, but when there is performance-critical code involved, it gives power to the developer to choose whether they want/need to create the virtual interfaces and possibly lose some performance optimizations. Some situations where this approach has worked well for me include a serialization library and a stream library. In both cases, there are a lot of operators/transformations that aren't doing much and are being composed, so getting the compiler to optimize as much as possible is really beneficial.

The approach is this: the primary interface is a concept interface (hopefully well documented!) to allow for optimization when possible. However, concept interfaces are a superset of virtual interfaces, so we also make a corresponding abstract virtual base class that itself implements the concept and makes a vtable for all of the methods in the concept. Finally, we can close the loop for developers by providing a convenience class which takes any type implementing the concept, creates a subclass which overrides the abstract base class with that type, and stores it internally as the abstract base class (thus erasing the original type but creating a vtable wrapper which also implements the concept).

I'll give an example in a second, but I want to reiterate that I've found the advantage of this approach is that other developers can create optimized interfaces, but can also choose to either erase complex types if needed or store the interface virtually in situations where it may differ at runtime. I'd also like to point out that, being an approach that is primarily a concept interface, it does have the downside of large template errors if you're not careful, so keep that in mind.

Example

Let's walk through a simple example. Suppose you want to make a streaming interface. It might have a concept such as:

template <typename T>
concept Stream = requires {
    typename T::value_type;
    requires requires(T a, typename T::value_type &v) {
        { a.next(v) } -> bool;
    };
};

That is, a type T satisfies the Stream concept if it defines a member typedef value_type and if it has a next method that takes a reference to the member typedef type and returns a bool. We stream values by calling next(v) repeatedly until the return value is false. We also take a reference to not impose any allocations on the interface.

You could imagine writing stream interfaces which wrap others and add more transformations along the way. With a concept interface, such composition can be optimized really well, almost always as if you had written a single stream to do all of the operations that might be composed.

You could expose the abstract base class to developers, but really all you need is the wrapper which erases the original type using an interface, so you might want to hide it as an implementation detail. Regardless, the 'virtualized' interface might look like:

template <typename T>
struct stream {
    using value_type = T;

    stream() = default;
    stream(stream &&other) = default;

    template <typename From, typename = std::enable_if_t<!std::is_same_v<From,stream>>>
    stream(From &&f) : wrapped(std::make_unique<wrapper<std::remove_reference_t<From>>>(std::forward<From>(f)))
    {
        static_assert(std::is_same_v<typename std::remove_reference_t<From>::value_type,T>,
                      "mismatched stream value types");
    }

    stream& operator=(stream &&other) = default;

    bool next(value_type &v) { return wrapped->next(v); }

  private:
    struct base {
        virtual ~base() {}

        bool next(T &v) { return do_next(v); }
      private:
        virtual bool do_next(T &) = 0;
    };

    template <typename From>
    struct wrapper : public base {
        wrapper(const From &inner) : inner(inner) {}
        wrapper(From &&inner) : inner(std::move(inner)) {}

        bool do_next(T &v) override { return inner.next(v); }

      private:
        From inner;
    };

    std::unique_ptr<base> wrapped;
};

You might also want to add a creation function to make the correct virtualized wrapper type...

template <typename From>
stream<typename std::remove_reference_t<From>::value_type> virtual(From &&f) {
  return stream<typename std::remove_reference_t<From>::value_type>(std::forward<From>(f));
}

... or a c++17 deduction guide for the stream class:

template <typename From, typename = std::enable_if_t<!std::is_same_v<From,stream>>>
stream(From &&) -> stream<typename std::remove_reference_t<From>::value_type>;

Note that I haven't tried to compile the above examples, but they should be pretty close to functional, as it's all been functional before :)

So now developers can implement the interface by implementing the given Stream concept, but if they need to, they can erase ugly types or swap implementations at runtime by using the stream wrapper class anywhere they were using the custom concept implementation. For example:

//! A stream which yields integers in the range [0,end].
struct num_stream {
    using value_type = int;

    num_stream(int end) : end(end) {}

    bool next(value_type &v) {
        if (n >= end) return false;
        v = n++;
        return true;
    }

  private:
    int n = 0;
    const int end;
};

//! A stream which drops every other item from the source stream.
template <typename Stream>
struct drop_stream {
    using value_type = typename Stream::value_type;
    drop_stream(Stream &&source) : source(std::move(source)) {}

    bool next(value_type &v) {
        // Read once without returning (unless there's an error).
        if (!inner.next(v)) return false;
        // Read and return the next value.
        return inner.next(v);
    }

  private:
    Stream inner;
};

// Could keep types by using an auto type and returning a tuple, if we wanted to.
std::vector<stream<int>> do_stuff() {
    std::vector<stream<int>> streams;
    streams.emplace_back(drop_stream{drop_stream{num_stream{20}}});
    streams.emplace_back(num_stream{100});
    return streams;
}

Conclusion

I hope this approach is inspiring enough that others may use it in their interfaces. Again, it's specifically useful when you have an interface that will be composed/chained or may be doing simple things (maybe only a couple of CPU instructions) as compilers can inline and optimize very well. For other interface situations, it's probably still better to simply use an abstract base class if performance isn't much of a concern and you like easily-navigable error messages.

I haven't really seen something like this (a virtual wrapper over a concept) in other libraries, but I'd be surprised if it wasn't present elsewhere (and maybe I have used libraries that do it but it wasn't obvious).

Now if only concepts were supported by the compilers, we'd really be in business...

Comments