Правильное поведение с помощью виртуальных методов

Предположим, у меня есть чисто виртуальный метод в интерфейсе base, который возвращает мне список something:

class base 
{ 
public:
     virtual std::list<something> get() = 0; 
}; 

Предположим, у меня есть два класса, которые наследуют класс base:

class A : public base 
{ 
public:
     std::list<something> get();
}; 

class B : public base 
{ 
public:
     std::list<something> get(); 
};

Я хочу, чтобы только класс A мог возвращать list<something>, но мне также нужно иметь возможность получить список с помощью указателя base, например:

base* base_ptr = new A();
base_ptr->get();

Что мне нужно сделать?

Должен ли я вернуть указатель на этот список? Ссылка?

Должен ли я возвращать нулевой указатель из метода класса B? Или я должен генерировать исключение, когда я пытаюсь получить список, используя объект B? Или я должен изменить метод класса base get, сделав его нечистым, и сделать это в классе base?

Должен ли я сделать что-то еще?


person Nick    schedule 06.07.2012    source источник
comment
Если это относится только к class A, не должно ли это быть только в class A, а не в Base?   -  person Naveen    schedule 06.07.2012
comment
@Naveen да, но, как я уже писал, мне нужна возможность получить список, используя указатель на класс base.   -  person Nick    schedule 06.07.2012
comment
@Nick Тогда, вероятно, class B не должен быть подклассом Base. Что-то может быть сломано в конструкции.   -  person betabandido    schedule 06.07.2012
comment
Почему? Если у вас есть указатель на base, вы всегда можете привести его к A *, либо используя static_cast, либо используя dynamic_cast, в зависимости от того, знаете ли вы конкретный тип. (Редактировать: изменено A на A *.)   -  person    schedule 06.07.2012
comment
@betabandido Я не могу, мне нужна также возможность хранить объекты A и B, например, в одном списке.   -  person Nick    schedule 06.07.2012
comment
@hvd Я предпочитаю ничего не бросать, я хочу использовать виртуальную функцию.   -  person Nick    schedule 06.07.2012
comment
Опять же, почему? Вы хотите использовать виртуальную функцию, когда нет очевидного смысла использовать виртуальную функцию, поэтому было бы действительно полезно, если бы вы объяснили подробнее.   -  person    schedule 06.07.2012
comment
Тут виноват дизайн. Если вам приходится полагаться на неявное знание того, на какой конкретный тип указывает указатель базового класса, чтобы вы могли его привести, вы также можете не передавать указатель базового класса, потому что вы уже потеряли преимущества полиморфизма. . В этом случае B концептуально явно не является base, поэтому не должно быть производным от base.   -  person razlebe    schedule 06.07.2012
comment
@hvd Больше информации? См. это и это и это.   -  person Nick    schedule 06.07.2012
comment
Пункт Мейерса 32: Make sure public inheritance models "is-a". Все, что верно для базового класса, должно быть верно и для производного класса. Если B действительно является Base и вызов get() на B недействителен, то вызов get() на Base должен быть недействителен.   -  person BoBTFish    schedule 06.07.2012
comment
@BoBTFish Хорошо, можно ссылку?   -  person Nick    schedule 06.07.2012
comment
@Nick Если я правильно прочитал эти другие вопросы, единственная причина, по которой у вас есть базовый класс, - это возможность хранить A и B в одном списке. И я полагаю, что вы сейчас делаете итерацию по списку (имеется в виду, что вы получаете base *) и пытаетесь вызвать get()? Что вы хотите, чтобы произошло, когда ваш список содержит B объектов?   -  person    schedule 06.07.2012
comment
@hvd да, вы правильно поняли. B — это просто тип объекта, который имеет что-то общее с A, но не все. Не могли бы вы лучше перефразировать свой вопрос?   -  person Nick    schedule 06.07.2012
comment
@Nick Я имею в виду, что B::get() не должен работать, ваш список может содержать объекты B, и вы не хотите проверять, является ли объект объектом A (dynamic_cast), так что вы хотите, чтобы base->get() делал, когда base указывает на B объект?   -  person    schedule 06.07.2012


Ответы (3)


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

Когда вы получаете указатель на базовый класс, поскольку метод был объявлен в базовом классе и является виртуальным, фактическая реализация будет найдена в таблице виртуальных функций класса и вызвана соответствующим образом.

So

base* base_ptr = new A();
base_ptr->get();

Будет вызывать A::get(). Вы не должны возвращать null из реализации (ну, вы не можете, так как null не может быть преобразован в std::list‹ что-то в любом случае). Вы должны предоставить реализацию в A/B, так как метод базового класса объявлен чисто виртуальным.

ИЗМЕНИТЬ:

вы не можете иметь только A, возвращающий std::list‹ что-то ›, но не B, поскольку B также наследует базовый класс, а базовый класс имеет чисто виртуальный метод, который должен быть переопределен в производном классе. Наследование от базового класса является отношением «является». Единственным другим способом, который я мог видеть, было бы частное наследование от класса, но это предотвратило бы преобразование производного в базовое.

Если вы действительно не хотите, чтобы B имел метод get, не наследуйте его от base. Некоторые альтернативы:

Вызов исключения в B::get(): Вы можете выдать исключение в B::get(), но убедитесь, что вы хорошо объяснили свое обоснование, поскольку оно противоречит интуиции. ИМХО, это довольно плохой дизайн, и вы рискуете запутать людей, используя свой базовый класс. Это дырявая абстракция, и ее лучше избегать.

Отдельный интерфейс: вы можете разбить базу на отдельный интерфейс:

class IGetSomething
{
public:
    virtual ~IGetSomething() {}
    virtual std::list<something> Get() = 0;
};

class base
{
public:
    // ...
};

class A : public base, public IGetSomething
{
public:
    virtual std::list<something> Get()
    {
        // Implementation
        return std::list<something>();
    }
};

class B : public base
{

};

Множественное наследование в этом случае допустимо, поскольку IGetSomething — это чистый интерфейс (у него нет переменных-членов или нечистых методов).

EDIT2:

Основываясь на комментариях, кажется, что вы хотите иметь общий интерфейс между двумя классами, но при этом иметь возможность выполнять некоторые операции, которые выполняет одна реализация, но не предоставляет другая. Это довольно запутанный сценарий, но мы можем черпать вдохновение из COM (пока не стреляйте в меня):

class base
{
public:
    virtual ~base() {}
    // ... common interface

    // TODO: give me a better name
    virtual IGetSomething *GetSomething() = 0;
};

class A : public Base
{
public:
    virtual IGetSomething *GetSomething()
    {
        return NULL;
    }
};

class B : public Base, public IGetSomething
{
public:
    virtual IGetSomething *GetSomething()
    {
        // Derived-to-base conversion OK
        return this;
    }
};

Теперь вы можете сделать следующее:

base* base_ptr = new A();
IGetSomething *getSmthing = base_ptr->GetSomething();
if (getSmthing != NULL)
{
    std::list<something> listOfSmthing = getSmthing->Get();
}

Это запутанно, но есть несколько преимуществ этого метода:

  • Вы возвращаете общедоступные интерфейсы, а не конкретные классы реализации.

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

  • Его трудно использовать по ошибке: base не предоставляет std::list get(), потому что это не обычная операция между конкретной реализацией.

  • #P17# <блочная цитата> #P18#

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

Как насчет того, чтобы просто вернуть boost::Optional‹ std::list‹ что-то › › ? (как предложил Андрей)

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

Недостатком является то, что он помещает boost внутри вашего интерфейса, чего я предпочитаю избегать (я могу использовать boost, но клиенты интерфейса не должны быть вынуждены использовать boost).

person Julien Lebot    schedule 06.07.2012
comment
Хорошо, но если я напишу base* base_ptr = new B();, как поведет себя base_ptr->get();? - person Nick; 06.07.2012
comment
Я полагаю, что OP спрашивает, как мне избежать необходимости предоставлять реализацию в B(), но при этом иметь возможность вызывать A::get() из указателя базового класса. - person razlebe; 06.07.2012
comment
Я изменил свой ответ, чтобы отразить это. Короче, поведение такое же. Поскольку вы наследуете базовый класс, вы должны реализовать его чисто виртуальные функции. - person Julien Lebot; 06.07.2012
comment
@LeSnip3R razlebe объясняет, о чем я спрашиваю, что я должен вернуть, используя B::get()? - person Nick; 06.07.2012
comment
Когда вы говорите If you really don't want B to have the get method, don't inherit from base., вы не понимаете моего вопроса, но идея IGetSomething может быть хорошей, поэтому +1 - person Nick; 06.07.2012
comment
В любом случае помните, что таким образом я не могу сделать это: base_ptr->get(). Так что это не то, что я хочу. - person Nick; 06.07.2012
comment
Вам нужно будет расширить свой вопрос, чтобы лучше объяснить это. Я добавлю к моему ответу третью альтернативу, которая может делать то, что вы хотите. - person Julien Lebot; 06.07.2012
comment
@ LeSnip3R Большое спасибо. - person Nick; 06.07.2012

вернуть boost::optional в случае, если вам нужна возможность не возвращаться (в классе B)

class base 
{ 
public:
     virtual boost::optional<std::list<something> > get() = 0; 
}; 
person Andrew    schedule 06.07.2012
comment
Мне это нравится, за исключением того факта, что он ставит ускорение в базовый класс. Ничего не имею против буста, но не заставляю клиентов интерфейса его использовать. - person Julien Lebot; 06.07.2012

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

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

person go4sri    schedule 06.07.2012