C++11 Удалить переопределенный метод
Предисловие:
Это вопрос о передовом опыте, касающемся нового значения оператора удаления, введенного в C++11 при применении к дочернему классу, переопределяющему виртуальный метод унаследованного родителя.
Фон:
Согласно стандарту, первый процитированный случай использования заключается в явном запрещении вызова функций для определенных типов, где в противном случае преобразования были бы неявными, как, например, в примере из §8.4.3 последнего проекта стандарта C++11:
struct sometype {
sometype() = delete; // OK, but redundant
some_type(std::intmax_t) = delete;
some_type(double);
};
Приведенный выше пример понятен и целеустремлен. Однако следующий пример, в котором новый оператор переопределяется и не может быть вызван путем определения его как удаленного, заставил меня задуматься о других сценариях, которые я позже идентифицирую в разделе вопросов (пример ниже взят из §8.4.3 C++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 */ }
};
Хотя я вижу много преимуществ такого подхода, я думаю, что больше склоняюсь к мысли, что это злоупотребление этой функцией. Основной недостаток, который я вижу в приведенном выше примере, заключается в том, что классические отношения наследования "есть-а" нарушены. Я читал много статей, которые настоятельно рекомендуют против любого использования наследования выражать отношения типа "как есть" и вместо этого использовать композицию с функциями-обертками для четкой идентификации отношений классов. В то время как следующий часто осуждаемый пример требует больше усилий для реализации и поддержки (относительно количества строк, написанных для этого фрагмента кода, поскольку каждая наследуемая функция, которая должна быть доступна публично, должна быть явно вызвана наследующим классом), использование Удалить, как показано выше, очень похожи во многих отношениях:
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 */ }
};
Заранее благодарим вас за ваши ответы и дальнейшее понимание этого потенциального варианта использования удаления.
3 ответа
Пункт 8.4.3/2 Стандарта C++ косвенно запрещает удаление функции, которая переопределяет виртуальную функцию:
"Программа, которая ссылается на удаленную функцию неявно или явно, кроме как для ее объявления, является неправильно сформированной. [Примечание: это включает в себя вызов функции неявно или явно и формирование указателя или указателя на член функции"
Вызов переопределяющей виртуальной функции через указатель на базовый класс является попыткой неявного вызова функции. Следовательно, согласно 8.4.3/2 конструкция, которая допускает это, является незаконной. Также обратите внимание, что никакой компилятор, соответствующий C++11, не позволит вам удалить переопределяющую виртуальную функцию.
Более конкретно, то же самое предусмотрено в пункте 10.3 / 16:
"Функция с удаленным определением (8.4) не должна переопределять функцию, у которой нет удаленного определения. Аналогично, функция, у которой нет удаленного определения, не должна переопределять функцию с удаленным определением".
10.3p16:
Функция с удаленным определением (8.4) не должна переопределять функцию, у которой нет удаленного определения. Аналогично, функция, которая не имеет удаленного определения, не должна переопределять функцию с удаленным определением.
Другие ответы объясняют, почему довольно хорошо, но там у вас есть официальный Ты не должен.
Рассмотрим некоторую функцию:
void f(LibraryVersion1* p)
{
p->doSomething1();
}
Это скомпилируется до того, как LibraryVersion2 будет даже написан.
Итак, теперь вы реализуете LibraryVersion2 с удаленным виртуальным.
Ф уже скомпилирован. До времени выполнения он не знает, с каким подклассом LibraryVersion1 он был вызван.
Вот почему удаленный виртуальный не является законным, это не имеет никакого смысла.
Лучшее, что вы можете сделать, это:
class LibraryVersion2 : public LibraryVersion1
{
public:
virtual void doSomething1() override
{
throw DeletedFunctionException();
}
}