Must a c++ interface obey the rule of five?

China☆狼群 提交于 2019-12-21 03:33:42

问题


What is the correct way to declare instantiation methods when defining an interface class?

Abstract base classes are required to have a virtual destructor for obvious reasons. However, the following compilation warning is then given: "'InterfaceClass' defines a non-default destructor but does not define a copy constructor, a copy assignment operator, a move constructor or a move assignment operator", which is the 'rule of five'.

I understand why the 'rule of five' should be obeyed in general, but is it still applicable for an abstract base class or interface?

My implimentation is then:

class InterfaceClass
{
    //  == INSTANTIATION ==
  protected:
    //  -- Constructors --
    InterfaceClass()                      = default;
    InterfaceClass(const InterfaceClass&) = default;
    InterfaceClass(InterfaceClass&&)      = default;

  public:
    //  -- Destructors --
    virtual ~InterfaceClass() = 0;


    //  == OPERATORS ==
  protected:
    //  -- Assignment --
    InterfaceClass& operator=(const InterfaceClass&) = default;
    InterfaceClass& operator=(InterfaceClass&&)      = default;


    //  == METHODS ==
  public:
    // Some pure interface methods here...
};



//  == INSTANTIATION ==
//  -- Destructors --
InterfaceClass::~InterfaceClass()
{
}

Is this correct? Should these methods be = delete instead? Is there some way of declaring the destructor to be virtual pure whilst also somehow remaining default?

Even if I declare the destructor as: virtual ~InterfaceClass() = default;, if I do not explicitly default the other four then I will get the same compiler warning.

Tl;dr: What is the correct way to satisfy the 'rule of five' for an interface class as the user must define a virtual destructor.

Thanks for your time and help!


回答1:


Is this correct? Should these methods be = delete instead?

Your code seems correct. The need of defining special copy/move member functions as default and protected comes clear when you try to copy a derived class polymorphycally. Consider this additional code:

#include <iostream>

class ImplementationClass : public InterfaceClass
{
  private:
    int data;
  public:
    ImplementationClass()
    {
        data=0;    
    };
    ImplementationClass(int p_data)
    {
        data=p_data;
    };
    void print()
    {
        std::cout<<data<<std::endl;
    };
};


int main()
{
    ImplementationClass A{1};
    ImplementationClass B{2};
    InterfaceClass *A_p = &A;
    InterfaceClass *B_p = &B;
    // polymorphic copy
    *B_p=*A_p;
    B.print();
    // regular copy
    B=A;
    B.print();
    return 0;
}

And consider 4 options for defining special copy/move member functions in your InterfaceClass.

  1. copy/move member functions = delete

With special copy/move member functions deleted in your InterfaceClass, you would prevent polymorphic copy:

*B_p = *A_p; // would not compile, copy is deleted in InterfaceClass

This is good, because polymorphic copy would nor be able co copy the data member in the derived class.

On the other hand, you would also prevent normal copy, as the compiler won't be able to implicitly generate a copy assignment operator without the base class copy assignment operator:

B = A; //  would not compile either, copy assignment is deleted in ImplementationClass 
  1. copy/move special member functions public

With copy/move special member functions as default and public, (or without defining copy/move member functions), normal copy would work:

B = A; //will copile and work correctly

but polymorphic copy would be enabled and lead to slicing:

*B_p = *A_p; // will compile but not copy the extra data members in the derived class. 
  1. copy/move special member functions not defined

If move&copy special member functions are not defined, behavior with respect to copy is similar to 2: the compiler will implicitly generate deprecated copy special members (leading to polymorphic slicing). However in this case the compiler will not implicitly generate move special members, so copy will be used where a move would be possible.

  1. protected copy/move member functions (your proposal)

With special copy/move member functions as default and protected, as in your example, you will prevent polymorphic copy which may lead to slicing:

*B_p = *A_p; // will not compile, copy is protected in InterfaceClass

However, the compile will explicitly generate a default copy assignment operator InterfaceClass, and ImplementationClass will be able to implicitly generate its copy assignment operator:

B = A; //will compile and work correctly

So your approach seems the best and safest alternative




回答2:


For destructor, if you want to make it both pure virtual and default, you can default it in implementation:

class InterfaceClass
{
    //  -- Destructors --
    virtual ~InterfaceClass() = 0;
};

InterfaceClass::~InterfaceClass() = default;

It does not make much difference if the destructor is default or empty, though.

Now for the rest of your question.

Typically you should have copy constructor and assignment operator defaulted. This way, they don't prevent making default assignment operators and copy constructor in derived classes. Default implementation is correct, as there's no invariant to copy.

So if you want to implement easily Clone method, deleting copy constructor would harm:

class InterfaceClass
{
    virtual  InterfaceClass* Clone() = 0;
    virtual ~InterfaceClass() = 0;
};

class ImplementationClass : public InterfaceClass
{
public:
    // This will not work if base copy constructor is deleted
    ImplementationClass(const ImplementationClass&) = default; 
    // Writing copy constructor manually may be cumbersome and hard to maintain,
    // if class has a lot of members

    virtual  ImplementationClass* Clone() override
    {
        return new ImplementationClass(*this); // Calls copy constructor
    }
};

Note also that default implementation of copy/move constructor would not be accidentally used against intention - as instances of abstract base class cannot be created. So you will always be copying derived classes, and they should define, if copying is legal or not.

However, for some classes making copies totally would not make sense, in this case it may be wise to prohibit copying/assigning in the very base class.

Tl;dr: it depend, but most likely you'd better leave them as default.




回答3:


In general, if any of the big 3 special functions has none-[trivial/default] definition, the other 2 should be defined. If the 2 special move functions have none-[trivial-default] definition, then you need take care of all 5. In the case of an interface with a nop defined dtor, you don't need bother defining the rest - unless for other reasons. Even none-trivial definitions do not nessecitate a redefinition of other functions; only when some sort of resource management(e.g. memory, file, io, sync...) is involved, one need define the big 3(5).



来源:https://stackoverflow.com/questions/49961811/must-a-c-interface-obey-the-rule-of-five

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!