2009-03-12 6 views
16

Mam wrażenie, że wcześniej ktoś o to pytał, ale nie mogę go znaleźć na SO, ani nie mogę znaleźć niczego przydatnego w Google. Może "kowariancja" nie jest słowem, którego szukam, ale ta koncepcja jest bardzo podobna do kowariantnych typów zwrotów funkcji, więc myślę, że to prawdopodobnie jest poprawne. Oto, co chcę zrobić i to daje mi błąd kompilatora:Szablony covariant w C++

class Base; 
class Derived : public Base; 

SmartPtr<Derived> d = new Derived; 
SmartPtr<Base> b = d; // compiler error 

Przyjmijmy te zajęcia są w pełni uregulowana ... Myślę, że masz pomysł. Nie można przekonwertować wartości SmartPtr<Derived> na SmartPtr<Base> z jakiegoś niejasnego powodu. Pamiętam, że jest to normalne w C++ i wielu innych językach, choć w tej chwili nie pamiętam dlaczego.

Moje główne pytanie brzmi: jaki jest najlepszy sposób wykonania tej operacji przypisania? Obecnie wyciągam wskaźnik z SmartPtr, wyraźnie upcasting go do typu podstawowego, a następnie owijając go w nowy SmartPtr odpowiedniego typu (należy pamiętać, że nie jest to przeciekające zasoby, ponieważ nasza rodzima klasa SmartPtr używa nachalnych odniesień rachunkowość). To długo i niechlujnie, zwłaszcza, gdy muszę następnie owinąć SmartPtr w jeszcze jeden obiekt ... dowolne skróty?

Odpowiedz

11

Zarówno konstruktor kopiowania, jak i operator przypisania, powinni mieć możliwość pobrania SmartPtr innego typu i próby skopiowania wskaźnika z jednego do drugiego. Jeśli typy nie są kompatybilne, kompilator będzie narzekał, a jeśli są one zgodne, to rozwiązałeś swój problem. Coś takiego:

template<class Type> class SmartPtr 
{ 
    .... 
    template<class OtherType> SmartPtr(const SmartPtr<OtherType> &blah) // same logic as the SmartPtr<Type> copy constructor 

    template<class OtherType> SmartPtr<Type> &operator=(const SmartPtr<OtherType> &blah) // same logic as the SmartPtr<Type> assignment operator 
}; 
+0

ukryj komentarze @MSN: Nauczyłem się przez próbę i błąd, że to nie wystarcza do spełnienia "normalnego" konstruktora kopiowania i operatora przypisania. Musisz więc zaimplementować oba: SmartPtr (const SmartPtr &) I szablon SmartPtr (const SmartPtr &) (to samo dla op =) – mmmmmmmm

+0

No tak, właśnie o to mi chodziło :) – MSN

+0

Od C++ 11 lat dodajesz również move-constructor i move-assignment (te z '&&'). –

3

Zależy od klasy SmartPtr. Jeśli ma konstruktora kopiowania (lub w twoim przypadku operatora przypisania), który zajmuje SmartPtr<T>, gdzie T jest typem, z którym został zbudowany, to nie zadziała, ponieważ SmartPtr<T1> nie ma związku z SmartPtr<T2>, nawet jeśli T1 i T2 są powiązane z dziedziczeniem.

Jeśli jednak SmartPtr ma templatized operatora kopii konstruktor/przydziału, z parametrem szablonu TOther, który przyjmuje SmartPtr<TOther>, to powinien działać.

+0

wydaje się to genialne ... muszę przetestować ten ASAP, aby sprawdzić, czy zadziała. Niestety, używam MSVC6, który ma główne problemy z szablonami, a IIRC będzie barfował na templatyzowanych funkcjach w klasach templatyzowanych, nawet jeśli jawnie określisz parametry szablonu podczas wywoływania go. – rmeador

+0

Naprawdę? - Wiem, że zrobiłem to dokładnie pod MSVC6. – Eclipse

+0

Napisałem klasę SmartPtr, która robi to dobrze pod MSVC6, więc powinieneś być w porządku. –

2

Zakładając, że mamy kontrolę nad klasie SmartPtr, rozwiązaniem jest zapewnienie szablonie konstruktora:

template <class T> 
class SmartPtr 
{ 
    T *ptr; 
public: 

    // Note that this IS NOT a copy constructor, just another constructor that takes 
    // a similar looking class. 
    template <class O> 
    SmartPtr(const SmartPtr<O> &src) 
    { 
     ptr = src.GetPtr(); 
    } 
    // And likewise with assignment operator. 
}; 

Jeśli typy T i O są zgodne, to będzie działać, jeśli nie dostaniesz błędu kompilacji.

+0

To nie zawsze jest takie proste; musisz upewnić się, że została zachowana odpowiednia liczba referencyjna. Nie można usunąć obiektu pochodnego po tym, jak ostatni Ptr zniknął, gdy nadal istnieje Ptr dla tego samego Pochodnego. – MSalters

+0

Cóż, tak, nadal musisz upewnić się, że rzeczywiście zaimplementujesz inteligentne wskaźniki inteligentnego wskaźnika. – Eclipse

15

SmartPtr<Base> i SmartPtr<Derived> są dwiema różnymi instancjami szablonu SmartPtr. Te nowe klasy nie współdzielą dziedziczenia, które robią: Base i Derived. Stąd twój problem.

what is the best way to perform this assignment operation?

SmartPtr<Base> b = d; 

Nie wywoływać operator przypisania. To wywołuje copy-konstruktor (kopia jest pomijana w większości przypadków) i jest dokładnie tak, jak gdybyś napisał:

SmartPtr<Base> b(d); 

Zapewnić kopiowaniem ctor że trwa SmartPtr<OtherType> i wdrożyć go. To samo dotyczy operatora przypisania. Będziesz musiał napisać copy-ctor i op = pamiętając o semantykach SmartPtr.

+0

Jestem świadomy, że to nie działa, i jestem teraz ponownie świadomy (dziękuję), że to z powodu braku związku między dwiema klasami szablonów. Pytam, jak sprawić, by zachowywał się tak, jakby były ze sobą powiązane, być może poprzez dodanie jakiegoś sprytnego ctor lub innego oszustwa? – rmeador

+2

Wiele zależy od dokładnej semantyki klasy SmartPtr, np. czy ma transfer własności, czy robi to z liczeniem itp. Będziesz musiał napisać copy-ctor i op = mając na uwadze semantykę SmartPtr. – dirkgently

5

Szablony nie są kowariantne i to dobrze; sobie wyobrazić, co by się stało w następującym przypadku:

vector<Apple*> va; 
va.push_back(new Apple); 

// Now, if templates were covariants, a vector<Apple*> could be 
// cast to a vector<Fruit*> 
vector<Fruit*> & vf = va; 
vf.push_back(new Orange); // Bam, we just added an Orange among the Apples! 

Aby osiągnąć to, co staramy się robić, klasa sprytny wskaźnik musi mieć templatized konstruktora, że ​​trwa albo inny sprytny wskaźnik lub wskaźnik innego typu. Możesz rzucić okiem na boost :: shared_ptr, co robi dokładnie to.

template <typename T> 
class SmartPointer { 

    T * ptr; 

    public: 
    SmartPointer(T * p) : ptr(p) {} 
    SmartPointer(const SmartPointer & sp) : ptr(sp.ptr) {} 

    template <typename U> 
    SmartPointer(U * p) : ptr(p) {} 

    template <typename U> 
    SmartPointer(const SmartPointer<U> & sp) : ptr(sp.ptr) {} 

    // Do the same for operator= (even though it's not used in your example) 
}; 
+0

Doskonały punkt na swoim przykładzie ... Nie zastanawiałem się nad tym. W przypadku inteligentnego wskaźnika, nie sądzę jednak, abyś natknął się na ten problem, ponieważ interfejs klasy jest zasadniczo taki sam jak interfejs wskazanego typu (przez przeciążenie -> i *). – rmeador

+0

Myślę, że twój przykład jest błędnym przykładem, ponieważ właściwie nie jest to kowariantem, ponieważ możesz umieścić na liście coś podobnego do Orange. Będzie to tylko kowariant, jeśli interfejs publiczny tylko raz zwróci wartość * Owoc *, ale nie będzie wartością "Akceptuj_ Owoc". C2 dostarcza słowa kluczowe "w" i "na zewnątrz", aby generic typy odpowiednio kowariantne lub contravariant. Statycznie wymusza użycie typów w celu uniknięcia opisanej sytuacji. – LostSalad

0

Myślę, że najłatwiej jest zapewnienie automatycznej konwersji do innego SmartPtr zgodnie z poniższym:

template <class T> 
class SmartPtr 
{ 
public: 
    SmartPtr(T *ptr) { t = ptr; } 
    operator T *() const { return t; } 
    template <class Q> operator SmartPtr<Q>() const 
    { return SmartPtr<Q>(static_cast<Q *>(static_cast<T *>(* this))); } 
private: 
    T *t; 
}; 

pamiętać, że ta realizacja jest mocny w tym sensie, że szablon operator konwersji nie musi znać semantykę inteligentnego wskaźnika, więc liczenie odwołań nie musi być replikowane itp.

+0

Spróbuj {return SmartPtr (t); } Kompilator powie Ci, czy T * można przypisać do Q * bez wszystkich rzutów. Upewnij się, że logika liczników odwołań może współdzielić liczbę odwołań między typami szablonów. Liczba odwołań int * powinna być możliwa. – jmucchiello