用C ++开发接口类


接口类在C ++程序中非常广泛地使用。 但是,不幸的是,在实现基于接口类的解决方案时经常会犯错误。 本文介绍了如何正确设计接口类;考虑了几个选项。 详细介绍了智能指针的使用。 给出了一个基于接口类的异常类和集合类模板的实现示例。




目录




引言


接口类是没有数据的类,主要由纯虚函数组成。 通过此解决方案,您可以将实现与接口完全分开(客户端使用接口类),而在另一个位置创建派生类,在该派生类中将重新定义纯虚函数并定义工厂函数。 实施细节对客户端完全隐藏。 这样,就实现了真正的封装,这对于通常的类是不可能的。 您可以从Scott Meyers [Meyers2]中了解有关接口类的信息。 接口类也称为协议类。


使用接口类可以削弱项目不同部分之间的依赖性,从而简化团队开发并减少编译/组装时间。 当在运行时有选择地加载模块时,接口类使实现灵活,动态的解决方案更加容易。 使用接口类作为接口(API)库(SDK)可以简化二进制兼容性问题的解决方案。


接口类的用途非常广泛,借助它们的帮助,它们可以实现库(SDK)的接口(API),插件(插件)的接口等等。 使用接口类自然可以实现许多四个[GoF]模式。 接口类包括COM接口。 但是,不幸的是,在实现基于接口类的解决方案时经常会犯错误。 让我们尝试澄清这个问题。



1.特殊的成员函数,创建和删除对象


本节简要介绍了许多C ++功能,您需要了解这些功能才能完全理解为接口类提供的解决方案。



1.1。 特殊会员职能


如果程序员尚未从以下列表中定义该类的成员函数-默认构造函数,复制构造函数,复制赋值运算符,析构函数-则编译器可以为他执行此操作。 C ++ 11在此列表中添加了移动构造函数和移动赋值运算符。 这些成员函数称为特殊成员函数。 仅在使用它们并且满足每个功能特定的附加条件时才生成它们。 我们提请注意以下事实:这种用法可能被隐藏起来(例如,在实现继承时)。 如果无法生成所需的功能,则会生成错误。 (除重定位操作外,它们均由复制操作代替。)编译器生成的成员函数是公共的并且是可嵌入的。


特殊成员函数不会被继承,如果派生类中需要特殊成员函数,则编译器将始终尝试生成该特殊成员函数;程序员在基类中定义的相应成员函数的存在不会对此产生影响。


程序员可以禁止生成特殊的成员函数,在C ++ 11中声明时,必须使用"=delete"构造,在C ++ 98中,声明相应的成员函数为private而不定义。 在类继承中,禁止在基类中生成特殊成员函数的情况适用于所有派生类。


如果程序员对编译器生成的成员函数感到满意,那么在C ++ 11中,他可以明确地指出这一点,而不仅仅是删除声明。 为此,在声明时必须使用"=default"构造,这样可以更好地阅读代码,并且出现与管理访问级别有关的其他功能。


特殊成员功能的详细信息可以在[Meyers3]中找到。



1.2。 创建和删除对象-基本详细信息


使用new/delete运算符创建和删除对象是典型的二合一操作。 调用new ,首先为该对象分配内存。 如果选择成功,则调用构造函数。 如果构造函数引发异常,则释放分配的内存。 当调用delete运算符时,一切都以相反的顺序进行:首先,调用析构函数,然后释放内存。 析构函数不应引发异常。


如果使用new运算符创建对象数组,则首先为整个数组分配内存。 如果选择成功,则将为数组的每个元素(从零开始)调用默认构造函数。 如果任何构造函数引发异常,则对于数组的所有已创建元素,将以构造函数调用的相反顺序调用析构函数,然后释放分配的内存。 若要删除数组,必须调用delete[]运算符(对于数组称为delete运算符),对于数组的所有元素,将以构造函数调用的相反顺序调用析构函数,然后释放分配的内存。


注意! 您必须根据delete单个对象还是数组来调用delete运算符的正确形式。 必须严格遵守此规则,否则您将获得未定义的行为,即可能发生任何事情:内存泄漏,崩溃等。 有关详细信息,请参见[Meyers2]。


标准内存分配函数std::bad_alloc满足请求,并抛出std::bad_alloc类型的异常。


将任何形式的delete运算符应用于空指针都是安全的。


在以上描述中,需要进行澄清。 对于所谓的琐碎类型(内置类型,C样式结构),可能不会调用构造函数,并且析构函数在任何情况下均不执行任何操作。 另请参阅第1.6节。



1.3。 析构函数访问级别


当将delete运算符应用于指向类的指针时,该类的析构函数必须在delete调用点可用。 (此规则有一些例外,将在1.6节中讨论。)因此,通过使析构函数安全或关闭,程序员可以在析构函数不可用的情况下禁止使用delete运算符。 回想一下,如果在类中未定义析构函数,则编译器将自行执行此操作,并且此析构函数将打开(请参见1.1节)。



1.4。 在一个模块中创建和删除


如果new运算符创建了一个对象,则delete运算符必须在同一模块中才能delete它。 形象地说,“把它放到哪里去”。 该规则是众所周知的;例如,参见[Sutter / Alexandrescu]。 如果违反此规则,则可能会发生分配和释放内存的功能“不匹配”,这通常会导致程序崩溃。



1.5。 多态删除


如果要设计类的多态层次结构,其类的实例使用delete运算符delete ,则在基类中必须有一个开放的虚拟析构函数,这可确保在将delete运算符应用于指向基类的指针时, delete调用对象实际类型的析构函数。 如果违反此规则,则可能会发生对基类析构函数的调用,这可能导致资源泄漏。



1.6。 类声明不完整时删除


delete运算符的杂项性可能会导致某些问题;可以将其应用于void*类型的指针或具有不完整(抢先)声明的类的指针。 在这种情况下,不会发生错误,只是跳过对析构函数的调用,仅调用释放内存的函数。 考虑一个例子:


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

即使完整的X类声明在delete拨号对等方不可用,该代码也会编译。 是的,在编译(Visual Studio)时会发出警告:


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


如果存在XCreateX() ,则将CreateX()代码,如果CreateX()返回指向由new运算符创建的对象的指针,则调用Foo()成功执行,则不调用析构函数。 显然,这可能会导致资源的消耗,因此,再次需要注意警告。


这种情况并非易事,当使用诸如智能指针或描述符类之类的类时,很容易出现这种情况。 Scott Meyers在[Meyers3]中处理了此问题。



2.纯粹的虚函数和抽象类


接口类的概念基于纯虚拟函数和抽象类等C ++概念。



2.1。 纯虚拟功能


使用"=0"构造声明的虚函数称为纯虚函数。


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

与常规虚函数不同,纯虚函数无法定义(析构函数除外,请参见第2.3节),但必须在派生类之一中对其进行重新定义。


可以定义纯虚函数。 Emblem Sutter为此功能提供了一些有用的用途[Shutter]。



2.2。 抽象类


抽象类是至少具有一个纯虚函数的类。 从抽象类派生并且不覆盖至少一个纯虚函数的类也将是抽象的。 C ++标准禁止创建抽象类的实例;您只能创建非抽象类的派生实例。 因此,创建了一个抽象类以用作基类。 因此,如果在抽象类中定义了构造函数,则将其打开是没有意义的,必须对其进行保护。



2.3。 纯虚拟析构函数


在某些情况下,建议制作一个纯虚拟析构函数。 但是此解决方案具有两个功能。


  1. 必须定义一个纯虚拟的析构函数。 (通常使用默认定义,即使用"=default"构造。)派生类析构函数沿整个继承链调用基类析构函数,因此,可以确保队列到达根-纯虚拟析构函数。
  2. 如果程序员没有在派生类中重新定义纯虚拟析构函数,则编译器将为他做这件事(请参见1.1节)。 因此,从具有纯虚拟析构函数的抽象类派生的类可能会失去其抽象性,而不会显式覆盖析构函数。

在第4.4节中可以找到使用纯虚拟析构函数的示例。



3.接口类


接口类是没有数据的抽象类,主要由纯虚函数组成。 这样的类可以具有普通的虚函数(不是纯虚函数),例如析构函数。 也可能有静态成员函数,例如工厂函数。



3.1。 实作


接口类的实现将称为派生类,在派生类中将重新定义纯虚函数。 同一接口类可以有多种实现,并且有两种方案是可能的:水平(当几个不同的类继承相同的接口类时)和垂直(当接口类是多态层次结构的根时)。 当然,可能会有杂种。


接口类概念的关键是接口与实现的完全隔离-客户端仅与接口类一起使用,实现不可用。



3.2。 对象创建


实现类的不可访问性在创建对象时引起某些问题。 客户端必须创建实现类的实例,并获得指向将通过其访问对象的接口类的指针。 由于实现类不可用,因此无法使用构造函数,因此,将使用在实现端定义的工厂函数。 此函数通常使用new运算符创建一个对象,并返回指向创建的对象的指针,并将其转换为指向接口类的指针。 工厂函数可以是接口类的静态成员,但不是必须的,例如,它可以是特殊工厂类(本身又可以是接口类)的成员或自由函数。 工厂函数不能返回指向接口类的原始指针,而可以返回智能指针。 此选项在3.3.4和4.3.2节中讨论。



3.3。 删除物件


移除对象是非常关键的操作。 错误会导致内存泄漏或双重删除,通常会导致程序崩溃。 下面的问题被认为是尽可能详细的,并且要特别注意防止错误的客户行为。


有四个主要选项:


  1. 使用delete运算符。
  2. 使用特殊的虚拟功能。
  3. 使用外部功能。
  4. 使用智能指针自动删除。


3.3.1。 使用delete运算符


为此,您必须在接口类中有一个开放的虚拟析构函数。 在这种情况下,需要在客户端上调用指向接口类的指针的delete操作符提供对实现类的析构函数的调用。 此选项可能有效,但很难将其识别为成功。 我们在“屏障”的不同侧收到newdelete运算符的调用,在实现侧是delete ,在客户端是delete 。 而且,如果接口类的实现是在单独的模块中完成的(这是很常见的事情),那么我们将违反第1.4节中的规则。



3.3.2。 使用特殊的虚拟功能


更具进步性的另一个选择是:接口类必须具有删除对象的特殊虚函数。 最终,这样的功能归结为调用delete this ,但这已经在实现方面发生。 可以以不同的方式调用此函数,例如Delete() ,但也使用其他选项: Release()Destroy()Dispose()Free()Close()等。 除了遵守第1.4节中的规则之外,此选项还具有其他一些优点。


  1. 允许您将用户定义的内存分配/释放函数用于实现类。
  2. 允许您实现更复杂的方案来控制实现对象的生存期,例如使用引用计数器。

在该实施例中,可以编译甚至执行使用delete操作符删除对象的尝试,但这是错误的。 为了防止在接口类中使用它,只要有一个空的或纯虚拟的受保护的析构函数就足够了(请参阅第1.3节)。 请注意, delete操作符的使用可能被掩盖了,例如,默认情况下,标准智能指针使用delete操作符删除对象,并且相应的代码已深深地埋在其实现中。 受保护的析构函数使您可以在编译阶段检测所有此类尝试。



3.3.3。 使用外部功能


此选项可能会吸引创建和删除对象的过程的某种对称性,但实际上,它比以前的版本没有优势,但是还有许多其他问题。 不建议使用此选项,以后也不会考虑使用此选项。



3.3.4。 使用智能指针自动删除


在这种情况下,工厂函数不会返回指向接口类的原始指针,而是返回对应的智能指针。 该智能指针在实现侧创建,并封装删除对象,当智能指针(或其最后一个副本)超出客户端范围时,删除对象将自动删除实现对象。 在这种情况下,可能不需要用于删除实现对象的特殊虚拟函数,但是仍然需要一个受保护的析构函数,因此必须防止错误使用delete运算符。 (正确的是,应该显着降低此类错误的可能性。)此选项在4.3.2节中有更详细的讨论。



3.4。 其他用于管理实现类实例的生存期的选项


在某些情况下,客户端可能会收到指向该接口类的指针,但不是该接口类的拥有者。 实现对象生命周期的管理完全在实现方面。 例如,对象可以是静态单例对象(此解决方案对于工厂来说是典型的)。 另一个示例与双向交互有关,请参见第3.7节。 客户端不应删除此类对象,但是需要此类接口类的受保护析构函数,因此有必要防止错误使用delete运算符。



3.5。 复制语义


对于接口类,无法使用copy构造函数创建实现对象的副本,因此,如果需要复制,则该类必须具有一个虚拟函数,该函数创建实现对象的副本并返回指向接口类的指针。 此类函数通常称为虚拟构造函数,其传统名称为Clone()Duplicate()


禁止使用副本分配运算符,但不能认为这是一个好主意。 复制分配运算符始终配对;它必须与复制构造函数配对。 默认编译器生成的运算符是没有意义的;它什么也不做。 从理论上讲,可以在随后的重新定义中声明一个纯虚拟的赋值运算符,但不建议使用虚拟赋值;有关详细信息,请参见[Meyers1]。 而且,分配看起来非常不自然:通常通过指向接口类的指针来访问实现类的对象,因此分配看起来像这样:


 * = *; 

最好禁止赋值运算符,并且在必要时,此类语义在接口类中具有对应的虚函数。


有两种禁止分配的方法。


  1. 声明分配运算符已删除( =delete )。 如果接口类形成层次结构,则足以在基类中完成。 这种方法的缺点是它影响实现类,该禁令也适用于它。
  2. 用默认定义( =default )声明一个受保护的赋值语句。 这不会影响实现类,但是在接口类层次结构的情况下,必须在每个类中进行此类声明。


3.6。 接口类构造函数


通常,不声明接口类的构造函数。 在这种情况下,编译器会生成实现继承所需的默认构造函数(请参见1.1节)。 该构造函数是开放的,尽管足够安全。 如果在接口类中将复制的构造函数声明为delete( =delete ),则默认情况下禁止该构造函数的编译器生成,并且必须显式声明此类构造函数。 使用默认定义( =default )使其安全是很自然的。 原则上,这样受保护的构造函数的声明总是可以完成的。 第4.4节提供了一个示例。



3.7。 双向互动


接口类对于使用双向通信很方便。 如果可以通过接口类访问某些模块,则客户端还可以创建某些接口类的实现,并在模块中传递指向它们的指针。 通过这些指针,模块可以从客户端接收服务,还可以向客户端发送数据或通知。



3.8。 智能指针


由于通常通过指针来访问实现类的对象,因此使用智能指针来控制其生命周期是很自然的。 但是应该记住,如果使用了第二种删除对象的选项,那么使用标准的智能指针,就必须传输用户删除器(类型)或这种类型的实例。 如果不这样做,那么智能指针将使用delete运算符删除该对象,并且代码将根本无法编译(由于受保护的析构函数)。 在[Josuttis],[Meyers3]中详细讨论了标准智能指针(包括自定义删除器的使用)。 在第4.3.1节中可以找到使用自定义卸妆的示例。


, , , .



3.9。 -


- const. , , -, .



3.10。 COM-


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> // IIBase using UniquePtr = std::unique_ptr<I, BaseDeleter>; 

, , - , UniquePtr .


-:


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

:


 template <class I> // IIBase 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 .


.


, , , , .





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


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


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


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


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


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


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


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




Source: https://habr.com/ru/post/zh-CN427281/


All Articles