Must a c++ interface obey the rule of five?
Asked Answered
P

4

24

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!

Protector answered 22/4, 2018 at 1:49 Comment(11)
"the following compilation warning is then given" - by which compiler/version?Controversial
clang 6.0 warns about a depreciated copy constructor. clang-tidy 6.0 static analyser gives the specific warning string above. g++ 4.2.1 does not seem to trigger a warning for this case. I am using mac os High Sierra 10.13.4Protector
A base class destructor should be either protected (preventing polymorphic delete) or public and virtual (making polymorphic delete safe). The protected and virtual combination you have is quite weird.Stanton
Thanks for the tip. I'll update the example to reflect this.Protector
Non-owning interfaces, that don't own, shouldn't own, that's the rule of zero. en.cppreference.com/w/cpp/language/rule_of_threeDelaunay
Ah I have just spotted the paragraph: 'When a base class is intended for polymorphic use, its destructor may have to be declared public and virtual. This blocks implicit moves (and deprecates implicit copies), and so the special member functions have to be declared as defaulted'. I did not realise that defaulting four methods with a public virtual destructor was considered to follow the rule of zero.Protector
Does your intetface class have any data members?Ferocity
Also why ever define a pure virtual destructor? This makes very little sense.Ferocity
@n.m. If your class has no other pure virtual methods but you still want it to be abstract. Or just for consistency, because pure virtual means "must be overridden". A pure virtual dtor still needs an out-of-line definition though: struct C { ~C()=0; }; C::~C() = default; because it will be called by derived dtors. An undefined pure virtual dtor would be a linker error.Valor
@Valor I always found this argument preposterous. If it doesn't have pure virtual methods, it doesn't need to be abstract. You may want it to be abstract, bit such desire is not necessarily justified.Ferocity
My recommendation is to follow the C++ Core Guidelines, specifically C.67, C.130, C.ctor (C.20, C.21, C.22).Flammable
G
10

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 not be able to 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 compile and work correctly

but polymorphic copy would be enabled and lead to slicing:

*B_p = *A_p; // will compile but will 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 would otherwise had lead to slicing:

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

However, the compiler will explicitly generate a default copy assignment operator for 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

Goodlooking answered 7/3, 2019 at 12:5 Comment(1)
Regarding 1 and 4: 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 (...) well why is that not good? To avoid the risk of slicing, follow C.67: A polymorphic class should suppress public copy/move. If you want to "clone" a polymorphic type add a clone function (C.130). The clone API can be supported by derived classes.Flammable
E
1

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.

Earwitness answered 23/2, 2019 at 18:43 Comment(0)
S
0

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).

Squat answered 7/3, 2019 at 16:25 Comment(2)
Only defining the big 3 is indeed safe, but move semantic special members will not be implicitly generated, and you will force objects to be copied when they could be moved. What does nop defined dtor mean? non-public? Does this solve the problem of slicing in polymorphic copy? I would happily do without defining special member functions in interfaces if I was sure it was safeGoodlooking
@Goodlooking nop means no-operation. Move is not always different or cheaper than copy. Move is just an optimization or a means of ownership transfer on none-copyable objects.Squat
J
0

I ran into this problem recently in trying to define an abstract Observer class. Clang-tidy (version 14.0.6) gives a very similar warning to the original, marked [cppcoreguidelines-special-member-functions]. At first I tried to define copy/move constructors and copy/move assignment operators, marked = delete (also tried = default). Eventually I got a compilation error because the concrete Observer did need a regular constructor to get a Subject pointer to register for notifications.

Upon reflection, however, I think no abstract class really needs a copy/move constructor (and therefore doesn't need to follow the rule of three/five), because by definition, an abstract class cannot be instantiated. So clang-tidy or any other compiler/analyzer that issues a "special member functions" warning on an abstract class is incorrect.

Jempty answered 1/12, 2023 at 1:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.