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

Конструкторы/деструкторы абстрактного базового класса С++ - общая правильность

Недавно я был немым как разработчик, поэтому я сделал решительный шаг, получил книгу на С++ и научился правильно поступать. В моей голове я знаю, что я хотел бы сделать. Я действительно хочу, чтобы Interface, когда он был унаследован, должен быть переопределен (если это возможно?). До сих пор у меня есть следующее:

class ICommand{

public:
    //  Virtual constructor. Needs to take a name as parameter
    //virtual ICommand(char*) =0;
    //  Virtual destructor, prevents memory leaks by forcing clean up on derived classes?
    //virtual ~ICommand() =0; 
    virtual void CallMe() =0;
    virtual void CallMe2() =0;
};

class MyCommand : public ICommand
{
public:
    // Is this correct?
    MyCommand(char* Name) { /* do stuff */ }
    virtual void CallMe() {}
    virtual void CallMe2() {}
};

Я намеренно оставил, как я думаю, что конструктор/деструктор должен быть реализован в ICommand. Я знаю, что если удалить комментарии, он не будет компилироваться. Пожалуйста, кто-нибудь может:

  • Покажите мне, как объявить конструктор/деструктор в ICommand и как они предназначены для использования в MyCommand
  • Правильно ли задал вещи в ICommand, чтобы MyCommand должен переопределить CallMe и CallMe2.

Надеюсь, я не пропустил что-то очень простое...

4b9b3361

Ответ 1

C++ не допускает виртуальных конструкторов. Простая реализация (без виртуального конструктора) будет выглядеть примерно так:

class ICommand {
public:
    virtual ~ICommand() = 0;
    virtual void callMe() = 0;
    virtual void callMe2() = 0;
};

ICommand::~ICommand() { } // all destructors must exist

Обратите внимание, что даже чистый виртуальный деструктор должен быть определен.

Конкретная реализация будет выглядеть точно так же, как ваш пример:

class MyCommand : public ICommand {
public:
    virtual void callMe() { }
    virtual void callMe2() { }
};

У вас есть несколько вариантов для конструктора. Один из вариантов - отключить конструктор по умолчанию для ICommand, чтобы подклассам пришлось реализовывать конструктор, который вызывает ваш конструктор ICommand:

#include <string>

class ICommand {
private:
    const std::string name;
    ICommand();
public:
    ICommand(const std::string& name) : name(name) { }
    virtual ~ICommand() = 0;
    virtual void callMe() = 0;
    virtual void callMe2() = 0;
};

ICommand::~ICommand() { } // all destructors must exist

Конкретная реализация теперь будет выглядеть примерно так:

class MyCommand : public ICommand {
public:
    MyCommand(const std::string& name) : ICommand(name) { }
    virtual void callMe() { }
    virtual void callMe2() { }
};

Ответ 2

Я знаю, что это старый, но это все еще мой первый хит в этом вопросе. Вот как бы я это сделал.

Заголовок интерфейса foo.h:

#pragma once
#include <memory>

enum class Implementations {Simple, Fancy};

class Foo
{
public:
    using Ptr = std::unique_ptr<Foo>;
    virtual ~Foo() = default;
    virtual void do_it() = 0;
};

Foo::Ptr create_foo(Implementations impl); // factory

Да, я знаю, что "однажды прагма", строго говоря, не стандартная, но она работает для меня.

Обратите внимание, что здесь ничего не реализовано. Конструктора нет: абстрактный класс не может быть создан. Вы получаете указатель на интерфейс через фабрику. Чтобы вызовы виртуальных функций работали, они должны вызываться через указатель. Виртуальный деструктор по умолчанию, потому что он не должен делать ничего особенного, кроме полиморфизации реализации. Фабрика - это бесплатная функция. Не нужно пытаться сделать его статическим членом или что-то в этом роде. Это не Ява.

Интерфейс foo.cpp:

#include "foo.h"
#include "foo_impl.h"

Foo::Ptr create_foo(Implementations impl)
{
    switch (impl)
    {
    case Implementations::Simple:
        return std::make_unique<Simple_foo>();
    case Implementations::Fancy:
        return std::make_unique<Fancy_foo>();
    default:
        return nullptr;
    }
}

Здесь фабрика реализована. Обратите внимание, что фабрика должна знать реализацию (и). Вот почему мы не реализуем его встроенным: если он был встроенным, заголовок интерфейса должен был бы включать заголовок реализации, и через него знания о реализации "просачивались" к месту вызова.

Заголовок реализации foo_impl.h:

#pragma once
#include "foo.h"

class Simple_foo : public Foo
{
    void do_it() override;
};

class Fancy_foo : public Foo
{
    void do_it() override;
};

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

Реализация foo_impl.cpp:

#include "foo_impl.h"
#include <iostream>

void Simple_foo::do_it()
{
    std::cout << "simple foo\n";
}

void Fancy_foo::do_it()
{
    std::cout << "fancy foo\n";
}

Просто реализуй функции.

Main.cpp:

#include "foo.h"

int main()
{
    auto sf = create_foo(Implementations::Simple);
    sf->do_it();
    auto ff = create_foo(Implementations::Fancy);
    ff->do_it();
    return 0;
}

Посредством перечисления мы можем выбрать реализацию, которую мы хотим. Указатели имеют тип Foo::Ptr, псевдоним для std::unique_ptr<Foo>. Callsite вообще не знает о реализации, только интерфейс.

Результат будет таким, как ожидалось:

simple foo
fancy foo