📜 ⬆️ ⬇️

Factory method without placement in dynamic memory

The classical implementation of the factory method in C ++ has one major drawback - the dynamic polymorphism used in the implementation of this pattern implies the placement of objects in dynamic memory. If at the same time the sizes of the objects created by the factory method are not large, but they are often created, this can negatively affect the performance. This is due to the fact that, in the first place, the new operator is not very efficient in allocating memory of small size, and secondly, the frequent allocation of small memory blocks in itself requires a lot of resources.
To solve this problem, it would be good to keep dynamic polymorphism (without it, the template cannot be implemented) and still allocate memory on the stack.
If you're wondering how I did it, welcome to cat.



One of the possible implementations of the classic factory method:
 #include <iostream> #include <memory> struct Base { static std::unique_ptr<Base> create(bool x); virtual void f() const = 0; virtual ~Base() { std::cout << "~Base()" << std::endl;} }; struct A: public Base { A() {std::cout << "A()" << std::endl;} virtual void f() const override {std::cout << "A::f\t" << ((size_t)this) << std::endl;} virtual ~A() {std::cout << "~A()" << std::endl;} }; struct B: public Base { B() {std::cout << "B()" << std::endl;} virtual void f() const override {std::cout << "B::f\t" << ((size_t)this) << std::endl;} virtual ~B() {std::cout << "~B()" << std::endl;} }; std::unique_ptr<Base> Base::create(bool x) { if(x) return std::unique_ptr<Base>(new A()); else return std::unique_ptr<Base>(new B()); } int main() { auto p = Base::create(true); p->f(); std::cout << "p addr:\t" << ((size_t)&p) << std::endl; return 0; } // compile & run: // g++ -std=c++11 1.cpp && ./a.out 

output:
 A() A::f 21336080 p addr: 140733537175632 ~A() ~Base() 

I think there is no need to comment. By address ranges, you can indirectly make sure that the created object is really located on the heap.
')
Now let's get rid of dynamic memory allocation.
As I said above, we assume that the objects created are small and the variant proposed below improves performance due to a small memory overflow.
 #include <iostream> #include <memory> struct Base { virtual void f() const = 0; virtual ~Base() { std::cout << "~Base()" << std::endl;} }; struct A: public Base {/* code here */}; struct B: public Base {/* code here */}; class BaseCreator { union U { A a; B b; }; public: BaseCreator(bool x) : _x(x) { if(x) (new(m) A()); else (new(m) B()); } ~BaseCreator() { if(_x) { reinterpret_cast<A*>(m)->A::~A(); } else { reinterpret_cast<B*>(m)->B::~B(); } } Base* operator->() { return reinterpret_cast<Base *>(m); } private: bool _x; unsigned char m[sizeof(U)]; }; int main(int argc, char const *argv[]) { BaseCreator p(true); p->f(); std::cout << "p addr:\t" << ((size_t)&p) << std::endl; return 0; } 

output:
 A() A::f 140735807769160 p addr: 140735807769160 ~A() ~Base() 

At the printed addresses, you can see that yes. The object is located on the stack.
The idea here is very simple: we take the union of objects that will create the factory method and with the help of it we will find out the size of the most capacious type. Then we allocate on the stack the memory of the desired size unsigned char m[sizeof(U)]; and with the help of the special form new place the object new(m) A() .
reinterpret_cast<A*>(m)->A::~A(); correctly destroys the object placed in the allocated memory.

In principle, this could have been stopped, but in the resulting solution I don’t like the fact that information about the created types in the BaseCreator class is present in three places. And if we need, so that our factory method would create objects of another type, we will have to synchronously make changes to all these three places. Moreover, in case of an error, the compiler will not say anything. Yes, and in runtime, the error may not immediately emerge. And if the types will not be 2-3, but 10-15, then it’s generally a disaster.

Let's try to improve our BaseCreator class.
 class BaseCreator { union U { A a; B b; }; public: BaseCreator(bool x) { if(x) createObj<A>(); else createObj<B>(); } ~BaseCreator() { deleter(m); } //   BaseCreator(const BaseCreator &) = delete; //   BaseCreator(BaseCreator &&) = default; Base* operator->() { return reinterpret_cast<Base *>(m); } private: typedef void (deleter_t)(void *); template<typename T> void createObj() { new(m) T(); deleter = freeObj<T>; } template<typename T> static void freeObj(void *p) { reinterpret_cast<T*>(p)->T::~T(); } unsigned char m[sizeof(U)]; deleter_t *deleter; }; 


Thus, there are not three, but two places that require editing when adding new types. Already better, but still not perfect. The main problem remained.
To solve this problem you need to get rid of the union. But at the same time maintain the visibility provided to them and the ability to determine the required size.

But what if we had a “smart union” that would not only know its size, but also allow us to dynamically create in it the objects of the types listed in this union? Well and at the same time, of course, would carry out type control.

No problems! This is C ++!
 template <typename ...Types> class TypeUnion { public: //     TypeUnion() {}; //   TypeUnion(const TypeUnion &) = delete; //   TypeUnion(TypeUnion &&) = default; ~TypeUnion() { //     -  //  ,   if(deleter) deleter(mem); } //     ""   T //    T          //   args    template <typename T, typename ...Args> void assign(Args&&... args) { //         "" static_assert ( usize, "TypeUnion is empty" ); static_assert ( same_as<T>(), "Type must be present in the types list " ); //      -    //  ,    . if(deleter) deleter(mem); //        //  ,     new(mem) T(std::forward<Args>(args)...); //       deleter = freeMem<T>; } //      ""  template<typename T> T* get() { static_assert ( usize, "TypeUnion is empty" ); assert ( deleter ); // TypeUnion::assign was not called return reinterpret_cast<T*>(mem); } private: //         typedef void (deleter_t)(void *); //      TypeUnion    ? static constexpr size_t max() { return 0; } //      static constexpr size_t max(size_t r0) { return r0; } template <typename ...R> static constexpr size_t max(size_t r0, R... r) { return ( r0 > max(r...) ? r0 : max(r...) ); } // is_same    template <typename T> static constexpr bool same_as() { return max( std::is_same<T, Types>::value... ); } //          template<typename T> static void freeMem(void *p) { reinterpret_cast<T*>(p)->T::~T(); } //          static constexpr size_t usize = max( sizeof(Types)... ); //  ,     unsigned char mem[usize]; deleter_t *deleter = nullptr; }; 


Now BaseCreator looks much nicer:
 class BaseCreator { TypeUnion<A, B> obj; public: BaseCreator(bool x) { if(x) obj.assign<A>(); else obj.assign<B>(); } //   BaseCreator(const BaseCreator &) = delete; //   BaseCreator(BaseCreator &&) = default; Base* operator->() { return obj.get<Base>(); } }; 

Now perfect. The record TypeUnion<A, B> obj clearer than union U {A a; B b;} union U {A a; B b;} . And an error with type mismatch will be caught at compile time.

Full sample code
 #include <iostream> #include <memory> #include <cassert> struct Base { virtual void f() const = 0; virtual ~Base() {std::cout << "~Base()\n";} }; struct A: public Base { A(){std::cout << "A()\n";} virtual void f() const override{std::cout << "A::f\n";} virtual ~A() {std::cout << "~A()\n";} }; struct B: public Base { B(){std::cout << "B()\n";} virtual void f() const override{std::cout << "B::f\n";} virtual ~B() {std::cout << "~B()\n";} size_t i = 0; }; template <typename ...Types> class TypeUnion { public: //     TypeUnion() {}; //   TypeUnion(const TypeUnion &) = delete; //   TypeUnion(TypeUnion &&) = default; ~TypeUnion() { //     -  //  ,   if(deleter) deleter(mem); } //     ""   T //    T          //   args    template <typename T, typename ...Args> void assign(Args&&... args) { //         "" static_assert ( usize, "TypeUnion is empty" ); static_assert ( same_as<T>(), "Type must be present in the types list " ); //      -    //  ,    . if(deleter) deleter(mem); //        //  ,     new(mem) T(std::forward<Args>(args)...); //       deleter = freeMem<T>; } //      ""  template<typename T> T* get() { static_assert ( usize, "TypeUnion is empty" ); assert ( deleter ); // TypeUnion::assign was not called return reinterpret_cast<T*>(mem); } private: //         typedef void (deleter_t)(void *); //      TypeUnion    ? static constexpr size_t max() { return 0; } //      static constexpr size_t max(size_t r0) { return r0; } template <typename ...R> static constexpr size_t max(size_t r0, R... r) { return ( r0 > max(r...) ? r0 : max(r...) ); } // is_same    template <typename T> static constexpr bool same_as() { return max( std::is_same<T, Types>::value... ); } //          template<typename T> static void freeMem(void *p) { reinterpret_cast<T*>(p)->T::~T(); } //          static constexpr size_t usize = max( sizeof(Types)... ); //  ,     unsigned char mem[usize]; deleter_t *deleter = nullptr; }; class BaseCreator { TypeUnion<A, B> obj; public: BaseCreator(bool x) { if(x) obj.assign<A>(); else obj.assign<B>(); } //   BaseCreator(const BaseCreator &) = delete; //   BaseCreator(BaseCreator &&) = default; Base* operator->() { return obj.get<Base>(); } }; int main(int argc, char const *argv[]) { BaseCreator p(false); p->f(); std::cout << "sizeof(BaseCreator):" << sizeof(BaseCreator) << std::endl; std::cout << "sizeof(A):" << sizeof(A) << std::endl; std::cout << "sizeof(B):" << sizeof(B) << std::endl; return 0; } // // clang++ -std=c++11 1.cpp && ./a.out 



Are there any rakes that I did not notice?

Thanks for attention!

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


All Articles