The Interface Principle in C++
The Interface Principle in C++ encompasses a specific combination of features and ways of considering what an interface is, that allows to write expressive C++ code that preserves encapsulation. It has been around for a while, is still currently used, and may be enriched in the future versions of the language. So it’s worth being aware of.
Note that the Interface Principle goes beyond the general concept of having interfaces, and is not directly related to polymorphism.
The convention we will use throughout this article is this:
- a method designates a routine that is a member of a class,
- a (free) function is a routine that is not part of a class.
Non-member (non-friend) functions
In Item 23 of Effective C++, Scott Meyers encourages us to pull methods of a given class outside of the class, whenever it is possible to implement them in terms of the public interface of the class (or with other methods that have been taken out of the class).
To illustrate this let’s consider the Circle
class that provides its radius, area and perimeter:
class Circle { public: explicit Circle(double radius) : m_radius(radius) {} double getRadius() const {return m_radius;} double getPerimeter() const {return 2 * Pi * m_radius;} double getArea() const {return Pi * m_radius * m_radius;} private: double m_radius; };
A first improvement would be to use the public interface inside the implementation of the methods:
double getPerimeter() const {return 2 * Pi * getRadius();} double getArea() const {return Pi * getRadius() * getRadius();}
And then these methods can be taken out of the class. Indeed they don’t need to be class methods, because they don’t use anything a external function couldn’t use. Taking them out of the class and making them free functions guarantees that this characteristic of not using anything else than the public interface will be preserved, and therefore contributes to the encapsulation of the insides of the Circle
class.
class Circle { public: explicit Circle(double radius) : m_radius(radius) {} double getRadius() const {return m_radius;} private: double m_radius; }; double getPerimeter(Circle const& circle) {return 2 * Pi * circle.getRadius();} double getArea(Circle const& circle) {return Pi * circle.getRadius() * circle.getRadius();}
Another way to see this is that this decreased the amount of code that could be impacted by a change in the implementation of the class Circle
, therefore making the code a bit more robust to future change.
If you want a way to reproduce this consistently, here is the methodology we applied:
- check that the implementation of a given methods only depends on the public interface (or make it so if it’s not too much hassle),
- create a free function with the same name as the method,
- add the type of the class as first parameter:
- pass it by reference if the methods was not const
- pass it by reference-to-const if the method was const
- paste the implementation, adding the object name before each call to the class public interface.
It is important to note that the new free function should have the same name as the old method. Sometimes we are reluctant to call a free function getPerimeter
. We would be more inclined to call it something like getCirclePerimeter
. Indeed, since it is not enclosed in the Circle
class, we may feel it is ambiguous to omit the term “Circle”. But this is wrong: the term “Circle” already appears in the type of the first argument. Therefore it is reasonably expressive both for a human and a compiler to omit the type name in the function name.
Actually, including the argument type in the function name would even lead to code looking slightly weird:
getCirclePerimeter(circle); // "Circle" mentioned twice
as opposed to:
getPerimeter(circle);
which reads more naturally. Also, the fact that the argument type is a Circle
makes it unambiguous for the compiler that this is the function you mean to call, even if there are other overloads sharing the name getPerimeter
.
The Interface Principle
The new version of the class Circle
has something that may seem disturbing: it has functionality declared outside of its interface. That was the purpose of making methods non-members in the first place, but normally a class should expose its responsibilities within its “public:” section, right?
True, a class should expose its responsibilities in its interface. But an interface can be defined by something more general than just the public section of a class declaration. This is what the Interface Principle does. It is explained in great details in Herb Sutter’s Exceptional C++ from Item 31 to 34, but its definition is essentially this:
A free function is part of a class interface if:
- it takes an object of the class type as a parameter,
- it is in the same namespace as the class,
- it is shipped with the class, meaning that it is declared in the same header as the class.
This is the case for the getPerimeter
and getArea
functions (here they are in a global namespace, but the next section adds namespaces to precisely see how this interacts with the Interface Principle). Therefore if you declare a function taking an object of the class type as a parameter, declared in the same namespace and header as a class, then your are expressing that this function is conceptually part of the class interface.
As a result, the only difference between a function and a method of the class interface becomes its invocation syntax:
getPerimeter(circle);
for the function, versus
circle.getPerimeter();
for the method. But beyond this difference, the Interface Principle implies that these two syntaxes express the same thing: invoking the getPerimeter
routine from the Circle
interface.
This lets us take code away from the class to improve encapsulation, while still preserving the semantics of the method.
The ADL: the Interface Principle playing nice with namespaces
With solely the above definition of the Interface Principle, there would be an issue with namespaces: calling non-member functions would have a burden over calling methods, because it would need to add the namespace to the invocation.
To illustrate, let’s put the interface of Circle
in a namespace, geometry
:
namespace geometry { class Circle { public: explicit Circle(double radius) : m_radius(radius) {} double getRadius() const {return m_radius;} private: double m_radius; }; double getPerimeter(Circle const& circle) {return 2 * Pi * circle.getRadius();} double getArea(Circle const& circle) {return Pi * m_radius * circle.getRadius();} } // end of namespace geometry
Then calling the function provided in the interface could be done the following way:
geometry::getArea(circle);
Compare this to the call to method:
circle.getArea();
This discrepancy is a problem, because the Interface Principle wants the method and the free function to be considered as semantically equivalent. Therefore you shouldn’t have to provide any additional information when calling the free function form. And the problem gets bigger in the case of nested namespaces.
This is solved by the Argument Dependent Lookup (ADL), also called Koenig lookup.
The ADL is a native C++ feature that brings all functions declared in the namespaces of the arguments types of the call to the scope of the functions searched for resolving the call. In the above example, circle
being an object of the type Circle
in the namespace geometry
, all free functions in this namespace are considered for resolving the function call. And this includes getArea
. So you can write the following code:
getArea(circle);
which therefore expresses just as much as what a human and a compiler need to understand what you mean.
Generic code
On the top of encapsulation, free functions let you do more flexible things than methods in cases of generic code.
We saw in the first section of this article that it was preferable to avoid adding the argument type in the function name, for code clarity. But having general names also makes it easier to create generic code. Imagine you had a class Rectangle
over which you can calculate a perimeter too:
double getPerimeter(Rectangle const& rectangle);
Then the getPerimeter
function can be used in generic code more easily than if it contained superfluous information about argument types in its name:
template <typename Shape> void operateOnShape(Shape const& shape) { double perimeter = getPerimeter(shape); .... }
Consider how much harder this would be to write such code with functions like getCirclePerimeter
and getRectanglePerimeter
.
Also, there are types on which you cannot add methods, because they are native C++ types for example, or because it is code that for some reason you don’t have the possibility to change. Then you can define free functions that accept these types as argument.
An example can be found in the STL with the functions (not methods) std::begin
and std::end
. These functions call the begin
and end
methods of their container arguments, and have a specific implementation for arrays (T[]
), because arrays don’t have begin
and end
methods. This lets you write generic code that can accept both containers and arrays indifferently.
A uniform function call syntax in C++?
The language already has features that facilitate benefiting from the Interface Principle. The ADL is one of them. And there seems to be a trend with new or future features to go in that direction.
std::invoke
allows to have exactly the same syntax for calling a function or a method. The following syntax:
std::invoke(f, x, x1, ..., xn);
- calls
f(x, x1, ..., xn)
if f is not a class method, - calls
x.f(x1, ..., xn)
if f is a class method.
std::invoke
becomes available in C++17.
Finally, there have been discussions around the proposal to implement this equivalence natively in the language, so that
f(x, x1, ..., xn);
calls x.f(x1, ..., xn)
if f is not a function but a method, and
x.f(x1, ..., xn);
calls f(x, x1, ..., xn)
if f is a not method but a free function. This is called the Unified Call Syntax, here is a description of it by Bjarne Stroustrup and Herb Sutter.
I don’t know if this particular proposal will make it to the standard one day, but one thing is sure: the language has been evolving and continues to evolve in that direction. Keeping this in mind when designing code makes it more natural, more robust, and more expressive.
Related articles:
Don't want to miss out ? Follow:   Share this post!