Должен ли интерфейс C++ подчиняться правилу пяти?

Как правильно объявлять методы создания экземпляров при определении класса интерфейса?

Абстрактные базовые классы должны иметь виртуальный деструктор по очевидным причинам. Однако затем выдается следующее предупреждение компиляции: "InterfaceClass определяет деструктор не по умолчанию, но не определяет конструктор копирования, оператор назначения копирования, конструктор перемещения или оператор назначения перемещения", что является "правилом пяти". ".

Я понимаю, почему "правило пяти" следует соблюдать в целом, но применимо ли оно к абстрактному базовому классу или интерфейсу?

Моя импликация тогда:

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()
{
}

Это правильно? Должны ли эти методы быть = delete вместо? Есть ли какой-нибудь способ объявить деструктор виртуальным чистым, оставаясь при этом по умолчанию?

Даже если я объявлю деструктор как: virtual ~InterfaceClass() = default;Если я не буду явно задавать по умолчанию остальные четыре, я получу то же предупреждение компилятора.

Tl; dr: Как правильно выполнить "правило пяти" для класса интерфейса, поскольку пользователь должен определить виртуальный деструктор.

Спасибо за ваше время и помощь!

0 ответов

Это правильно? Должны ли эти методы быть = удалить вместо этого?

Ваш код кажется правильным. Необходимость определения специальных функций-членов копирования / перемещения по умолчанию и защищенных становится очевидной при попытке полиморфно скопировать производный класс. Рассмотрим этот дополнительный код:

#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;
}

И рассмотрите 4 варианта определения специальных функций-членов копирования / перемещения в вашем InterfaceClass.

  1. копировать / переместить функции-члены = удалить

С помощью специальных функций-членов копирования / перемещения, удаленных в вашем InterfaceClass, вы предотвратите полиморфное копирование:

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

Это хорошо, потому что полиморфная копия не сможет копировать элемент данных в производном классе.

С другой стороны, вы бы также запретили нормальное копирование, поскольку компилятор не сможет неявно сгенерировать оператор присваивания копии без оператора присваивания копии базового класса:

B = A; //  would not compile either, copy assignment is deleted in ImplementationClass 
  1. копировать / перемещать специальные функции-члены

При использовании специальных функций-членов копирования / перемещения по умолчанию и общедоступных (или без определения функций-членов копирования / перемещения) будет работать обычное копирование:

B = A; //will copile and work correctly

но полиморфная копия будет включена и приведет к нарезке:

*B_p = *A_p; // will compile but not copy the extra data members in the derived class. 
  1. копировать / перемещать специальные функции-члены не определены

Если специальные функции-члены перемещения и копирования не определены, поведение по отношению к копированию аналогично 2: компилятор неявно генерирует устаревшие специальные элементы-копии (что приводит к полиморфному разрезанию). Однако в этом случае компилятор не будет неявно генерировать специальные члены перемещения, поэтому копирование будет использоваться там, где перемещение было бы возможным.

  1. функции защищенного копирования / перемещения (ваше предложение)

С помощью специальных функций-членов копирования / перемещения по умолчанию и защищенных, как в вашем примере, вы предотвратите полиморфное копирование, которое может привести к нарезке:

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

Тем не менее, компиляция будет явно генерировать оператор назначения копирования по умолчанию InterfaceClass, и AvailabilityClass сможет неявно генерировать свой оператор назначения копирования:

B = A; //will compile and work correctly

Таким образом, ваш подход кажется лучшей и самой безопасной альтернативой

Для деструктора, если вы хотите сделать его как чисто виртуальным, так и по умолчанию, вы можете использовать его по умолчанию в реализации:

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

InterfaceClass::~InterfaceClass() = default;

Это не имеет большого значения, если деструктор по умолчанию или пустой.

Теперь для остальной части вашего вопроса.

Обычно у вас должен быть конструктор копирования и оператор присваивания по умолчанию. Таким образом, они не мешают создавать операторы присваивания по умолчанию и конструктор копирования в производных классах. Реализация по умолчанию верна, так как нет инварианта для копирования.

Так что если вы хотите легко реализовать Clone Метод, удаляющий конструктор копирования, повредит:

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
    }
};

Также обратите внимание, что реализация конструктора копирования / перемещения по умолчанию не будет случайно использоваться против намерения - поскольку экземпляры абстрактного базового класса не могут быть созданы. Таким образом, вы всегда будете копировать производные классы, и они должны определить, является ли копирование легальным или нет.

Однако для некоторых классов создание копий полностью не имеет смысла, в этом случае может быть целесообразно запретить копирование / присвоение в самом базовом классе.

Tl;dr: это зависит, но, скорее всего, вам лучше оставить их по умолчанию.

В общем, если какая-либо из трех больших специальных функций не имеет определения [trivial/default], следует определить другие 2. Если у 2 специальных функций перемещения нет определения [trivial-default], вам нужно позаботиться обо всех 5. В случае интерфейса с dop-определенным dtor вам не нужно беспокоиться об определении остальных - если только для других причины. Даже нетривиальные определения не требуют переопределения других функций; только когда требуется какое-то управление ресурсами (например, память, файл, io, синхронизация...), нужно определить большую 3(5).

Другие вопросы по тегам