Inicjalizacja inteligentnego wskaźnika

0

Pytanie dotyczy nie tylko inteligentnego wskaźnika, ale na tym przykładzie najłatwiej problem przedstawić.

Weźmy sobie inteligentny wskaźnik, który jest właścicielem obiektu, na który wskazuje. Czyli niszczy go w swym destruktorze, a podczas kopiowania wskaźnika korzysta z wzorca prototyp i metody 'clone' by odpowiedni obiekt sklonować.

Jestem ciekaw jaka jest ogólnie przyjęta praktyka. Który z dwóch wariantów ustawiania wskaźnika jest lepszy?

Wariant 1:

//Konstruktor
void Pointer<Object>::Pointer(Object* object) {
    pointer = object;
}

//Użycie:
Pointer<Object> ptr(new Object);

Wariant 2:

//Konstruktor
void Pointer<Object>::Pointer(const Object& object) {
    pointer = object.clone();
}

//Użycie:
Pointer<Object> ptr(Object());

Sposób pierwszy wydaje się znacznie bardziej naturalny. Sam tworze obiekt i jego adres przekazuje mojemu wskaźnikowi.
Co jednak z zasadą "pamięć alokujemy tylko w destruktorach", na którą wielokrotnie się natykałem? Drugi sposób wydaje się znacznie bezpieczniejszy, bo użytkownik wskaźnika sam świadomie żadnej pamięci nie alokuje - tworzy jedynie obiekt tymczasowy, który zostanie sklonowany. Jednak to mój własny pomysł i raczej wszędzie spotykałem się z wariantem pierwszym, który wydaje się bardziej naturalny, ale mniej bezpieczny.

1

i metody 'clone' by odpowiedni obiekt sklonować
yyy... a po co wymyślać dziwne twory, jak jest już dokładnie do tego konstruktor kopiujący?

0

Opowiedziałbym się za pierwszym wariantem, bo klasa inteligentnego wskaźnika to jednak nadal "tylko wskaźnik", a operator new wyraźnie pokazuje że chodzi o alokację nowego obiektu.

Najbardziej naturalnie wyglądałoby tak:

Pointer<Object> ptr = new Object();

ale trzeba by dokładniej przemyśleć konsekwencje.

0

@Platyna, nie ogarniam. Azarien napisał, że do tego trzeba użyć konstruktora kopiującego, a ty go bezmyślnie minusujesz zamiast zrobić jakiś większy research.

Według mnie inteligentny wskaźnik nie powinien się tym zajmować, tj. to ty już powinieneś dostarczyć mu tę kopię. Ale jak tak bardzo chcesz.. to konstruktor za to odpowiedzialny może wyglądać tak:

template <typename TActual> Pointer(const TActual& obj) : pointer(new TActual(obj)) { }
  1. Powoduje to taki problem, że dla wskaźników o typach dziedziczących z Object kompilator wybierze konstruktor ten powyżej. Wyrzuci też błąd na etapie kompilacji, więc nic katastrofalnego się nie stanie. Rozwiązanie to rzutowanie typu wskaźnika na klasę bazową przed przesłaniem go do konstruktora albo zabawa z boostowym enable_if i type traits do wyłączenia tego konstruktora dla referencji na wskaźniki.
  2. Parametry szablonowego poprzedzaj literą T.
0
Rev napisał(a):

@Platyna, nie ogarniam. Azarien napisał, że do tego trzeba użyć konstruktora kopiującego, a ty go bezmyślnie minusujesz zamiast zrobić jakiś większy research.
Według mnie inteligentny wskaźnik nie powinien się tym zajmować, tj. to ty już powinieneś dostarczyć mu tę kopię. Ale jak tak bardzo chcesz.. to konstruktor za to odpowiedzialny może wyglądać tak:

template <typename TActual> Pointer(const TActual& obj) : pointer(new TActual(obj)) { }
  1. Powoduje to taki problem, że dla wskaźników o typach dziedziczących z Object kompilator wybierze konstruktor ten powyżej. Wyrzuci też błąd na etapie kompilacji, więc nic katastrofalnego się nie stanie. Rozwiązanie to rzutowanie typu wskaźnika na klasę bazową przed przesłaniem go do konstruktora albo zabawa z boostowym enable_if i type traits do wyłączenia tego konstruktora dla referencji na wskaźniki.
  2. Parametry szablonowego poprzedzaj literą T.

Przepraszam, ale wydaje mi się, że jednak ja mam rację. Jeśli nie, wskaż mi błąd w rozumowaniu.
Mówimy o wskaźniku, który sam zadba o usunięcie obiektu, na który wskazuje. Jest to bardzo popularny wzorzec i z całą pewnością ma sens by wskaźnik się tym zajmował. Są różne typy inteligentnych wskaźników. Od takich, które zliczają liczbę odniesień do obiektu po takie które są właścicielami obiektu. I o tych drugich tu mówimy.
W takim przypadku przy kopiowaniu takiego wskaźnika NIE MOŻNA bezmyślnie skopiować adresu, bo wskaźnik jest "właścicielem obiektu" i w wyniku dwa wskaźniki usuną ten sam obiekt co będzie katastrofą. W przypadku tego typu "inteligentnego wskaźnika" nie należy bezmyślnie kopiować adresu. Kopiujemy cały obiekt na który wskaźnik wskazuje. I nie możemy do tego użyć zwykłego konstruktora kopiującego! Wskaźnik może wskazywać na typ abstrakcyjny (i jest to chyba jedyne zastosowanie tego wzorca!). Musimy tu zastosować wzorzec projektowy 'prototyp', aby skopiować faktycznie wskazywany obiekt, a nie sugerować się tylko typem wewnętrznego skłądowego wskaźnika.
Mówisz, że nie stanie się nic katastrofalnego dla typów dziedziczących. Rzecz w tym, że się stanie. Nie będzie błędu kompilacji, a nawet gdyby był to i tak źle. Zostanie skopiowana sama część pochodząca z klasy bazowej, a tego nie chcemy. Nie taki jest cel.

0

Mówimy o wskaźniku, który sam zadba o usunięcie obiektu, na który wskazuje.

O tym przeczytałem już w temacie wątku.

z całą pewnością ma sens by wskaźnik się tym zajmował.

Miałem na myśli kopiowanie podanego mu argumentu. Inteligentny wskaźnik ma zająć się automatycznym zwolnieniem podanego mu wskaźnika. I tyle, to jest jego podstawowa rola. Ty dodatkowo chcesz dodać mu funkcjonalność sklonowania danego mu obiektu. Spoko, można to zrobić, ale z perspektywy inteligentnego wskaźnika to lukier składniowy i nie powinno go to za bardzo obchodzić. Koniec końców, on tylko zarządza swoim wskaźnikiem: czy będzie to wskaźnik zaalokowany wcześniej przez ciebie czy kopia obiektu - nie ma to absolutnie żadnego znaczenia. Można nawet powiedzieć, że łamie to single responsibility principle.

W takim przypadku przy kopiowaniu takiego wskaźnika NIE MOŻNA bezmyślnie skopiować adresu, bo wskaźnik jest "właścicielem obiektu" i w wyniku dwa wskaźniki usuną ten sam obiekt co będzie katastrofą.

I dlatego po przekazaniu wskaźnika do inteligentnego wskaźnika to ty powinieneś uważać, żeby go nie zwolnić. Nie ma tutaj żadnej magicznej bariery, która cię przed takim głupstwem ochroni.

Base* ptr = new Derived;
std::unique_ptr<Base> uniquePtr(ptr);
delete ptr;

Co do skopiowania samego inteligentnego wskaźnika - tutaj jest prościej, bo możemy zwyczajnie wyłączyć możliwość jego kopiowania (ustawiając widoczność konstruktora kopiującego na private) albo użyć move semantics (a z referencjami rvalue z C++11 jeszcze się to upraszcza).

No, ale widzę, że uparłeś się tego klonowania obiektu.

Wskaźnik może wskazywać na typ abstrakcyjny (i jest to chyba jedyne zastosowanie tego wzorca!). Musimy tu zastosować wzorzec projektowy 'prototyp', aby skopiować faktycznie wskazywany obiekt, a nie sugerować się tylko typem wewnętrznego skłądowego wskaźnika.

Przeczytaj jeszcze raz linijkę kodu, którą wysłałem w poprzednim poście i zwróć uwagę na to, który konstruktor kopiujący zostanie wywołany - klasy bazowej czy pochodnej.

1
  1. jestem za pierwszym użyciem - jest bardziej tradycyjne, naturalne. W drugim nie wiadomo o co chodzi, dodatkowo następuje w nim kopiowanie.

  2. clone() to standardowy wzorzec OOP pt. "prototyp"
    http://www.oodesign.com/prototype-pattern.html

  3. @Platyna: nie wiem co chcesz osiągnąć, ale zainteresuj się boost::intrusive_ptr

0

Miałem na myśli kopiowanie podanego mu argumentu.

Ok, t tej sprawie zwracam honor. Nie zoruzmiałem

Inteligentny wskaźnik ma zająć się automatycznym zwolnieniem podanego mu wskaźnika. I tyle, to jest jego podstawowa rola. Ty dodatkowo chcesz dodać mu funkcjonalność sklonowania danego mu obiektu. Spoko, można to zrobić, ale z perspektywy inteligentnego wskaźnika to lukier składniowy i nie powinno go to za bardzo obchodzić. Koniec końców, on tylko zarządza swoim wskaźnikiem: czy będzie to wskaźnik zaalokowany wcześniej przez ciebie czy kopia obiektu - nie ma to absolutnie żadnego znaczenia. Można nawet powiedzieć, że łamie to single responsibility principle.

No i tu się myślisz. Dla takiego wskaźnika owo klonowanie jest niezbędne. Chyba nie rozumiesz w jakich przypadkach taki wskaźnik znajduje zastosowanie więc pozwól, że spróbuję przedstawić przykład. Będzie trochę Javowy, ale nie szkodzi. Weźmy abstrakcyjną klasę Container<T> i dziedziczące po nim Set<T> i List<T>. Implementują one metodę

Iterator<T> insert(const T& x);

która zwraca iterator na dodany do kontenera element. Ponieważ Set i List używają zupełnie inaczej działających iteratorów, zatem Iterator<T> też jest klasą abstrakcyjną, po której dziedziczy IteratorList<T> i IteratorSet<T>. Skoro tak to nie możemy Iteratora zwracać przez wartość. Musimy zrobić tak:

Iterator<T>* insert(const T& x);

Ale Iterator musi być dynamicznie tworzony wewnątrz metody insert. To sprawia, że użytkownik metody ma obowiązek zwolnić pamięć po iteratorze nawet gdy wcale go nie potrzebował. I tu w grę wchodzi inteligentny wskaźnik! Mamy taką metodę insert:

Pointer<Iterator<T> > insert(const T& x);

Teraz niezależnie od tego czy nas iterator interesuje czy nie, nie musimy się martwić o to, że była wewnątrz inserta dynamicznie zaalokowana pamięć.
A jeśli nas ten iterator interesuje:

Pointer<Iterator<T> > it =  insert(x);

To zauważ, że niezbędny jest konstruktor kopiujący dla wskaźnika, który KLONUJE obiekt. Bo jakby go nie klonował tylko przepisywał adres to dwa wskaźniku rządziły by jednym obiektem i oba by go chciały usunąć.

W takim przypadku przy kopiowaniu takiego wskaźnika NIE MOŻNA bezmyślnie skopiować adresu, bo wskaźnik jest "właścicielem obiektu" i w wyniku dwa wskaźniki usuną ten sam obiekt co będzie katastrofą.

I dlatego po przekazaniu wskaźnika do inteligentnego wskaźnika to ty powinieneś uważać, żeby go nie zwolnić. Nie ma tutaj żadnej magicznej bariery, która cię przed takim głupstwem ochroni.

No i właśnie ja powinienem uważać, ale konstruktor kopiujący wskaźnika również musi zadbać oto by dwa wskaźniki nie były właścicielami tego samego obiektu.
I wiem, że nie ma ochrony przed głupstwem usunięcia obiektu samemu. Ale mam nadzieję, że teraz widzisz, że są jeszcze inne niebezpieczeństwa.

Co do skopiowania samego inteligentnego wskaźnika - tutaj jest prościej, bo możemy zwyczajnie wyłączyć możliwość jego kopiowania (ustawiając widoczność konstruktora kopiującego na private) albo użyć move semantics (a z referencjami rvalue z C++11 jeszcze się to upraszcza).

Na przedstawionym przykładzie widać, że wcale nie możemy.

No, ale widzę, że uparłeś się tego klonowania obiektu.

Ja się na nic nie uparłem. To jest znany wzorzec. Inteligentny wskaźnik tego typu korzysta z wzorca 'prototyp' by móc klonować obiekty.

@vpiotr

  1. jestem za pierwszym użyciem - jest bardziej tradycyjne, naturalne. W drugim nie wiadomo o co chodzi, dodatkowo następuje w nim kopiowanie.

Pierwsza sensowna odpowiedź w tym temacie.

  1. @Platyna: nie wiem co chcesz osiągnąć, ale zainteresuj się boost::intrusive_ptr

Przedstawiony przykład powinien rozwiać wątpliwości co ciałem osiągnąć. Oczywiście był to tylko przykład i proszę mi nie wyjeżdżać zaraz z tekstami, że bez sensu jest takie dziedziczenie kontenerów, bo to wiem. Nie mogłem nic lepszego wymyślić na szybko. :)

0

Żeby twój przykład zadziałał tak jak chcesz, wystarczy, żeby inteligentny wskaźnik oferował move semantics. Sam zacytowałeś ten fragment.

Nie wiem tylko dlaczego wciąż starasz mi się udowodnić, że nie wiem czym są inteligentne wskaźniki ;)).

0
Rev napisał(a):

Żeby twój przykład zadziałał tak jak chcesz, wystarczy, żeby inteligentny wskaźnik oferował move semantics. Sam zacytowałeś ten fragment.

Nie wiem tylko dlaczego wciąż starasz mi się udowodnić, że nie wiem czym są inteligentne wskaźniki ;)).

Bo ja mówię o konkretnym istniejącym wzorcu projektowym, a wy mi wyskakujecie z propozycjami zupełnie innych rozwiązań chociaż wcale nie o to prosiłem. Moje pytanie dotyczyło tego który sposób implementacji takiego rodzaju inteligentnego wskaźnika jest lepszy. A wy mi wyjeżdżacie z propozycjami całkiem innych rozwiązań.

Tu częściowo zwracam honor. To prawda, w podanym przeze mnie przykładzie move semantics nie tylko jest dobrym rozwiązaniem, ale nawet lepszym od mojego wskaźnika, bo oszczędza pamięć. Może więc wymyśliłem na szybko nieco niefortunny przykład. Ale przecież może zajść sytuacja, w której kopiowany wskaźnik nie jest tylko obiektem tymczasowym. Że mimo wszystko potrzebuje zachować też źródło i wtedy move semantics się nie sprawdzi.

Więc wydawało mi się, że nie rozumiesz wskaźnika, o którym tu mowa, bo nie odpowiadałeś na pytanie zawarte w temacie. Tylko jakieś inne. ;)

EDIT:
Nawet mam przykładowe zastosowanie, w którym move semantics już się nie sprawdzi. Bardzo proste. Załóżmy, że mamy obiekt, który ma składowe wskaźnikowe, które musi usunąć. Ja chcę sobie umilić życie i nie musieć pisać własnego destruktora, konstruktora kopiującego i operatora przypisania. Wystarczy, że użyje mojego inteligentnego wskaźnika, który klonuje obiekt przy kopiowaniu i usuwa w destruktorze. Teraz mogę śmiało korzystać z wersji generowanych przez kompilator.

1 użytkowników online, w tym zalogowanych: 0, gości: 1