Копирование и замена через интерфейсы
Я пытаюсь реализовать идиому "копировать + своп" для достижения безопасности с сильными исключениями через уровень абстракции и, хотя принцип ясен, как это часто бывает, дьявол кроется в деталях.
Скажем, у меня есть класс, который выглядит так:
class AConcreteType :
public ISomething,
public ISwappable
{
public:
// From ISwappable
void Swap( ISwappable& );
};
Теперь я могу сделать это в методе, который имеет дело только с ISomething:
void AClass::DoSomething( ISomething& something )
{
// say there is a function that allows me to clone 'something'
// Probably it ought to go into an auto_ptr, but for clarity:
ISomething& somethingElse( clone( something ) );
// ... so that at the end, after doing stuff with somethingElese I can do
ISwappable& swappable1 = dynamic_cast<ISwappable&>( something );
ISwappable& swappable2 = dynamic_cast<ISwappable&>( somethingElse );
// ... I may want to check that the concrete types behind the interface are
// actually the same too with something like typeid, but I'll leave that out for clarity
swappable1.Swap( swappable2 );
}
где
void AConcreteType::Swap( ISwappable& swappable )
{
AConcreteType& somethingConcrete = dynamic_cast<AConcreteType&>(swappable);
std::swap( *this, somethingConcrete );
}
Это все работает, так как все dynamic_casts находятся на ссылках, что является операцией, которая выдает, когда тип не поддерживается; это оставляет мои объекты в хорошем состоянии, так как обмен не происходит до самого конца. Но что меня не устраивает, так это то, что вызов swappable1.Swap(swappable2) все еще может генерировать (через тот же механизм dynamic_cast), и это было бы нелогичным для пользователя Swap, поскольку он, вероятно, ничего не ожидал бросить в этот момент.
Альтернативой, о которой я подумал, был шаблон ISwappable, чтобы покончить с dynamic_cast внутри реализации Swap:
template< typename T >
class ISwappable
{
public:
virtual void Swap( T& ) = 0;
};
так что его реализация просто
class AConcreteType :
public ISomething,
public ISwappable<AConcreteType>
{
void Swap( AConcreteType& act ) { std::swap( *this, act ); }
};
Это позволяет вызову Swap быть безбросовым (и позволяет мне гарантировать, что два объекта действительно могут быть заменены во время компиляции), но теперь проблема заключается в том, что мне приходится иметь дело с конкретным типом внутри DoSomething, но я не у него нет доступа к AConcreteType внутри этой функции.
Есть идеи?
2 ответа
C++ не особенно ориентирован на интерфейсы, основанные на наследовании. Например, вы реализуете функцию, которая принимает ISomething, но также ожидает, что объект будет ISwappable. Языки, ориентированные на использование таких интерфейсов, как правило, имеют прямой способ выражения требований для нескольких интерфейсов одного типа.
Вместо этого, вероятно, лучше в C++ использовать шаблоны, а затем выражать требования к этим параметрам шаблона, когда это необходимо. Статические утверждения и черты типа - довольно простой и читаемый способ сделать это в C++.
template<typename T,typename Interface>
struct implements {
static constexpr bool value = std::is_base_of<Interface,T>::value;
}
template<typename T>
void AClass::DoSomething(T &something ) {
static_assert(implements<T,ISomething>::value, "requires ISomething");
static_assert(implements<T,ISwappable<T>>::value, "requires ISwappable");
T somethingElse = clone(something);
something.Swap(somethingElse);
}
Вы также можете отказаться от использования наследования для интерфейсов вообще. Обычно вы можете получить статическую проверку типов в ваших классах через static_asserts и черты типа без наследования:
template<typename T>
struct is_swappable { static constexpr bool value = ... };
class AConcreteType {
...
};
static_assert(is_swappable<AConcreteType>,"...");
template<typename T>
void AClass::DoSomething(T &something ) {
static_assert(is_something<T>::value, "requires something");
static_assert(is_swappable<T>::value, "requires swappable");
Если вы спросите меня, идея ISwappable
уже "некорректно", так как вы не можете поменять абстрактные типы друг на друга без последствий... То, что вы можете безопасно поменять, это адреса интерфейсов (указатели):
std::unique_ptr<ISomething> tI1(new AConcreteType(1)), tI2(new BConcreteType(2));
std::cout << tI1->IdentifyYourSelf() << std::endl; // -> prints "1"
std::cout << tI2->IdentifyYourSelf() << std::endl; // -> prints "2"
tI1.swap(tI2);
// contents are swapped now
std::cout << tI1->IdentifyYourSelf() << std::endl; // -> prints "2"
std::cout << tI2->IdentifyYourSelf() << std::endl; // -> prints "1"