📜 ⬆️ ⬇️

We separate the interface and implementation in the functional style in C ++

We separate the interface and implementation in the functional style in C ++


In C ++, header files are used to separate declarations of data structures (classes). They define the complete class structure, including private fields.
The reasons for this behavior are described in the excellent book Design and Evolution of C ++ by B. Straustrup.

We get a seemingly paradoxical situation: changes in the private private fields of a class require recompilation of all translation units (.cpp files) using only the external interface of the class. Of course, the reason for this lies in the need to know the size of the object when instantiating, but knowing the cause of the problem does not solve the problem itself.
')
We will try to use the power of modern C ++ to overcome this shortcoming. Interested please under the cat.

1. Introduction


To begin with, we will illustrate the thesis stated above once more. Let's say we have:

- Header file → interface1.h:

class A { public: void next_step(); int result_by_module(int m); private: int _counter; }; 

- Interface implementation → implementation1.cpp:

 #include "interface1.h" int A::result_by_module(int m) { return _counter % m; } void A::next_step() { ++_counter; } 

- cpp-file with the function main → main.cpp:

 #include "interface1.h" int main(int argc, char** argv) { A a; while (argc--) { a.next_step(); } return a.result_by_module(4); } 

In the header file, class A is defined, having a private _counter field. Up to this private field, only class methods have access and no one else (let's leave khaks, friend-s and other methods that violate encapsulation).

However, if we want to change the type of this field, we need to recompile both translation units: the implementation.cpp and main.cpp files. The member function is located in the implementation.cpp file, and a type A object is created on the stack in main.cpp.

This situation is understandable if we consider C ++ as a direct extension of the C language, i.e. macro assembler: you need to know the size of the object created on the stack.

But let's try to take a step forward and try to get rid of recompiling all translation units using the class definition.

2. Use PIMPL


The first thing that comes to mind is to use the PIMPL (Pointer to implementation) pattern.
But this pattern has a drawback: the need to write a wrapper for all class methods in this way (omit the additional complexity of memory management):

- interface2.h:

 class A_impl; class A { public: A(); ~A(); void next_step(); int result_by_module(int); private: A_impl* _impl; }; 

- implementation2.cpp:

 #include "interface2.h" class A_impl { public: A_impl(): _counter(0) {} void next_step() { ++_counter; } int result_by_module(int m) { return _counter % m; } private: int _counter; }; A::A(): _impl(new A_impl) {} A::~A() { delete _impl; } int A::result_by_module(int m) { return _impl->result_by_module(m); } void A::next_step() { _impl->next_step(); } 

3. We do the external interface on std :: function


Let's try to make this pattern “more functional” and untie the internal device of the class from its public interface.

For the external interface, we will use a structure with fields of type std :: function, storing methods. We also define a “virtual constructor” - a free function that returns a new object wrapped in a smart-pointer:

- interface3.h:

 struct A { std::function<int(int)> _result_by_module; std::function<void()> _next_couter; }; std::unique_ptr<A> create_A(); 

We got a completely, "galvanically", untied class interface. Time to think about the implementation.

Implementations begin in the free function - the virtual constructor.

 std::unique_ptr<A> create_A(int start_i) { std::unique_ptr<A> result(new A()); result->result_by_module_ = ??? result->next_counter_ = ??? return result; } 

How do we keep the internal state of object A? To do this, create a separate class that will describe the internal state of the external object, but will not be associated with it.

 struct A_context { int counter_; }; 

Thus, we have obtained the type of the object that will store the state and this type is in no way connected with the external interface!

Also, create a free static function __A_result_by_module, which will serve as the method. The function of the first argument will be an object of type A_context (more precisely, a smart-pointer; isn't it, it looks like python?). To narrow the scope, we place the function in an anonymous namespace:

 namespace { static int __A_result_by_module(std::shared_ptr<A_context> ctx, int m) { return ctx->counter_ % m; } } 

Let's go back to the create_A function. We use the function std :: bind to bind the C_context object and the function __A_result_by_module into a single unit.

For diversity, we implement the next_counter method without using a new function, but with the help of a lambda function.

 std::unique_ptr<A> create_A() { std::unique_ptr<A> result(new A()); auto ctx = std::make_shared<A_context>(); //   -    ctx->counter_ = 0; //   result->_result_by_module = std::bind( __A_result_by_module, ctx, std::placeholders::_1); result->_next_step = [ctx] () -> void { ctx->counter_++; }; return result; } 

4. Final example


Total, the code from the beginning of the article can now be rewritten as follows:

- interface.h:

 #include <functional> #include <memory> struct A { std::function<int(int)> _result_by_module; std::function<void()> _next_step; }; std::unique_ptr<A> create_A(); 

- implementation.cpp:

 #include "interface3.h" #include <memory> struct A_context { int counter_; }; namespace { static int __A_result_by_module(std::shared_ptr<A_context> ctx, int i) { return ctx->counter_ % i; } } std::unique_ptr<A> create_A() { std::unique_ptr<A> result(new A()); auto ctx = std::make_shared<A_context>(); ctx->counter_ = 0; result->_result_by_module = std::bind( __A_result_by_module, ctx, std::placeholders::_1); result->_next_step = [ctx] () -> void { ctx->counter_++; }; return result; } 

- main.cpp:

 #include "interface3.h" int main(int argc, char** argv) { auto a = create_A(); while (argc--) { a->_next_step(); } return a->_result_by_module(4); } 

4.1. A little bit about owning and managing memory


The ownership scheme of objects can be described as follows: an external interface object owns “methods” functors. The “methods” functors jointly own 1 object of internal state.

Thus, the lifetime of an external interface object determines the time for the release of internal state objects and functor objects. At the time of freeing the external interface object, functor objects will be freed. Since only objects-functors own the internal state object, when the last functor object is released, the object of the internal state will also be released.

5. Results


Thus, we managed to untie the internal state of the object from its external interface. Explicitly shared:

1. External interface:
- Uses an interface based on std :: function, not dependent on the internal state

2. The mechanism of generation of objects:
- Free function is used. This makes it easier to implement generating patterns.

3. The internal state of the object
- Used a separate class that describes the internal state of the object, the scope of which is completely within a single translation unit (cpp file).

4. Linking internal state and external interface
- Used lambda functions for small methods / getters / setters / ...
- The std :: bind function and free functions for methods with non-trivial logic are used.

In addition, the testability of the code within this code is higher, since it is now easier to write a unit-test for any method, since the method is just a free function.

Source: https://habr.com/ru/post/312148/


All Articles