📜 ⬆️ ⬇️

Development of interface classes in C ++


Interface classes are widely used in C ++ programs. But, unfortunately, when implementing solutions based on interface classes, mistakes are often made. The article describes how to properly design the interface classes, considered several options. The use of smart pointers is described in detail. An example implementation of an exception class and a collection class template based on interface classes is given.




Table of contents




Introduction


An interface class is a class that has no data and consists mainly of pure virtual functions. This solution allows you to completely separate the implementation from the interface — the client uses the interface class — a derived class is created elsewhere, in which pure virtual functions are redefined and the factory function is defined. Implementation details are completely hidden from the client. This way, true encapsulation is implemented, which is not possible when using a regular class. You can read about interface classes with Scott Meyers [Meyers2]. Interface classes are also called protocol classes.


The use of interface classes allows to weaken the dependencies between different parts of the project, which simplifies team development, reduces the compilation / assembly time. Interface classes make it easier to implement flexible, dynamic solutions when modules are loaded selectively during execution. Using interface classes as a library interface (API) (SDK) makes it easier to solve binary compatibility problems.


Interface classes are used quite widely, with their help they implement the library interface (API) (SDK), the plug-in interface, and much more. Many Gang of Four [GoF] patterns are naturally implemented using interface classes. Interface classes include COM interfaces. But, unfortunately, when implementing solutions based on interface classes, errors are often made. Let's try to clarify this issue.



1. Special member functions, creating and deleting objects


This section briefly describes a number of C ++ features that you need to know in order to fully understand the solutions offered for the interface classes.



1.1. Special member functions


If the programmer has not defined the class member functions from the following list — the default constructor, the copy constructor, the copy assignment operator, the destructor — then the compiler can do it for him. C ++ 11 added a relocation constructor and a move assignment operator to this list. These member functions are called special member functions. They are generated only if they are used, and additional conditions specific to each function are fulfilled. Note that this usage may be quite hidden (for example, when implementing inheritance). If the requested function cannot be generated, an error is generated. (With the exception of moving operations, they are replaced by copying ones.) The member functions generated by the compiler are public and embedded.


Special member functions are not inherited, if a special member function is required in a derived class, the compiler will always try to generate it, the presence of the corresponding member function in the base class determined by the programmer does not affect this.


The programmer can prohibit the generation of special member functions, in C ++ 11, you must apply the construction "=delete" when declaring, in C ++ 98, declare the corresponding member function closed and not define it. When inheriting classes, the prohibition of the generation of a special member function made in the base class applies to all derived classes.


If the programmer is satisfied with the member functions generated by the compiler, then in C ++ 11 he can designate this explicitly, rather than simply dropping the declaration. To do this, the declaration should use the construction "=default" , while the code is better readable and additional features related to access level control appear.


Details about special member functions can be found in [Meyers3].



1.2. Creating and deleting objects - basic details


Creating and deleting objects using new/delete operators is a typical two-in-one operation. When you call new , memory is first allocated for the object. If the selection is successful, the constructor is called. If the constructor throws an exception, the allocated memory is freed. When you call the delete operator, everything happens in reverse order: first the destructor is called, then the memory is freed. The destructor should not throw exceptions.


If the new operator is used to create an array of objects, memory is first allocated for the entire array. If the selection is successful, the default constructor is called for each element of the array, starting from zero. If any constructor throws an exception, then for all created elements of the array, the destructor is called in the reverse order of the constructor call, then the allocated memory is released. To delete an array, you must call the operator delete[] (called the operator delete for arrays), while for all elements of the array, the destructor is called in the reverse order of the constructor call, then the allocated memory is freed.


Attention! You must call the correct form of the delete operator, depending on whether a single object or an array is deleted. This rule must be observed strictly, otherwise you can get an undefined behavior, that is, anything can happen: memory leaks, abnormal termination, etc. See [Meyers2] for details.


Standard memory allocation functions, when it is impossible to satisfy the request, throw an exception of the type std::bad_alloc .


It is safe to apply any form of the delete operator to a null pointer.


In the above description, you need to make one clarification. For the so-called trivial types (built-in types, C-style structures), the constructor may not be called, and the destructor does not do anything in any case. See also section 1.6.



1.3. Destructor access level


When the delete operator is applied to a pointer to a class, the destructor of this class must be available at the point of the delete call. (There is some exception to this rule, discussed in Section 1.6.) Thus, by making the destructor protected or closed, the programmer prohibits the use of the delete operator where the destructor is not available. Recall that if a destructor is not defined in the class, the compiler will do it itself, and this destructor will be open (see section 1.1).



1.4. Creation and deletion in one module


If the new operator has created an object, then the call to the delete operator to delete it must be in the same module. Figuratively speaking, "put where you took." This rule is well known, see, for example, [Sutter / Alexandrescu]. If this rule is violated, a “discrepancy” of the functions of allocating and freeing memory may occur, which, as a rule, leads to the program termination.



1.5. Polymorphic deletion


If a polymorphic hierarchy of classes is being designed, the instances of which are deleted using the delete operator, then the base class must have an open virtual destructor, this guarantees that the destructor of the actual object type is called when the delete operator is applied to the pointer to the base class. If this rule is violated, a call to the base class destructor may occur, due to which resource leakage is possible.



1.6. Deletion on incomplete class declaration


Certain problems can be created by the “omnivorous nature” of the operator delete , it can be applied to a pointer of type void* or to a pointer to a class that has an incomplete (proactive) declaration. In this case, the error does not occur, simply the call to the destructor is skipped, only the function of freeing the memory is called. Consider an example:


 class X; //   X* CreateX(); void Foo() {    X* p = CreateX();    delete p; } 

This code is compiled, even if the full declaration of class X not available at the point of the call to delete . True, when compiling (Visual Studio), a warning is issued:


warning C4150: deletion of pointer to incomplete type 'X'; no destructor called


If there is an X implementation and CreateX() , then the code is assembled, if CreateX() returns a pointer to the object created by the new operator, the Foo() call succeeds, and the destructor is not called. It is clear that this can lead to a drain on resources, so once again the need to carefully consider warnings.


This situation is not contrived, it can easily arise when using classes such as smart pointer or classes-descriptors. Scott Meyers deals with this problem in [Meyers3].



2. Pure virtual functions and abstract classes


The concept of interface classes is based on such C ++ concepts as pure virtual functions and abstract classes.



2.1. Pure virtual functions


A virtual function declared using the "=0" construct is called purely virtual.


 class X { // ...    virtual void Foo() = 0; }; 

Unlike a conventional virtual function, a pure virtual function can not be defined (except for the destructor, see section 2.3), but it must be redefined in one of the derived classes.


Pure virtual functions can be defined. The Sutter coat of arms offers several useful uses for this feature [Shutter].



2.2. Abstract classes


An abstract class is a class that has at least one purely virtual function. A class derived from an abstract class and not redefining at least one purely virtual function will also be abstract. The C ++ standard prohibits creating instances of an abstract class; only instances of derived non-abstract classes can be created. Thus, an abstract class is created to be used as a base class. Accordingly, if a constructor is defined in an abstract class, then it does not make sense to make it open, it must be protected.



2.3. Pure virtual destructor


In some cases, it is advisable to make a destructor purely virtual. But this solution has two features.


  1. A pure virtual destructor must be defined. (Typically, the default definition is used, that is, using the "=default" construct.) The destructor of the derived class calls the destructors of the base classes along the entire inheritance chain and, therefore, the queue is guaranteed to reach the root - a pure virtual destructor.
  2. If the programmer has not redefined the pure virtual destructor in the derived class, the compiler will do it for him (see Section 1.1). Thus, a class derived from an abstract class with a purely virtual destructor may lose its abstractness without explicitly redefining the destructor.

An example of using a pure virtual destructor can be found in section 4.4.



3. Interface classes


An interface class is an abstract class that has no data and consists mainly of pure virtual functions. Such a class may have ordinary virtual functions (not purely virtual), for example, a destructor. There may also be static member functions, such as factory functions.



3.1. Implementations


The implementation of the interface class will be called the derived class in which pure virtual functions are redefined. There can be several implementations of the same interface class, with two possible schemes: horizontal, when several different classes inherit the same interface class, and vertical, when the interface class is the root of the polymorphic hierarchy. Of course, there may be hybrids.


The key point of the concept of interface classes is the complete separation of the interface from the implementation - the client works only with the interface class, the implementation is not available to it.



3.2. Object creation


The inaccessibility of the implementation class causes certain problems when creating objects. The client must create an instance of the implementation class and get a pointer to the interface class through which the object will be accessed. Since the implementation class is not available, the constructor cannot be used, so the factory function defined on the implementation side is used. This function usually creates an object using the new operator and returns a pointer to the created object, which is cast to a pointer to an interface class. A factory function can be a static member of an interface class, but this is not necessary; for example, it can be a member of a special factory class (which, in turn, can be an interface class itself) or a free function. The factory function can return not an intelligent pointer to the interface class, but an intelligent one. This option is discussed in sections 3.3.4 and 4.3.2.



3.3. Deleting an object


Object deletion is an extremely important operation. In case of an error, there is either a memory leak or a double deletion, which usually leads to a program crash. Below this issue is considered in as much detail as possible, with much attention being paid to the prevention of erroneous actions by the client.


There are four main options:


  1. Use the delete operator.
  2. Using a special virtual function.
  3. Using the external function.
  4. Automatic deletion using the smart pointer.


3.3.1. Using the delete operator


To do this, you must have an open virtual destructor in the interface class. In this case, the operator delete , called for a pointer to the interface class on the client side, provides a call to the destructor of the implementation class. This option may work, but it is difficult to recognize successful ones. We receive calls to the new and delete operators on opposite sides of the barrier, new on the implementation side, delete on the client side. And if the implementation of the interface class is done in a separate module (which is quite a common thing), then we get a violation of the rule in section 1.4.



3.3.2. Using a special virtual function


Another option is more progressive: the interface class must have a special virtual function that deletes the object. Such a function, in the end, comes down to calling delete this , but this happens already on the implementation side. Such a function can be called differently, for example, Delete() , but other options are used: Release() , Destroy() , Dispose() , Free() , Close() , etc. In addition to observing the rule in section 1.4, this option has several additional benefits.


  1. Allows the use of user-defined memory allocation / release functions for the implementation class.
  2. Allows you to implement a more complex scheme for controlling the lifetime of the object of implementation, for example, using the reference counter.

In this embodiment, an attempt to delete an object using the delete operator can be compiled and even executed, but this is an error. To prevent it, it is enough to have an empty or purely virtual protected destructor in the interface class (see section 1.3). Note that the use of the delete operator can be quite strongly disguised, for example, the standard smart pointers to delete an object by default use the delete operator and the corresponding code is deeply buried in their implementation. The protected destructor allows to detect all such attempts at the compilation stage.



3.3.3. Using the external function


This option may involve a certain symmetry of the procedures for creating and deleting an object, but in reality it has no advantages over the previous version, but there are many additional problems. This option is not recommended for use and is not considered further.



3.3.4. Automatic deletion using the smart pointer


In this case, the factory function returns not the raw pointer to the interface class, but the corresponding smart pointer. This smart pointer is created on the implementation side and encapsulates an object remover that automatically deletes the implementation object when the smart pointer (or its last copy) goes out of scope on the client side. In this case, a special virtual function may not be necessary to delete the implementation object, but the protected destructor is still needed; it is necessary to prevent the erroneous use of the delete operator. (However, it should be noted that the probability of such an error is markedly reduced.) This option is discussed in more detail in Section 4.3.2.



3.4. Other options for controlling the lifetime of an instance of an implementation class


In some cases, the client may receive a pointer to the interface class, but not own it. Managing the lifetime of the object of implementation is completely on the implementation side. For example, an object can be a static singleton object (such a decision is typical for factories). Another example is related to bidirectional interaction, see section 3.7. The client should not delete such an object, but a protected destructor for such an interface class is needed, it is necessary to prevent the erroneous use of the delete operator.



3.5. Copy semantics


For an interface class, creating a copy of an implementation object using a copy constructor is impossible, so if copying is required, then the class must have a virtual function that creates a copy of the implementation object and returns a pointer to the interface class. Such a function is often called a virtual constructor, and its traditional name is Clone() or Duplicate() .


Using the copy assignment operator is not prohibited, but cannot be considered a good idea. The copy assignment operator is always paired; it must be paired with the copy constructor. The operator generated by the compiler by default is meaningless, it does nothing. Theoretically, you can declare an assignment statement as purely virtual and then redefine, but virtual assignment is not a recommended practice, details can be found in [Meyers1]. Moreover, the assignment looks very unnatural: access to objects of the implementation class is usually done through a pointer to the interface class, so the assignment will look like this:


 * = *; 

It is best to prohibit the assignment operator and, if necessary, similar semantics, to have a corresponding virtual function in the interface class.


There are two ways to prohibit assignment.


  1. Declare the assignment operator to be deleted ( =delete ). If the interface classes form a hierarchy, then this is sufficient to do in the base class. The disadvantage of this method is that it affects the class of implementation, the prohibition applies to it.
  2. Declare a protected assignment statement with a default definition ( =default ). This does not affect the implementation class, but in the case of a hierarchy of interface classes, such an declaration must be made in each class.


3.6. Interface Class Designer


Often, the interface class constructor is not declared. In this case, the compiler generates a default constructor, which is necessary to implement inheritance (see Section 1.1). This constructor is open, although it is enough that it is protected. If in the interface class the copying constructor is declared remote ( =delete ), then the generation of the default constructor by the compiler is suppressed, and you must explicitly declare such a constructor. It is natural to make it protected with a default definition ( =default ). In principle, the declaration of such a protected constructor can always be done. An example is in section 4.4.



3.7. Bidirectional interaction


Interface classes are useful for organizing bidirectional interaction. If some module is available through interface classes, then the client can also create implementations of some interface classes and pass pointers to them into the module. Through these pointers, the module can receive services from the client as well as transmit data or notifications to the client.



3.8. Smart Pointers


Since access to objects of the implementation class is usually done through a pointer, it is natural to use intelligent pointers to control their lifetime. But it should be borne in mind that if the second option of deleting objects is used, then a standard smart pointer needs to be passed a user deleter (type) or an instance of this type. If this is not done, then the delete pointer will be used by the delete operator to remove the object, and the code simply will not be compiled (thanks to the protected destructor). Standard smart pointers (including the use of custom deleters) are discussed in detail in [Josuttis], [Meyers3]. An example of using a custom remover can be found in section 4.3.1.


If the interface class supports the reference counter, then it is advisable to use not standard intelligent pointers, but specially written for such a case, it is quite easy to do.



3.9. Constant member functions


You should carefully declare the member functions of the interface classes as const. One of the important advantages of interface classes is the possibility of separating the interface from the implementation as completely as possible, but the limitations associated with the constancy of a member function can create problems when developing an implementation class.



3.10. COM interfaces


COM- , , COM — , COM- , C, , . COM- C++ , COM.



3.11.


(API) (SDK). . -, -, . , (Windows DLL), : -. . , , . LoadLibrary() , -, .



4.



4.1.


, .


 class IBase { protected:    virtual ~IBase() = default; //   public:    virtual void Delete() = 0; //      IBase& operator=(const IBase&) = delete; //   }; 

.


 class IActivatable : public IBase { protected:    ~IActivatable() = default; //   public:    virtual void Activate(bool activate) = 0;    static IActivatable* CreateInstance(); // - }; 

, , . , IBase . , (. 1.3). , .



4.2.


 class Activator : private IActivatable { // ... private:    Activator(); protected:    ~Activator(); public:    void Delete() override;    void Activate(bool activate) override;    friend IActivatable* IActivatable::CreateInstance(); }; Activator::Activator() {/* ... */} Activator::~Activator() {/* ... */} void Activator::Delete() { delete this; } void Activator::Activate(bool activate) {/* ... */} IActivatable* IActivatable::CreateInstance() {    return static_cast<IActivatable*>(new Activator()); } 

, , , - , .



4.3.



4.3.1.


. - ( IBase ):


 struct BaseDeleter {    void operator()(IBase* p) const { p->Delete(); } }; 

std::unique_ptr<> - :


 template <class I> // I —  IBase using UniquePtr = std::unique_ptr<I, BaseDeleter>; 

, , - , UniquePtr .


-:


 template <class I> // I —  - CreateInstance() UniquePtr<I> CreateInstance() {    return UniquePtr<I>(I::CreateInstance()); } 

:


 template <class I> // I —  IBase UniquePtr<I> ToPtr(I* p) {    return UniquePtr<I>(p); } 

std::shared_ptr<> std::unique_ptr<> , , std::shared_ptr<> . Activator .


 auto un1 = CreateInstance<IActivatable>(); un1->Activate(true); auto un2 = ToPtr(IActivatable::CreateInstance()); un2->Activate(true); std::shared_ptr<IActivatable> sh = CreateInstance<IActivatable>(); sh->Activate(true); 

( — -):


 std::shared_ptr<IActivatable> sh2(IActivatable::CreateInstance()); 

std::make_shared<>() , ( ).


: , . : , - . 4.4.



4.3.2.


. -. std::shared_ptr<> , , ( ). std::shared_ptr<> ( ) - , delete . std::shared_ptr<> - ( ) - . .


 #include <memory> class IActivatable; using ActPtr = std::shared_ptr<IActivatable>; //   class IActivatable { protected:    virtual ~IActivatable() = default; //      IActivatable& operator=(const IActivatable&) = default; //   public:    virtual void Activate(bool activate) = 0;    static ActPtr CreateInstance(); // - }; //   class Activator : public IActivatable { // ... public:    Activator();  //      ~Activator(); //      void Activate(bool activate) override; }; Activator::Activator() {/* ... */} Activator::~Activator() {/* ... */} void Activator::Activate(bool activate) {/* ... */} ActPtr IActivatable::CreateInstance() {    return ActPtr(new Activator()); } 

- std::make_shared<>() :


 ActPtr IActivatable::CreateInstance() {    return std::make_shared<Activator>(); } 

std::unique_ptr<> , , - , .



4.4.


C# Java C++ «», . . IBase .


 class IBase { protected:    IBase() = default;    virtual ~IBase() = 0; // ,       virtual void Delete(); //   public:    IBase(const IBase&) = delete;            //      IBase& operator=(const IBase&) = delete; //      struct Deleter        // -    {        void operator()(IBase* p) const { p->Delete(); }    };    friend struct IBase::Deleter; }; 

, Delete() , .


 IBase::~IBase() = default; void IBase::Delete() { delete this; } 

IBase . Delete() , . - IBase . Delete() , - . Delete() , . , 4.3.1.



5. ,



5.1


, , , , .


, , IException Exception .


 class IException {    friend class Exception;    virtual IException* Clone() const = 0;    virtual void Delete() = 0; protected:    virtual ~IException() = default; public:    virtual const char* What() const = 0;    virtual int Code() const = 0;    IException& operator=(const IException&) = delete; }; class Exception {    IException* const m_Ptr; public:    Exception(const char* what, int code);    Exception(const Exception& src) : m_Ptr(src.m_Ptr->Clone()) {}    ~Exception() { m_Ptr->Delete(); }    const IException* Ptr() const { return m_Ptr; } }; 

Exception , IException . , throw , . Exception , . - , .


Exception , , .


IException :


 class ExcImpl : IException {    friend class Exception;    const std::string m_What;    const int m_Code;    ExcImpl(const char* what, int code);    ExcImpl(const ExcImpl&) = default;    IException* Clone() const override;    void Delete() override; protected:    ~ExcImpl() = default; public:    const char* What() const override;    int Code() const override; }; ExcImpl::ExcImpl(const char* what, int code)    : m_What(what), m_Code(code) {} IException* ExcImpl::Clone() const { return new ExcImpl(*this); } void ExcImpl::Delete() { delete this; } const char* ExcImpl::What() const { return m_What.c_str(); } int ExcImpl::Code() const { return m_Code; } 

Exception :


 Exception::Exception(const char* what, int code)    : m_Ptr(new ExcImpl(what, code)) {} 

, — .NET — , — , C++/CLI. , , , C++/CLI.



5.2


- :


 template <typename T> class ICollect { protected:    virtual ~ICollect() = default; public:    virtual ICollect<T>* Clone() const = 0;    virtual void Delete() = 0;    virtual bool IsEmpty() const = 0;    virtual int GetCount() const = 0;    virtual T& GetItem(int ind) = 0;    virtual const T& GetItem(int ind) const = 0;    ICollect<T>& operator=(const ICollect<T>&) = delete; }; 

, -, .


 template <typename T> class ICollect; template <typename T> class Iterator; template <typename T> class Contain {    typedef ICollect<T> CollType;    CollType* m_Coll; public:    typedef T value_type;    Contain(CollType* coll);    ~Contain(); //     Contain(const Contain& src);    Contain& operator=(const Contain& src); //     Contain(Contain&& src);    Contain& operator=(Contain&& src);    bool mpty() const;    int size() const;    T& operator[](int ind);    const T& operator[](int ind) const;    Iterator<T> begin();    Iterator<T> end(); }; 

. , . , , , , - begin() end() , . (. [Josuttis]), for . . , , .



6. -


. -, . . , ++. , .NET, Java Pyton. . , , . .NET Framework C++/CLI C++. .



7.


-, .


.


  1. delete .
  2. .
  3. .

.


, delete . , .


- , . , , delete .


.


, , , , .




Bibliography


List

[GoF]
., ., ., . - . .: . from English — .: , 2001.


[Josuttis]
, . C++: , 2- .: . from English — .: «.. », 2014.


[Dewhurst]
, . C++. .: . from English — .: , 2012.


[Meyers1]
, . C++. 35 .: . from English — .: , 2000.


[Meyers2]
, . C++. 55 .: . from English — .: , 2014.


[Meyers3]
, . C++: 42 C++11 C++14.: . from English — .: «.. », 2016.


[Sutter]
, . C++.: . from English — : «.. », 2015.


[Sutter/Alexandrescu]
, . , . ++.: . from English — .: «.. », 2015.





')

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


All Articles