Как я могу использовать ковариантные типы возврата с умными указателями? - PullRequest
59 голосов
/ 13 октября 2008

У меня есть такой код:

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. Я что-то не так делаю?
  2. Если нет, то почему такой язык - он должен быть расширяемым для обработки преобразования между умными указателями в этом сценарии? Есть ли желательный обходной путь?

Ответы [ 6 ]

23 голосов
/ 13 октября 2008

Во-первых, это действительно так в C ++: возвращаемый тип виртуальной функции в производном классе должен быть таким же, как и в базовом классе. Существует специальное исключение, что функция, которая возвращает ссылку / указатель на некоторый класс 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 {...}
     ...
};

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

1 голос
/ 11 сентября 2015

Вот моя попытка:

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 или нет. Эта часть не моя, я использовал тот же код, что и для определения, существует ли метод ( см. Здесь )

Поскольку мы хотим ковариации с умными указателями, мы хотим, чтобы наши умные указатели имитировали существующую классовую архитектуру. Проще объяснить, как это работает на примере.

Когда определено CovariantSharedPtr<B>, оно наследуется от ICovariantSharedPtr<B>, что интерпретируется как ICovariantSharedPtr<B, has_parent<B>::value>. Так как B наследует от Child<A>, has_parent<B>::value имеет значение true, поэтому 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>.

1 голос
/ 18 апреля 2013

А как насчет этого решения:

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_; }
};
1 голос
/ 13 октября 2008

Вы не можете изменять типы возвращаемых данных (для не указательных, нереферентных типов возвращаемых данных) при перегрузке методов в C ++. A1::get_r должен вернуть boost::shared_ptr<RetInterface>.

У Энтони Уильямса хороший всеобъемлющий ответ .

0 голосов
/ 09 июня 2009

возможно, вы могли бы использовать параметр out для обхода "ковариации с возвращенным бустом shared_ptrs.

 void get_r_to(boost::shared_ptr<RetInterface>& ) ...

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

0 голосов
/ 13 октября 2008

Mr Fooz ответил на часть 1 вашего вопроса. Во второй части это работает так, потому что компилятор не знает, будет ли он вызывать AInterface :: get_r или A1 :: get_r во время компиляции - ему нужно знать, какое возвращаемое значение он получит, поэтому он настаивает на обоих методах. возвращая тот же тип. Это часть спецификации C ++.

Для временного решения, если A1 :: get_r возвращает указатель на RetInterface, виртуальные методы в RetInterface будут по-прежнему работать должным образом, и соответствующий объект будет удален, когда указатель будет уничтожен. Нет необходимости в разных типах возврата.

...