Как заставить все производные классы реализовать виртуальный метод?

Скажем, у вас есть базовый класс Dep для дерева классов. Существует виртуальный метод Dep* Dep::create(), который я хочу реализовать в каждом отдельном листовом классе. Есть ли способ обеспечить это?

Примечание. Проблема здесь в том, что могут быть промежуточные классы (скажем, class B : public A : public Dep), реализующие этот метод (A::create) случайно или потому, что они думают, что они конечные классы, но на самом деле сами являются подклассами.

На этом вопрос заканчивается.

Контекст

Если вам интересно, зачем мне это нужно; У меня есть класс Master, в котором есть Dep объектов неизвестного конкретного типа. Если Master дублируется, мне нужно придумать соответствующий клон экземпляра Dep. Следующее, что лучше всего сделать, это идиома виртуального конструктора, которая вводит именно эту проблему.

Кроме того, я даже не могу поймать это (кроме ужасного сбоя), потому что по неясным причинам люди, которым есть что сказать больше, чем я, объявили dynamic_cast вне закона в этом проекте (возможно, это хорошее решение; но в любом случае это совсем другой разговор) .


person bitmask    schedule 02.09.2011    source источник
comment
Когда происходит сбой, согласуется ли трассировка стека?   -  person Tom Kerr    schedule 03.09.2011
comment
@Tom K: Под этим я подразумевал следующее: я должен static_cast указать Dep указателей на то, во что я верю. Поэтому, если бы они были созданы с неправильным типом, я бы превратил их во что-то, что, скорее всего, совершенно несовместимо (другой лист).   -  person bitmask    schedule 03.09.2011
comment
Хм, а можно ли потребовать, чтобы каждый класс наследовал какой-то дозорный класс? Если бы у вас было class T : public Base, public CloneTest<T>, то CloneTest<T> могло бы содержать typedef для declspec(this->T::clone()), что потерпело бы неудачу (?), если бы не было переопределения.   -  person Kerrek SB    schedule 03.09.2011
comment
@Kerrek SB: Разве это не просто приводит к тому, что проблема наследуется от CloneTest, или я что-то упустил?   -  person bitmask    schedule 03.09.2011
comment
@Kerrek также не требует особого внимания.   -  person Seth Carnegie    schedule 03.09.2011
comment
Да, чего следует избегать путем редизайна, вероятно. Если это так, то вы должны попытаться сделать так, чтобы им было трудно сделать это случайно. Если вы быстро выйдете из строя из-за ошибки seg, она не просочится в производственный код, если только они не проявят преднамеренную небрежность. Если трассировка стека непротиворечива, вы можете оставить комментарий, где они ее найдут.   -  person Tom Kerr    schedule 03.09.2011
comment
@bitmask: Да, это просто сместило бы проблему, но сместило бы ее в место, которое вам было бы легче проверить с помощью grep...   -  person Kerrek SB    schedule 03.09.2011


Ответы (5)


Используя любопытно повторяющиеся шаблоны, вы можете добиться чего-то очень похожего:

template<typename T>
class Cloneable : public T, public Dep
{
private:
    Cloneable<T>() : T() { }
public:
    static Cloneable<T>* Create() { return new Cloneable<T>(); }
    Cloneable<T>* clone() { return new Cloneable<T>(*this); }
};

Вместо наследования от Dep и создания экземпляра через new MyType используйте Cloneable<MyType>::Create. Поскольку Cloneable<MyType> является производным от MyType, вы можете использовать экземпляр так же, как и любой MyType, за исключением того, что теперь он гарантированно содержит Dep::clone.

Кроме того, ваш Master не должен принимать экземпляр типа Dep, но должен обеспечивать, чтобы он был Cloneable<T>. (Замените исходную функцию простым шаблоном функции, который обеспечивает это.) Это гарантирует, что любой Dep внутри мастера имеет правильно реализованную функцию clone.

Поскольку у Cloneable<MyType> нет открытого конструктора, его нельзя наследовать, однако ваш фактический MyType можно в дальнейшем наследовать и использовать так же, как и раньше.

person gha.st    schedule 03.09.2011
comment
Я реализовал очень похожий подход, но то, что вы называете T, по-прежнему является конечным классом, но содержит объект Clonerявляется Clonable, унаследованным от базы Dep). Я сделал это для некоторых вторичных ограничений ... тем не менее, ваш ответ ближе к моему первоначальному вопросу. Чувак, почему я не подумал об этом? - person bitmask; 04.09.2011

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

Например, A может наследоваться от Def и реализовать все его [чистые] виртуальные методы. Тогда, если B наследуется от A, ему не нужно ничего реализовывать. Нет никакого способа предотвратить это.

Так что ответ - нет, нет никакого способа обеспечить это.

person Seth Carnegie    schedule 02.09.2011
comment
Да, я знаю об этом. Отсюда вопрос. - person bitmask; 03.09.2011
comment
@bitmask ты знал, что это невозможно сделать, зачем спрашивать? Это ответ на ваш вопрос: нет. - person Seth Carnegie; 03.09.2011
comment
Нет, я знаю, как работает наследование (поэтому мое утверждение относится к вашему второму абзацу). Также я знал, что насколько мне известно, ответ - нет, но я действительно надеялся, что более умный человек, чем я, найдет решение. - person bitmask; 03.09.2011
comment
@bitmask Я тоже надеюсь, что кто-то умнее нас найдет решение. Хотя я сильно сомневаюсь. - person Seth Carnegie; 03.09.2011
comment
Каким-то образом это нарушает принцип ООП, так что может быть это и хорошо, но посмотрим. :) - person bitmask; 03.09.2011
comment
@Seth - хотя C ++ не предоставляет языковую функцию специально для предотвращения создания подклассов, есть способы сделать это - см. parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.11 . - person Andy Thomas; 03.09.2011

TPTB запретил все RTTI или только dynamic_cast<>()? Если вы можете использовать RTTI, вы можете обеспечить существование метода в качестве постусловия его вызова:

#include <typeinfo>
#include <cassert>
#include <iostream>
#include <stdexcept>

class Base {
protected:
  virtual Base* do_create() = 0;
  virtual ~Base() {}
public:
  Base* create() {
    Base *that = this->do_create();
    if( typeid(*this) != typeid(*that) ) {
      throw(std::logic_error(std::string() +
                             "Type: " +
                             typeid(*this).name() +
                             " != " +
                             typeid(*that).name()));
    }
    return that;
  }
};

class Derive1 : public Base {
protected:
  Base* do_create() { return new Derive1(*this); }
};

class Derive2 : public Derive1 {};

void f(Base*p) { std::cout << typeid(*p).name() << "\n"; }
int main() {
  Derive1 d1;
  Base *pD1 = d1.create(); // will succeed with correct semantics
  Derive2 d2;
  Base *pD2 = d2.create(); // will throw exception due to missing Derive2::do_create()
}
person Robᵩ    schedule 02.09.2011
comment
К сожалению (или лучше сказать к счастью?) деактивированы все RTTI, не только dynamic_cast. Но хороший подход, тем не менее! - person bitmask; 03.09.2011
comment
+1 - Черт возьми, у меня была такая же идея. Это использует NVI, невиртуальную идиому, которая позволяет базовому классу прикреплять пост-условия. Хотя это не отвечает заголовку вопроса, оно удовлетворяет конкретному контексту, указанному автором, если бы не это досадное полное отсутствие RTTI. :) - person Andy Thomas; 03.09.2011
comment
@bitmask, если вы хотите использовать этот подход и если у вас есть доступ ко всем сборкам для подклассов, вы можете просто использовать этот подход в независимой тестовой сборке, где включен RTTI, чтобы убедиться, что все реализовано правильно. то в штатном билде проверка просто бы не производилась. - person justin; 03.09.2011

Если вы управляете базовым классом AbstractDep, вы можете принудительно создать конкретные конечные классы с помощью шаблона класса WithCloning. Затем этот лист можно запечатать, чтобы он не мог быть унаследован. Или, точнее, экземпляры производного класса не могут быть созданы.

class AbstractDep
{
template< class Type > friend class WithCloning;
private:
    enum FooFoo {};
    virtual FooFoo toBeImplementedByLeafClass() = 0;

public:
    virtual AbstractDep* clone() const = 0;
};

template< class Type > class WithCloning;

class Sealed
{
template< class Type > friend class WithCloning;
private:
    Sealed() {}
};

template< class Type >
class WithCloning
    : public Type
    , public virtual Sealed
{
private:
    AbstractDep::FooFoo toBeImplementedByLeafClass()
    {
        return AbstractDep::FooFoo();
    }

public:
    virtual WithCloning* clone() const
    {
        return new WithCloning( *this );
    }
};

typedef WithCloning<AbstractDep>            Dep;


class AbstractDerivedDep
    : public AbstractDep
{
// AbstractDep::FooFoo toBeImplementedByLeafClass(); // !Not compile.
public:
};

typedef WithCloning<AbstractDerivedDep>     DDep;


struct Foo: Dep {};       // !Does not compile if instantiated.

int main()
{
    Dep     d;
    //Foo     f;
}

Если классы требуют большего, чем конструкция по умолчанию, то большинство из них можно решить дополнительно.

Одним из решений является пересылка пакета аргументов из конструктора WithCloning (в моем блоге есть пример реализации C++98, и C++0x поддерживает его напрямую).

Подводя итог, для создания экземпляра класс должен быть WithCloning.

Ура и чт.,

person Cheers and hth. - Alf    schedule 02.09.2011

когда вы говорите, что они неизвестны, я предполагаю, что они все еще наследуют от общего базового класса/интерфейса, верно?

единственное, что, как мне кажется, можно использовать для принудительного добавления виртуального базового класса

virtual Base* clone() {
    assert( 1==0 ); //needs to be overriden
}

поэтому вы вынуждены переопределить, но это обнаруживается только во время выполнения при попытке вызвать клон для экземпляра класса, в котором отсутствует переопределение

даже если вам не разрешено использовать dynamic_cast или RTTI, вы все равно можете включить его для целей отладки локально в вашей сборке, если это поможет вам найти typeid классов-нарушителей.

похоже, вы знакомы с шаблоном клонирования, но я тихо опубликую его здесь, и мы можем забыть о нем: http://www.cplusplus.com/forum/articles/18757/

person lurscher    schedule 02.09.2011
comment
Это не заставляет конечные классы переопределять clone. Единственное, что это делает, это примерно то же самое, что делает его чистым виртуальным. - person Seth Carnegie; 03.09.2011
comment
Да, все они наследуют от Dep. Ваш метод, по сути, является не принудительным чисто виртуальным методом. Таким образом, промежуточные классы по-прежнему могут реализовывать методы для своих дочерних элементов. - person bitmask; 03.09.2011