Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

how to implement the static interface pattern

NotificationsYou must be signed in to change notification settings

Bareflank/static_interface_pattern

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SIP provides the ability to implement the S.O.L.I.D. design principles in C++ without the need for virtual inheritance.

General Problem

Suppose we have the following class:

classA{public:voidfoo() { }};

Now suppose we wish to useA in another class as a private member variable as follows:

#include"a.h"classB{public:voidbar()    { m_a.foo(); }private:    A m_a;};

Finally, let us instantiateB in our application as follows:

#include"b.h"intmain(){    B b;    b.bar();return0;}

S.O.L.I.D

The above example is a simple demonstration of a class hierarchy that most C++ programmers have implemented at one point in time or another. Simply put, this example shows a class depending on another class.

The biggest issue with the above example is it doesn't adhere to the S.O.L.I.D design principles. The S.O.L.I.D design principles are a set of 5 principles designed to address different types of common problems found in projects that leverage object oriented programming. These 5 principles are as follows:

Of these 5, our example has issues with 3 of them.

Open–Closed Principle (OCP)

The general problem above does not adhere to the OCP asB is not open to extension.B directly depends onA, which means that any changes toA will changeB. In other words, there is no way to add functionality toA withoutB knowing about it. To fix this, we will need to provideB with an interface toA instead of directly relying onA itself.

Interface Segregation Principle (ISP)

Since we do not defineA's andB's responsibilities, we do not know ifA orB provide the needed level of abstraction to prevent clients from depending on interfaces they do not need. We can, however, still state that the general problem above does not adhere to the ISP as clients ofB are required to include the definition ofA, sinceB's definition includesA's definition. The larger a project gets, the more this type of problem will result in hard to debug dependency chains. To solve this problem, bothA andB will need their own interfaces that do not include the "details" of their implementations.

Dependency Inversion Principle (DIP)

The general problem above does not adhere to the DIP as bothA andB fail to depend on their own interfaces, meaning clients of bothA andB will have to depend on the "details" ofA andB instead of the interfaces ofA andB.

Dynamic Interfaces

Typically in C++, abstraction is implemented using pure virtual interfaces (i.e., dynamic interfaces) that leverage runtime polymorphism to separate the details of an object from its interface. For example:

classAInterface{public:virtual~AInterface() =default;virtualvoidfoo() = 0;};

The above pure virtual interface defines the interface forA. Using this interface,A can be defined as the following:

#include"a_interface.h"classA :    public AInterface{public:voidfoo()override { }};

As shown above,A now inherits our interface, and overrides the foo() function. To implementB, we must first define its interface as follows:

classBInterface{public:virtual~BInterface() =default;virtualvoidbar() = 0;};

With the above interface defined forB, we can now defineB as the following:

#include"b_interface.h"#include"a_interface.h"classB :    public BInterface{public:B(AInterface &a) :        m_a{a}    { }voidbar()override    { m_a.foo(); }private:    AInterface &m_a;};

As shown above,B now stores a reference to the interface ofA and not an instance ofA. Dynamic interfaces introduce an issue with ownership. Unlike our general problem (or static interfaces),B cannot createA as part of its own definition without breaking the OCP. The above example is one way to handle this, but there are many different ways to address this, including the factory pattern.

To useA andB, we can do the following:

#include"a.h"#include"b.h"intmain(){    A a;    Bb(a);    b.bar();return0;}

As shown above, we can see thatA andB now adhere to the S.O.L.I.D principles. SinceB only depends on the interface ofA,A can change without changingB. In addition, we can provideB with any version ofA we want meaning thatB is open to extension while closed to modifications. For example, we can implement a unit test ofB as follows:

#include"a_interface.h"#include"b.h"#include<iostream>classA_mock :    public AInterface{public:voidfoo()override    { std::cout <<"mocked foo\n"; }};intmain(){    A_mock a;    Bb(a);    b.bar();return0;}

As shown above, we can useA's interface to mockA, without making any modifications toB, meaning we now adhere to OCP. We also adhere to ISP asB no longer includes the definition ofA, only the interface, and we also adhere to DSP as bothA andB only depend on interfaces.

There are some issues with dynamic interfaces however. The first issue with dynamic interfaces is they add additional overhead. For example, with some tricks (can be seen in the source code examples) to ensure inlining is controlled, we can see the main function for the general problem looks like this following:

0000000000401020 <main>:  401020:e8 fb 00 00 00       callq  401120 <_ZN1A3fooEv.isra.0>  401025:31 c0                xor    %eax,%eax  401027:c3                   retq

The resulting code of this same logic using dynamic interfaces results in the following:

0000000000401040 <main>:  401040:48 83 ec 18          sub    $0x18,%rsp  401044:48 c7 44 24 08 60 20 movq   $0x402060,0x8(%rsp)  40104b:40 00  40104d:48 8d 7c 24 08       lea    0x8(%rsp),%rdi  401052:e8 f9 00 00 00       callq  401150 <_ZN1A3fooEv>  401057:31 c0                xor    %eax,%eax  401059:48 83 c4 18          add    $0x18,%rsp  40105d:c3                   retq

The extra logic seen above is needed to initialize the vTable ofA and get access to the function pointer tofoo(). Whether or not this overhead actually ends up becoming a problem for your applications depends on your application as modern compilers are amazing at removing the overhead of virtual inheritance.

The other issue with dynamic interfaces is they do not support static functions. Meaning, ifA defines a static function thatB needs to use, this entire scheme no longer works as there is no way to define a static function in a pure virtual interface.

Static Interfaces

The goal of the static interface pattern (SIP) is to address the issues of dynamic interfaces by implementing abstraction without the need for virtual inheritance. To accomplish this, we will use the following class:

template<template<typename>typename INTERFACE,typename DETAILS    >classtype :    public INTERFACE<type<INTERFACE, DETAILS>>{using details_type = DETAILS;    DETAILS d;friendclassINTERFACE<type<INTERFACE, DETAILS>>;constexprstatic DETAILS*details(INTERFACE<type<INTERFACE, DETAILS>> *i)    {return &(static_cast<type<INTERFACE, DETAILS> *>(i)->d); }constexprstaticconst DETAILS*details(const INTERFACE<type<INTERFACE, DETAILS>> *i)    {return &(static_cast<const type<INTERFACE, DETAILS> *>(i)->d); }};

The above class implements Static Polymorphism (also called Curiously Recurring Template Pattern). The difference is, the above class provides a means to define an object's interface and implementation separately and then combine different implementations with the same interface as needed, all at compile time.

To better understand how this class works, let us first look atA's interface as follows:

namespaceinterface{template<typename T>structA{constexprvoidfoo()    {T::details(this)->foo(); }};}

As shown above, the interface is defined using a template. We wrap the interface in an "interface" namespace so that the interface can be calledinterface::A. Each of the functions within the interface call into its own subtype using thedetails function, which returns a pointer to the subtype's private implementation.

With this interface,A is defined as follows:

#include"a_interface.h"namespacedetails{classA{public:voidfoo() { }};}using A = type<interface::A, details::A>;

As shown above,A is defined the same as our general problem. The only difference is, we wrap the definition ofA in a namespace called "details" allowing us to call the implementationdetails::A. From here, we use ourtype class to actually createA withusing A = type<interface::A, details::A>.

The next step is to define the interface forB as follows:

namespaceinterface{template<typename T>structB{constexprvoidbar()    {T::details(this)->bar(); }};}

As shown above, we use the same pattern as above to create our interface.B is define as follows:

#include"a_interface.h"#include"b_interface.h"namespacedetails{template<typename T>classB{public:voidbar()    { m_a.foo(); }private:    interface::A<T> m_a;};}template<typename T>using B = type<interface::B, details::B<T>>;

As shown above, the definition ofB is identical the the general problem with some exceptions. First,B is now a template class so that we can provideB with different versions ofA as needed. Instead of directly instantiatingA, we instantiateA using its interface using static polymorphism. LikeA,B is implemented in a "details" namespace, and defined using thetype class. The difference is thatB must remain a template type to ensure we can give it whateverA we want.

To use this code, we can do the following:

#include"a.h"#include"b.h"intmain(){    B<A> b;    b.bar();return0;}

Compared to dynamic interfaces, static interfaces solve the ownership issues asA is instantiated inB withoutB having to rely on the definition ofA. Static interfaces also support static functions as the interface can use thedetails_type type to access a static function, allowing the interface to override static functions (something dynamic interfaces cannot do).

With respect to performance, once again, this depends on your application. If we compare the resulting binary of our static interface with the resulting binary of our general problem (with --strip-all to remove strings), the binaries are byte-for-byte identical, meaning the compiler is able to take the above template code and reduce it to the same code as the general problem. This doesn't mean that static interfaces do not come at a cost as the applications string table is much larger with all of the additional decorations (so strip them), and the above code is far more difficult to understand without really knowing how templates work including static polymorphism, so there is a human cost to this abstraction (something C++20 will likely address with C++ Concepts)

For reference, mocking works as follows:

#include"a_interface.h"#include"b.h"#include<iostream>namespacedetails{classA_mock{public:voidfoo()    { std::cout <<"mocked foo\n"; }};}using A_mock = type<interface::A, details::A_mock>;intmain(){    B<A_mock> b;    b.bar();return0;}

In general, static interfaces address all of the problems that dynamic interfaces introduce when attempting to add abstraction and adhere to S.O.L.I.D at the expense of being more difficult to understand.


[8]ページ先頭

©2009-2025 Movatter.jp