Подтвердить что ты не робот

Лучший способ объявить интерфейс на С++ 11

Как мы все знаем, некоторые языки имеют понятие интерфейсов. Это Java:

public interface Testable {
  void test();
}

Как я могу достичь этого на С++ (или С++ 11) самым компактным способом и с небольшим шумом кода? Я был бы признателен за решение, для которого не требуется отдельное определение (пусть заголовок будет достаточным). Это очень простой подход, который даже я нахожу багги;-)

class Testable {
public:
  virtual void test() = 0;
protected:
  Testable();
  Testable(const Testable& that);
  Testable& operator= (const Testable& that);
  virtual ~Testable();
}

Это только начало.. и уже дольше, что я бы хотел. Как его улучшить? Возможно, есть базовый класс где-то в пространстве имен std, сделанном именно для этого?

4b9b3361

Ответ 1

Как насчет:

class Testable
{
public:
    virtual ~Testable() { }
    virtual void test() = 0;
}

В С++ это не влияет на возможность перезаписи дочерних классов. Все это говорит о том, что ребенок должен реализовать test (именно это вы хотите для интерфейса). Вы не можете создать экземпляр этого класса, поэтому вам не нужно беспокоиться о каких-либо неявных конструкторах, поскольку они никогда не могут быть вызваны непосредственно в качестве типа родительского интерфейса.

Если вы хотите, чтобы эти дочерние классы реализовали деструктор, вы также можете сделать это чистым (но вы все равно должны реализовать его в интерфейсе).

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

Ответ 2

Для динамического (runtime) полиморфизма я бы рекомендовал использовать идиому не виртуального интерфейса (NVI). Этот шаблон поддерживает интерфейс не виртуальный и общедоступный, деструктор виртуальный и общедоступный, а реализация - виртуальная и частная

class DynamicInterface
{
public:
    // non-virtual interface
    void fun() { do_fun(); } // equivalent to "this->do_fun()"

    // enable deletion of a Derived* through a Base*
    virtual ~DynamicInterface() = default;    
private:
    // pure virtual implementation
    virtual void do_fun() = 0; 
};

class DynamicImplementation
:
    public DynamicInterface
{
private:
    virtual void do_fun() { /* implementation here */ }
};

Хорошая вещь о динамическом полиморфизме заключается в том, что вы можете - на время выполнения - передавать любой производный класс, где ожидается указатель или ссылка на базовый класс интерфейса. Система времени выполнения автоматически отключит указатель this от статического базового типа до динамического производного типа и вызовет соответствующую реализацию (обычно это происходит через таблицы с указателями на виртуальные функции).

Для статического (полиморфизм времени компиляции) я бы рекомендовал использовать шаблон Curiously Recurring Template Pattern (CRTP). Это значительно больше связано с тем, что с помощью static_cast необходимо выполнить автоматическое понижение уровня от базы до производного от динамического полиморфизма. Это статическое литье может быть определено в вспомогательном классе, каждый статический интерфейс которого происходит от

template<typename Derived>
class enable_down_cast
{
private:  
        typedef enable_down_cast Base;    
public:
        Derived const* self() const
        {
                // casting "down" the inheritance hierarchy
                return static_cast<Derived const*>(this);
        }

        Derived* self()
        {
                return static_cast<Derived*>(this);
        }    
protected:
        // disable deletion of Derived* through Base*
        // enable deletion of Base* through Derived*
        ~enable_down_cast() = default; // C++11 only, use ~enable_down_cast() {} in C++98
};

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

template<typename Impl>
class StaticInterface
:
    // enable static polymorphism
    public enable_down_cast< Impl >
{
private:
    // dependent name now in scope
    using enable_down_cast< Impl >::self;    
public:
    // interface
    void fun() { self()->do_fun(); }    
protected:
    // disable deletion of Derived* through Base*
    // enable deletion of Base* through Derived*
    ~StaticInterface() = default; // C++11 only, use ~IFooInterface() {} in C++98/03
};

и, наконец, вы выполните реализацию, которая вытекает из интерфейса с как параметром

class StaticImplementation
:
    public StaticInterface< StaticImplementation > 
{
private:
    // implementation
    friend class StaticInterface< StaticImplementation > ;
    void do_fun() { /* your implementation here */ }
};

Это все еще позволяет вам иметь несколько реализаций одного и того же интерфейса, но вам нужно знать во время компиляции, какую реализацию вы вызываете.

Итак, когда использовать какую-либо форму? Обе формы позволят вам повторно использовать общий интерфейс и внедрять тестовое тестирование pre/post внутри класса интерфейса. Преимущество динамического полиморфизма заключается в том, что вы обладаете гибкостью во время выполнения, но платите за это в вызовах виртуальных функций (как правило, вызов через указатель функции, с небольшой возможностью для вложения). Статический полиморфизм - это зеркало: нет накладных расходов на виртуальные функции, но недостатком является то, что вам нужен более шаблонный код, и вам нужно знать, что вы вызываете во время компиляции. В основном, эффективность/гибкость компромисса.

ПРИМЕЧАНИЕ. для полиморфизма времени компиляции, вы также можете использовать параметры шаблона. Разница между статическим интерфейсом через идиому CRTP и обычными параметрами шаблона заключается в том, что интерфейс типа CRTP является явным (на основе функций-членов), а интерфейс шаблона неявный (на основе действительных выражений)

Ответ 3

Согласно Скотту Мейерсу (Effective Modern C++): при объявлении интерфейса (или полиморфного базового класса) вам необходим виртуальный деструктор для правильных результатов операций, таких как delete или typeid типа, для объекта производного класса, доступ к которому осуществляется через указатель или ссылку на базовый класс.

virtual ~Testable() = default;

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

Testable(Testable&&) = default; 
Testable& operator=(Testable&&) = default;

Объявление операций перемещения отключает операции копирования, и вам также необходимо:

Testable(const Testable&) = default;
Testable& operator=(const Testable&) = default;

И конечный результат:

class Testable 
{
public:
    virtual ~Testable() = default; // make dtor virtual
    Testable(Testable&&) = default;  // support moving
    Testable& operator=(Testable&&) = default;
    Testable(const Testable&) = default; // support copying
    Testable& operator=(const Testable&) = default;

    virtual void test() = 0;

};

Еще одна интересная статья здесь: Правило нуля в C++

Ответ 4

Заменив слово class на struct, все методы будут общедоступными по умолчанию, и вы можете сохранить строку.

Нет необходимости защищать конструктор, так как вы все равно не можете создавать экземпляр класса с помощью виртуальных методов. Это касается и конструктора копирования. Созданный компилятором конструктор по умолчанию будет пустым, поскольку у вас нет каких-либо элементов данных, и он полностью достаточен для ваших производных классов.

Вы правы, если вас беспокоит оператор =, поскольку созданный компилятором, безусловно, сделает неправильную вещь. На практике никто никогда не беспокоится об этом, потому что копирование одного объекта интерфейса в другой никогда не имеет смысла; это не ошибка, которая обычно случается.

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

Конечный результат - только одна строка длиннее, чем эквивалент Java:

struct Testable {
    virtual void test() = 0;
    virtual ~Testable();
};

Ответ 5

Имейте в виду, что "правило из трех" не является необходимым, если вы не управляете указателями, ручками и/или все члены данных класса имеют свои собственные деструкторы, которые будут управлять любой очисткой. Также в случае виртуального базового класса, поскольку базовый класс никогда не может быть напрямую создан, нет необходимости объявлять конструктор, если все, что вы хотите сделать, - это определить интерфейс, который не имеет элементов данных... компилятор значения по умолчанию просто прекрасны. Единственный элемент, который вам нужно сохранить, - это виртуальный деструктор, если вы планируете вызывать delete по указателю типа интерфейса. Таким образом, на самом деле ваш интерфейс может быть таким же простым, как:

class Testable 
{
    public:
        virtual void test() = 0;  
        virtual ~Testable();
}