Определение интерфейсов (абстрактных классов без элементов) в C++
Под интерфейсом (терминология C#) я подразумеваю абстрактный класс без элементов данных. Таким образом, такой класс указывает только контракт (набор методов), который должны реализовать подклассы. Мой вопрос:как правильно реализовать такой класс в современном С++?
Основные рекомендации C++ [1] поощряют использование абстрактных классов без элементов данных в качестве интерфейсов [I.25 и C.121]. Интерфейсы обычно должны полностью состоять из общедоступных чистых виртуальных функций и виртуального деструктора по умолчанию/пустого [из C.121]. Следовательно, я думаю, это должно быть объявлено с помощью
Чтобы разрешить использование и удаление объектов подкласса с помощью указателей на абстрактный класс, абстрактному классу необходим общедоступный виртуальный деструктор по умолчанию [C.127]. «Полиморфный класс должен подавлять копирование» [C.67] путем удаления операций копирования (оператор присваивания копирования, конструктор копирования) для предотвращения нарезки. Я предполагаю, что это также распространяется на конструктор перемещения и оператор присваивания перемещения, поскольку их также можно использовать для нарезки. Для фактического клонирования абстрактный класс может определить виртуальный
В C.129 в примере используются интерфейсы только с виртуальным наследованием. Если я правильно понимаю, не имеет значения, если интерфейсы производны (возможно, лучше: «реализованы»?) с использованием
Подводя итог, можно сказать, что:Интерфейс должен состоять только из общедоступных методов. Он должен объявить общедоступный виртуальный деструктор по умолчанию. Он должен явно удалять присваивание копии, копировать конструкцию, перемещать присваивание и перемещать конструкцию. Он может определять метод полиморфного клонирования. Я должен быть получен с использованием
Еще одна вещь, которая меня смущает: очевидное противоречие: «Абстрактному классу обычно не нужен конструктор» [C.126]. Однако если реализовать правило пяти, удалив все операции копирования (в соответствии с [C.67]), у класса больше не будет конструктора по умолчанию. Следовательно, подклассы никогда не могут быть созданы (поскольку конструкторы подклассов вызывают конструкторы базового класса), и поэтому абстрактный базовый класс всегда должен объявлять конструктор по умолчанию?!Я что-то неправильно понимаю?
Ниже приведен пример. Согласны ли вы с таким способом определения и использования абстрактного класса без членов (интерфейса)?
// C++17
/// An interface describing a source of random bits.
// The type `BitVector` could be something like std::vector<bool>.
#include <memory>
struct RandomSource { // `struct` is used for interfaces throughout core guidelines (e.g. C.122)
virtual BitVector get_random_bits(std::size_t num_bits) = 0; // interface is just one method
// rule of 5 (or 6?):
RandomSource() = default; // needed to instantiate sub-classes !?
virtual ~RandomSource() = default; // Needed to delete polymorphic objects (C.127)
// Copy operations deleted to avoid slicing. (C.67)
RandomSource(const RandomSource &) = delete;
RandomSource &operator=(const RandomSource &) = delete;
RandomSource(RandomSource &&) = delete;
RandomSource &operator=(RandomSource &&) = delete;
// To implement copying, would need to implement a virtual clone method:
// Either return a smart pointer to base class in all cases:
virtual std::unique_ptr<RandomSource> clone() = 0;
// or use `owner`, an alias for raw pointer from the Guidelines Support Library (GSL):
// virtual owner<RandomSource*> clone() = 0;
// Since GSL is not in the standard library, I wouldn't use it right now.
};
// Example use (class implementing the interface)
class PRNG : public virtual RandomSource { // virtual inheritance just for clarity?
// ...
BitVector get_random_bits(std::size_t num_bits) override;
// may the subclass ever define copy operations? I guess no.
// implemented clone method:
// owner<PRNG*> clone() override; // for the alternative owner method...
// Problem: multiple identical methods if several interfaces are inherited,
// each of which requires a `clone` method?
//Maybe the std. library should provide an interface
// (e.g. `Clonable`) to unify this requirement?
std::unique_ptr<RandomSource> clone() override;
//
// ... private data members, more methods, etc...
};
[1]: https://github.com/isocpp/CppCoreGuidelines, commit 2c95a33fefae87c2222f7ce49923e7841faca482
2 ответа
Вы задаете много вопросов, но я попробую.
Под интерфейсом (терминология C#) я подразумеваю абстрактный класс без элементов данных.
Ничего особенно похожего на интерфейс C# не существует. Наиболее близок абстрактный базовый класс C++, но есть и отличия (например, вам нужно будет определить тело для виртуального деструктора).
Таким образом, такой класс указывает только контракт (набор методов), который должны реализовать подклассы. Мой вопрос: как правильно реализовать такой класс в современном С++?
Как виртуальный базовый класс.
Пример:
class OutputSink
{
public:
~OutputSink() = 0;
// contract:
virtual void put(std::vector<std::byte> const& bytes) = 0;
};
OutputSink::~OutputSink() = default;
Следовательно, я предполагаю, что его следует объявить с помощью ключевого слова struct, поскольку в любом случае он содержит только общедоступные члены.
Существует несколько соглашений о том, когда использовать структуру, а когда класс. Я рекомендую руководство (эй, вы просили мнения: D) использовать структуры, когда у вас нет инвариантов их данных. Для базового класса используйте
«Полиморфный класс должен подавлять копирование»
В основном правда. Я написал код, в котором клиентский код не выполнял копии унаследованных классов, и код работал нормально (без их запрета). Базовые классы не запрещали это явно, но это был код, который я писал в своем хобби-проекте. При работе в команде рекомендуется специально ограничивать копирование.
Как правило, не утруждайте себя клонированием, пока не найдете реальный вариант его использования в своем коде. Затем реализуйте клонирование со следующей сигнатурой (пример для моего класса выше):
virtual std::unique_ptr<OutputSink> OutputSink::clone() = 0;
Если это по какой-то причине не работает, используйте другую подпись (например, верните shared_ptr).
Интерфейс должен состоять только из общедоступных методов. Он должен объявить [...]. Должно [...]. Он должен быть получен с использованием общедоступного виртуального.
Не пытайтесь представить идеальный интерфейс C# в C++. C++ более гибок, и вам редко потребуется добавлять один к одному реализацию концепции C# в C++.
Например, в базовые классы на C++ я иногда добавляю общедоступные невиртуальные реализации функций с виртуальными реализациями:
class OutputSink
{
public:
void put(const ObjWithHeaderAndData& o) // non-virtual
{
put(o.header());
put(o.data());
}
protected:
virtual void put(ObjectHeader const& h) = 0; // specialize in implementations
virtual void put(ObjectData const& d) = 0; // specialize in implementations
};
таким образом, абстрактный базовый класс всегда должен объявлять конструктор по умолчанию?! Я что-то неправильно понимаю?
При необходимости определите правило 5. Если код не компилируется из-за отсутствия конструктора по умолчанию, добавьте конструктор по умолчанию (используйте рекомендации только тогда, когда они имеют смысл).
Изменить: (обращаясь к комментарию)
как только вы объявляете виртуальный деструктор, вы должны объявить некоторый конструктор, чтобы класс можно было использовать любым способом.
Не обязательно. Лучше (но на самом деле «лучше» зависит от того, с чем вы согласны с вашей командой) понимать значения по умолчанию, которые компилятор добавляет для вас, и добавлять код построения только тогда, когда он отличается от этого. Например, в современном C++ вы можете инициализировать элементы встроенными, часто полностью устраняя необходимость в конструкторе по умолчанию.
Хотя на большинство вопросов уже дан ответ, я решил поделиться некоторыми мыслями о конструкторе по умолчанию и виртуальном наследовании.
Класс всегда должен иметь общедоступный (или, по крайней мере, защищенный) конструктор, чтобы гарантировать, что подклассы все еще могут вызывать суперконструктор. Несмотря на то, что в базовом классе нечего конструировать, это необходимо для синтаксиса C++ и концептуально не имеет никакого значения.
Мне нравится Java как пример интерфейсов и суперклассов. Люди часто задаются вопросом, почему Java разделяет абстрактные классы и интерфейсы на разные синтаксические типы. Однако, как вы, вероятно, уже знаете, это связано с проблемой наследования алмазов, когда два суперкласса имеют один и тот же базовый класс и, следовательно, копируют данные из базового класса. Java делает это невозможным, заставляя классы, несущие данные, быть классами, а не интерфейсами, и заставляя подклассы наследоваться только от одного класса (а не интерфейса, который не несет данных).
Имеем следующую ситуацию:
struct A {
int someData;
A(): someData(0) {}
};
struct B : public A {
virtual void modifyData() = 0;
};
struct C : public A {
virtual void alsoModifyData() = 0;
};
struct D : public B, public C {
virtual void modifyData() { someData += 10; }
virtual void alsoModifyData() { someData -= 10; }
};
Когда modifyData и alsoModifyData вызываются для экземпляра D, они не будут изменять одну и ту же переменную, как можно было бы ожидать, из-за того, что компилятор создаст две копии someData для классов B и C.
Чтобы противостоять этой проблеме, была введена концепция виртуального наследования. Это означает, что компилятор не просто рекурсивно создаст производный класс из членов суперклассов, но вместо этого проверит, происходят ли виртуальные суперклассы от общего предка. Точно так же в Java есть концепция интерфейса, которому не разрешено владеть данными, только функциями.
Но интерфейсы могут строго наследоваться от других интерфейсов, исключая для начала проблему алмаза. В этом, конечно же, Java отличается от C++. Эти «интерфейсы» С++ по-прежнему могут наследоваться от классов, владеющих данными, тогда как в java это невозможно.
Идея «виртуального наследования», сигнализирующего о том, что класс должен быть разделен на подклассы и что данные от предков должны быть объединены в случае алмазного наследования, делает необходимым (или, по крайней мере, идиомой) использование виртуального наследования на « Интерфейсы" ясно.
Я надеюсь, что этот ответ был (хотя и более концептуальным) полезным для вас!