С++ 11 Удалить переопределенный метод

Введение:

Это вопрос о лучших практиках относительно нового значения оператора delete, введенного с С++ 11, при применении к дочернему классу, переопределяющему унаследованный родительский виртуальный метод.

Фон:

В стандарте первый пример использования - это явно запретить вызывающие функции для определенных типов, в которых преобразования в противном случае были бы неявными, например, пример из §8.4.3 последнего Стандартная версия С++ 11:

struct sometype {
    sometype() = delete; // OK, but redundant
    some_type(std::intmax_t) = delete;
    some_type(double);
};

Приведенный выше пример ясен и целенаправлен. Однако следующий пример, в котором новый оператор переопределяется и не может быть вызван, определяя его как удаленный, начал думать о других сценариях, которые я позже идентифицировал в разделе вопросов (пример ниже приведен в §8.4.3 из стандартная черновик С++ 11:

struct sometype {
    void *operator new(std::size_t) = delete;
    void *operator new[](std::size_t) = delete;
};
sometype *p = new sometype; // error, deleted class operator new
sometype *q = new sometype[3]; // error, deleted class operator new[]

Вопрос:

Расширяя эту мысль до наследования, мне любопытно относиться к другим соображениям относительно того, является ли следующий пример использования ясным и действительным прецедентом или если это нечеткое злоупотребление вновь добавленной функцией. Просьба привести обоснование вашего ответа (пример, который дает наиболее убедительные аргументы, будет принят). В следующем примере проект пытается поддерживать две версии библиотеки (требуется, чтобы библиотека была создана), поскольку вторая версия библиотеки наследуется от первой. Идея состоит в том, чтобы позволить исправлениям ошибок или изменениям, внесенным в первую версию библиотеки, автоматически распространяться на вторую версию библиотеки, позволяя второй версии библиотеки сосредоточиться только на ее отличиях от первой версии. Чтобы отказаться от функции во второй версии библиотеки, оператор delete используется для запрета вызова переопределенной функции:

class LibraryVersion1 {
public:
    virtual void doSomething1() { /* does something */ }
    // many other library methods
    virtual void doSomethingN() { /* does something else */ }
};

class LibraryVersion2 : public LibraryVersion1 {
public:
    // Deprecate the doSomething1 method by disallowing it from being called
    virtual void doSomething1() override = delete;

    // Add new method definitions
    virtual void doSomethingElse() { /* does something else */ }
};

Хотя я вижу много преимуществ для такого подхода, я думаю, что я больше склоняюсь к мысли о том, что это злоупотребление этой функцией. Первичная ошибка, которую я вижу в приведенном выше примере, заключается в том, что классическая взаимосвязь "is-a" наследования нарушена. Я прочитал много статей, которые настоятельно рекомендуют против любого использования наследования выражать отношения "sort-of-is-a" и вместо этого использовать композицию с функциями-оболочками, чтобы четко идентифицировать отношения классов. В то время как следующий часто ошибочный пример требует больше усилий для реализации и поддержки (в отношении количества строк, написанных для этого фрагмента кода, поскольку каждая унаследованная функция, доступная публично, должна быть явно вызвана классом наследования), использование удаления, как показано выше, очень похоже во многих отношениях:

class LibraryVersion1 {
public:
    virtual void doSomething1() { /* does something */ }
    virtual void doSomething2() { /* does something */ }
    // many other library methods
    virtual void doSomethingN() { /* does something */ }
};

class LibraryVersion2 : private LibraryVersion1 {
    // doSomething1 is inherited privately so other classes cannot call it
public:
    // Explicitly state which functions have not been deprecated
    using LibraryVersion1::doSomething2();
    //  ... using (many other library methods)
    using LibraryVersion1::doSomethingN();

    // Add new method definitions
    virtual void doSomethingElse() { /* does something else */ }
};

Заранее благодарю вас за ваши ответы и дальнейшую информацию об этом потенциальном случае использования delete.

Ответ 1

Параграф 8.4.3/2 Стандарта С++ косвенно запрещает удаление функции, которая переопределяет виртуальную функцию:

"Программа, которая ссылается на удаленную функцию неявно или явно, кроме объявления, плохо сформирована. [Примечание. Этот включает вызов функции неявно или явно и формируя указатель или указатель на элемент функции"

Вызов переопределяющей виртуальной функции с помощью указателя на базовый класс заключается в попытке неявно вызвать функцию. Поэтому, согласно 8.4.3/2, конструкция, которая допускает это, является незаконной. Также обратите внимание, что никакой компилятор, отвечающий требованиям С++ 11, не позволит вам удалить переопределяющую виртуальную функцию.

Более конкретно, то же самое предусмотрено в пункте 10.3/16:

"Функция с удаленным определением (8.4) не должна переопределять функцию, которая не имеет удаленного определения. Аналогично, функция, которая не имеет удаленного определения, не должна переопределять функцию с удаленным определением".

Ответ 2

10.3p16:

Функция с удаленным определением (8.4) не должна переопределять функцию, которая не имеет удаленного определения. Аналогично, функция, которая не имеет удаленного определения, не должна переопределять функцию с удаленным определением.

Другие ответы объясняют, почему довольно хорошо, но там у вас есть чиновник, которого нет.

Ответ 3

Рассмотрим некоторую функцию:

void f(LibraryVersion1* p)
{
    p->doSomething1();
}

Это будет скомпилировано до того, как LibraryVersion2 будет даже записано.

Итак, теперь вы реализуете LibraryVersion2 с удаленным виртуальным.

f уже скомпилирован. Он не знает до тех пор, пока не будет вызван подкласс подкласса LibraryVersion1.

Вот почему удаленный виртуальный не является законным, это не имеет никакого смысла.

Лучшее, что вы можете сделать, это:

class LibraryVersion2 : public LibraryVersion1
{
public:
    virtual void doSomething1() override
    {
         throw DeletedFunctionException();
    }
}