Как я могу использовать ковариантные типы возврата с помощью интеллектуальных указателей?
У меня есть такой код:
class RetInterface {...}
class Ret1: public RetInterface {...}
class AInterface
{
public:
virtual boost::shared_ptr<RetInterface> get_r() const = 0;
...
};
class A1: public AInterface
{
public:
boost::shared_ptr<Ret1> get_r() const {...}
...
};
Этот код не компилируется.
В визуальной студии это поднимает
C2555: переопределение типа возвращаемой виртуальной функции отличается и не является ковариантным
Если я не использую boost::shared_ptr
но возвращаю необработанные указатели, код компилируется (я понимаю, это связано с ковариантными типами возврата в C++). Я вижу проблему в том, что boost::shared_ptr
из Ret1
не является производным от boost::shared_ptr
из RetInterface
. Но я хочу вернуть boost::shared_ptr
из Ret1
для использования в других классах, иначе я должен привести приведенное значение после возврата.
- Я делаю что-то неправильно?
- Если нет, то почему язык таков - он должен быть расширяемым для обработки преобразования между умными указателями в этом сценарии? Есть ли желательный обходной путь?
Ответы
Ответ 1
Во-первых, это действительно то, как это работает в С++: тип возврата виртуальной функции в производном классе должен быть таким же, как в базовом классе. Существует специальное исключение, что функция, возвращающая ссылку/указатель на некоторый класс X, может быть переопределена функцией, которая возвращает ссылку/указатель на класс, который происходит от X, но, как вы заметили, это не позволяет использовать интеллектуальные указатели (например, shared_ptr
), только для простых указателей.
Если ваш интерфейс RetInterface
является достаточно полным, вам не нужно будет знать фактический возвращаемый тип в вызывающем коде. В общем, это не имеет никакого смысла: причина get_r
- это функция virtual
, в первую очередь, потому что вы будете называть ее указателем или ссылкой на базовый класс AInterface
, и в этом случае вы можете Не знаю, какой тип возвращаемого производного класса. Если вы вызываете это с помощью фактической ссылки A1
, вы можете просто создать отдельную функцию get_r1
в A1
, которая сделает то, что вам нужно.
class A1: public AInterface
{
public:
boost::shared_ptr<RetInterface> get_r() const
{
return get_r1();
}
boost::shared_ptr<Ret1> get_r1() const {...}
...
};
В качестве альтернативы вы можете использовать шаблон посетителя или что-то вроде метода Dynamic Double Dispatch для передачи обратного вызова в возвращаемый объект, который затем может вызывают обратный вызов с правильным типом.
Ответ 2
Вы не можете изменять типы возвращаемых данных (для типов без указателей, без ссылок) при перегрузке в С++. A1::get_r
должен возвращать boost::shared_ptr<RetInterface>
.
У Энтони Уильямса хороший всеобъемлющий ответ .
Ответ 3
Как насчет этого решения:
template<typename Derived, typename Base>
class SharedCovariant : public shared_ptr<Base>
{
public:
typedef Base BaseOf;
SharedCovariant(shared_ptr<Base> & container) :
shared_ptr<Base>(container)
{
}
shared_ptr<Derived> operator ->()
{
return boost::dynamic_pointer_cast<Derived>(*this);
}
};
например:
struct A {};
struct B : A {};
struct Test
{
shared_ptr<A> get() {return a_; }
shared_ptr<A> a_;
};
typedef SharedCovariant<B,A> SharedBFromA;
struct TestDerived : Test
{
SharedBFromA get() { return a_; }
};
Ответ 4
Вот моя попытка:
template<class T>
class Child : public T
{
public:
typedef T Parent;
};
template<typename _T>
class has_parent
{
private:
typedef char One;
typedef struct { char array[2]; } Two;
template<typename _C>
static One test(typename _C::Parent *);
template<typename _C>
static Two test(...);
public:
enum { value = (sizeof(test<_T>(nullptr)) == sizeof(One)) };
};
class A
{
public :
virtual void print() = 0;
};
class B : public Child<A>
{
public:
void print() override
{
printf("toto \n");
}
};
template<class T, bool hasParent = has_parent<T>::value>
class ICovariantSharedPtr;
template<class T>
class ICovariantSharedPtr<T, true> : public ICovariantSharedPtr<typename T::Parent>
{
public:
T * get() override = 0;
};
template<class T>
class ICovariantSharedPtr<T, false>
{
public:
virtual T * get() = 0;
};
template<class T>
class CovariantSharedPtr : public ICovariantSharedPtr<T>
{
public:
CovariantSharedPtr(){}
CovariantSharedPtr(std::shared_ptr<T> a_ptr) : m_ptr(std::move(a_ptr)){}
T * get() final
{
return m_ptr.get();
}
private:
std::shared_ptr<T> m_ptr;
};
И небольшой пример:
class UseA
{
public:
virtual ICovariantSharedPtr<A> & GetPtr() = 0;
};
class UseB : public UseA
{
public:
CovariantSharedPtr<B> & GetPtr() final
{
return m_ptrB;
}
private:
CovariantSharedPtr<B> m_ptrB = std::make_shared<B>();
};
int _tmain(int argc, _TCHAR* argv[])
{
UseB b;
UseA & a = b;
a.GetPtr().get()->print();
}
Пояснения:
Это решение подразумевает метапрограммирование и изменение классов, используемых в ковариантных интеллектуальных указателях.
Простая структура шаблона Child
предназначена для привязки типа Parent
и наследования. Любой класс, наследующий от Child<T>
, наследует от T
и определяет T
как Parent
. Классы, используемые в ковариантных интеллектуальных указателях, должны определить этот тип.
Класс has_parent
используется для обнаружения во время компиляции, если класс определяет тип Parent
или нет. Эта часть не моя, я использовал один и тот же код, чтобы определить, существует ли метод (см. Здесь)
Поскольку мы хотим ковариации с интеллектуальными указателями, мы хотим, чтобы наши интеллектуальные указатели имитировали существующую архитектуру классов. Легче объяснить, как это работает в этом примере.
Когда a CovariantSharedPtr<B>
определен, он наследует от ICovariantSharedPtr<B>
, который интерпретируется как ICovariantSharedPtr<B, has_parent<B>::value>
. Поскольку B
наследует от Child<A>
, has_parent<B>::value
является истинным, поэтому ICovariantSharedPtr<B>
является ICovariantSharedPtr<B, true>
и наследуется от ICovariantSharedPtr<B::Parent>
, который равен ICovariantSharedPtr<A>
. Поскольку A
не имеет Parent
, has_parent<A>::value
является ложным, ICovariantSharedPtr<A>
является ICovariantSharedPtr<A, false>
и наследуется от ничего.
Главное, что B
наследует от A
, мы имеем ICovariantSharedPtr<B>
, наследующий от ICovariantSharedPtr<A>
. Таким образом, любой метод, возвращающий указатель или ссылку на ICovariantSharedPtr<A>
, может быть перегружен методом, возвращающим то же самое на ICovariantSharedPtr<B>
.
Ответ 5
В этом блоге опубликовано аккуратное решение (от Рауля Борхеса)
Выдержка из бита перед добавлением поддержки множественного наследования и абстрактных методов:
template <typename Derived, typename Base>
class clone_inherit<Derived, Base> : public Base
{
public:
std::unique_ptr<Derived> clone() const
{
return std::unique_ptr<Derived>(static_cast<Derived *>(this->clone_impl()));
}
private:
virtual clone_inherit * clone_impl() const override
{
return new Derived(*this);
}
};
class concrete: public clone_inherit<concrete, cloneable>
{
};
int main()
{
std::unique_ptr<concrete> c = std::make_unique<concrete>();
std::unique_ptr<concrete> cc = b->clone();
cloneable * p = c.get();
std::unique_ptr<clonable> pp = p->clone();
}
Я бы посоветовал прочитать статью полностью. Это просто написано и хорошо объяснено.
Ответ 6
Mr Fooz ответил на часть 1 вашего вопроса. Часть 2, он работает таким образом, потому что компилятор не знает, будет ли он называть AInterface:: get_r или A1:: get_r во время компиляции - он должен знать, какую возвращаемую стоимость он получит, поэтому он настаивает на обоих методах возвращая тот же самый тип. Это часть спецификации С++.
Для обходного пути, если A1:: get_r возвращает указатель на RetInterface, виртуальные методы в RetInterface будут работать, как ожидалось, и соответствующий объект будет удален при уничтожении указателя. Нет необходимости в разных типах возврата.
Ответ 7
Возможно, вы могли бы использовать параметр out, чтобы обойти "ковариацию с возвратом boost shared_ptrs".
void get_r_to(boost::shared_ptr<RetInterface>& ) ...
поскольку я подозреваю, что вызывающий может отказаться от более уточненного типа shared_ptr в качестве аргумента.