When working on embedded systems, using C++, it is often desirable to have a single class implementing multiple interfaces. The naive way of doing this (i.e., using multiple inheritance from a set of interfaces), while perfectly reasonable for normal situations, does pose some performance and memory problems for its use on embedded systems.
Given these costs, an approach used in embedded systems is to do away with interfaces altogether, thus saving all the implementation overheads. But this also leads to more coupled designs for object interaction, since method invocations occur on concrete objects instead of on interfaces, and it might also lead to code duplication and a loss of maintainability.
We propose a technique to use interfaces within the tight constraints of an embedded environment, and discuss its merits against alternative solutions to the problem.
Use scenario
During the development process, it is often required that one class must implement multiple interfaces; in other words, a class must be developed that can receive different sets of messages from different types of users. It is worth noting that the term interface is used as defined in [GoF94], “The set of all signatures defined by an object’s operations. The interface describes the set of requests to which an object can respond.†This is a narrower definition of interface than the usual one, since it does not require the interface to have many implementations. In fact, the technique proposed here only works for single-implementation interfaces.
In embedded systems’ development, there are often many restrictions in both the amount of memory available and the amount of processing resources available; these limitations make some of the most common ways to perform this multiple implementation undesirable. We will review some common solutions, along with their specific drawbacks for embedded systems.
Alternative solutions
The typical solution for this problem is ‘dynamic polymorphism’ [Strachey67]: to create an interface (with its methods declared as virtual) for each user type, and to create a concrete class that inherits from all interfaces and implements their methods. Users of the class then access the base class object via pointers to the corresponding interface. Within embedded environments, where memory is scarce (which means that it is preferable to avoid having vtables) and there may be strenuous time constraints (which makes indirections undesirable due to their time cost), dynamic polymorphism is not an optimal solution. Figure 1 shows a typical solution’s UML diagram.
Figure 1 |
Two techniques related to the use of dynamic polymorphism are using either dynamic_cast or the pimpl idiom [Coplien92] (where a pointer to the implementing class is kept in the interface), but any of these two options would also incur in the memory and processing overhead caused by the usage of virtual dispatching, because both alternatives require pointer indirections to be used. Since avoiding indirection costs (both in performance and in memory) is our goal, we will not analyze these alternatives any further.
Other alternatives considered were: the Role Object design pattern [Baumer97] and relying on compiler’s devirtualization [Namolaru06].
The Role Object pattern imposes a significant overhead on runtime performance (because roles are resolved dynamically), which renders it unusable for embedded environments.
Devirtualization is the process of resolving virtual method calls on compile time, thus saving the dynamic dispatch overhead. It is not required by the standard, and there is no guarantee that it will be available on any toolchain chosen to build a project. Our technique, while far more limited in scope, can be used with any standard-compliant toolchain.
The technique we present is an alternative that has no pointer indirection overheads (thus saving the memory and processing costs incurred by using pointers), but that still allows interfaces to be used.
The technique
The proposal is to reverse the normal order of inheritance, dividing the responsibilities between three kinds of classes.
- Base implementation: this is the base class for the entire hierarchy. It contains all the interfaces’ method implementations. Its constructor is protected, to prevent instantiations of this class.
- Interfaces: there is one of these classes for every type of user. One of the interfaces inherits from base implementation, and then every new interface inherits from the most derived interface thus far. All inheritances are protected, and so are all constructors. Each interface exposes its methods within its public part with using declarations.
- Getter implementer: this is the last class in the hierarchy, and it inherits privately from the most derived interface. It is the only class in the entire hierarchy that can be instantiated, and it provides a getter method for each one of the interfaces in the hierarchy.
Figure 2 is the technique’s UML diagram.
Figure 2 |
An example
Let there be two interfaces, Writer
and Reader
; Writer
shows the write
and close
methods, and Reader
shows read
and close
methods. OnlyImplementation
implements both interfaces.
Using the typical solution (dynamic polymorphism) would yield something similar to Listing 1.
struct WriterInterface { virtual void write() = 0; virtual void close() = 0; }; struct ReaderInterface { virtual void read() = 0; virtual void close() = 0; }; class OnlyImplementation : public WriterInterface, public ReaderInterface { private: virtual void read() { /* ... */ } virtual void write() { /* ... */ } virtual void close() { /* ... */ } }; |
Listing 1 |
A usage example would be as in Listing 2.
void writerUser() { OnlyImplementation oi; WriterInterface* const wif = &oi; wif->write(); wif->close(); } |
Listing 2 |
In this design, the interfaces are base classes and implementation is on a derived class.
The previous example, adapted to the technique, is shown in Listing 3.
class OnlyImplementationBase { protected: void read() { /* ... */ } void write() { /* ... */ } void close() { /* ... */ } OnlyImplementationBase() = default; }; class WriterInterface : protected OnlyImplementationBase { public: using OnlyImplementationBase::write; using OnlyImplementationBase::close; protected: WriterInterface() = default; }; class ReaderInterface : protected WriterInterface { public: using OnlyImplementationBase::read; using OnlyImplementationBase::close; protected: ReaderInterface() = default; }; class OnlyImplementation final : private ReaderInterface { public: ReaderInterface& getReader() { return *this; } WriterInterface& getWriter() { return *this; } }; |
Listing 3 |
A usage example would be as in Listing 4.
void writerUser() { OnlyImplementation oi; WriterInterface& const wif = oi.getWriter(); wif.write(); wif.close(); } |
Listing 4 |
Two C++11 features (=default
for constructors [Crowl07] and final
on the most derived class [ISO/IEC 14882-2011]) are used, but they both have equivalent expressions in C++03.
As you may see, the overhead on the user’s side is an invocation of the getter method to get the desired interface. On the developer’s side, the code is more contrived, but it does not incur in any type of pointer indirection overhead.
Conclusion
This technique was created to allow the use of multiple interfaces even under very rigorous performance and memory constraints. As it was shown, the overheads associated with all the other alternatives that were considered have been avoided.
The drawback of the technique is that there is no automatic upcast from implementation to interfaces, thus forcing the use of getters for the various interfaces. These calls to getters could be inlined to minimize their performance impact, but they do place an additional burden on the developer.
It should be stressed that this technique is only useful when the implementation is unique; when multiple implementations of one of the interfaces are necessary, this technique cannot be used. While this is a significant limitation, it is a frequent situation in embedded systems, where single implementations of interfaces are not uncommon; thus we consider that this technique, albeit limited in scope, helps to solve a family of problems with a better trade-off than its alternatives.
Acknowledgements
We would like to thank Fabio Bustos, Fernando Cacciola and Angel Bustamante for their comments and suggestions. We would also like to thank the editing team of Overload for their helpful reviews.
References
[Baumer97] D. Bäumer, D. Riehle, W. Sibersky, M. Wulf (1997) ‘The Role Object Pattern’ http://st-www.cs.illinois.edu/users/hanmer/PLoP-97/Proceedings/riehle.pdf
[Coplien92] James O. Coplien (1992) Advanced C++ Programming Styles and Idioms, Addison-Wesley
[Crowl07] Crowl, Lawrence. Defaulted and Deleted Functions.
[GoF94] E. Gamma, R. Helm, R. Johnson, J. Vlissides Design Patterns: Elements of Reusable Object-Oriented Software, First Edition, 1994.
[ISO/IEC 14882-2011] ISO/IEC 14882-2011. Programming Languages – C++, Third Edition, 2011.
[Namolaru06] M. Namolaru, ‘Devirtualization in GCC’, http://ols.fedoraproject.org/GCC/Reprints-2006/namolaru-reprint.pdf
[N2346= 07-0206]. ‘Programming Language C++’ Evolution Working Group.
[Strachey67] C. Strachey ‘Fundamental Concepts in Programming Languages’ http://www.itu.dk/courses/BPRD/E2009/fundamental-1967.pdf
Overload Journal #121 - June 2014 + Programming Topics
Browse in : |
All
> Journals
> Overload
> o121
(6)
All > Topics > Programming (877) Any of these categories - All of these categories |