Autor Wątek: C++, SDL i shared_ptr - jak używać?  (Przeczytany 5086 razy)

Offline kwonitf

  • Użytkownik

# Marzec 16, 2014, 17:14:21
Jakiś czas temu pytałem tu na forum o boosta, oraz jak zarządzacie pamięcią w swoich grach.
Wiele osób odpowiedziało o wrapperach RAII.

Jako, że ostatnio używam VS2013, postanowiłem wypróbować shared_ptr.
Napisałem sobie do zwykłych testów coś takiego:
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
printf("SDL_Error: %s\n", SDL_GetError());
}
else
{
std::shared_ptr<SDL_Window> window (SDL_CreateWindow("SharedPtr Test", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, SDL_WINDOW_SHOWN));
if (window.get() == NULL)
{
printf("Error", SDL_GetError());
}
else
{
SDL_Delay(2000);
SDL_DestroyWindow(window.get());
}
}
SDL_Quit();
return 0;

Ale podczas działania programu otrzymuję error _BLOCK_TYPE_IS_VALID.
I tu mam kilka pytań:
1. Czy zastępowanie zwykłych wskaźników przez shared_ptr ma w ogóle sens?
2. Czy jest możliwość zadeklarowania shared_ptra ale zainicjalizowania go później?
3. Czy żeby pobrać wskaźnik muszę używać get() i czy to nie jest dodatkowy narzut wydajnościowy?
4. Czemu występuje ten error? Co robię źle w powyższym przykładzie?



Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline Xirdus

  • Redaktor

# Marzec 16, 2014, 17:49:22
SDL_Window jest typem niekompletnym - jest zadeklarowany, ale nigdzie nie ma jego definicji. Nie, nie jest to błąd - specjalnie tak jest zrobione (nie wiem dlaczego, ale tak jest). W takim przypadku shared_ptr nie utworzysz. Jeśli koniecznie chcesz RAII dla okna, będziesz musiał napisać wrapper sam - w konstruktorze wołać SDL_CreateWindow, w destruktorze wołać SDL_DestroyWindow, jakiś getter do pobierania gołego wskaźnika (jest niezbędny do niektórych funkcji SDL-a), i jak chcesz to owrappować dodatkowo wszystkie gettery i settery jakie tam są w SDL_video.h.

Offline kwonitf

  • Użytkownik

# Marzec 16, 2014, 17:59:32
Wybacz jeśli zadaję trywialne pytania (moja znajomość C++ może nie jest wystarczająca) ale:

Czy to znaczy, że ten błąd leci ponieważ użyłem shared_ptr dla SDL_Window? Dla innych typów np SDL_Texture będzie działać?
Jeśli tak to moje drugie pytanie z poprzedniego posta pozostaje aktualne.
To że użyłem shared_ptr dla okna to tylko dla testów. Było to pierwszy typ jaki mi przyszedł na myśl, ale ogólnie to myślałem o podmianie wszystkich  wskaźników w programie na shared_ptr, ale może to nie ma sensu.
Tylko co jeśli użyję zwykłego wskaźnika gdzieś (np. dla okna) a program wyłoży mi się gdzieś w środku i pamięć odnośnie okna nie zostanie zwolniona?
« Ostatnia zmiana: Marzec 16, 2014, 18:02:50 wysłana przez kwonitf »

Offline Xirdus

  • Redaktor

# Marzec 16, 2014, 18:20:30
Czy to znaczy, że ten błąd leci ponieważ użyłem shared_ptr dla SDL_Window? Dla innych typów np SDL_Texture będzie działać?
Tak, to przez to że użyłeś shared_ptr dla SDL_Window. Nie, dla SDL_Texture również nie zadziała z dokładnie tego samego powodu.

To że użyłem shared_ptr dla okna to tylko dla testów. Było to pierwszy typ jaki mi przyszedł na myśl, ale ogólnie to myślałem o podmianie wszystkich  wskaźników w programie na shared_ptr, ale może to nie ma sensu.
Nikt ci nie broni napisać własnych wrapperów na wszystkie SDL-owe typy, a być może i funkje - tak, żeby poza tymi wrapperami nie było żadnych odwołań do gołego SDL-a i żadnych gołych wskaźników. Nie jest to wcale takie głupie jak ci się może wydawać (choć na początek to polecałbym ci jednak ograniczyć się do prostych wrapperów z konstruktorem tworzącym SDL-owy obiekt, destruktorem niszczącym go i metodą get(), i poza funkcjami tworzącymi i niszczącymi korzystać z SDL-a bezpośrednio).

Tylko co jeśli użyję zwykłego wskaźnika gdzieś (np. dla okna) a program wyłoży mi się gdzieś w środku i pamięć odnośnie okna nie zostanie zwolniona?
Wskaźniki nie wywalają programów ot tak sobie - jeśli nie będziesz robił nic głupiego, to wszystko będzie OK. Tak więc SDL-owe struktury owrappuj we własne klasy, w reszcie kodu stosuj stałe referencje, a tam gdzie musisz to shared_ptr, najlepiej jako weak_ptr.


I odpowiadając na resztę pytań z pierwszego postu:

2. Czy jest możliwość zadeklarowania shared_ptra ale zainicjalizowania go później?
Tak - metodą reset().

3. Czy żeby pobrać wskaźnik muszę używać get() i czy to nie jest dodatkowy narzut wydajnościowy?
Tak, musisz. I tak, jest to narzut. Pamiętaj jednak, że przedwczesna optymalizacja jest źródłem wszelkiego zła.

Offline kwonitf

  • Użytkownik

# Marzec 16, 2014, 18:29:38
Nadużyję Twojej uprzejmości jeśli poproszę o przykład takiego najprostszego wrappera?
Chodzi o to że piszę klasę, która w sobie tworzy wskaźnik do obiektu i ma destruktor który zwalnia (delete) ten wskaźnik?

Czy jeśli program się nieoczekiwanie wywali to destruktor się wykona?

Offline Xirdus

  • Redaktor

  • +1
# Marzec 16, 2014, 19:05:37
Nadużyję Twojej uprzejmości jeśli poproszę o przykład takiego najprostszego wrappera?
namespace mySDL
{

class Window
{
public:
    Window(std::string title, int x, int y, int w, int h, Uint32 flags)
    {
        window = SDL_CreateWindow(title.c_str(), x, y, w, h, flags);
    }

    ~Window()
    {
        SDL_DestroyWindow(window);
    }

    SDL_Window* get()
    {
        return window;
    }

private:
    SDL_Window* window;

// TODO: konstruktor kopiujący, konstruktor przenoszący, operator przypisania (kopiujący i przenoszący), jak chcesz to jeszcze konstruktor domyślny
}

}

Czy jeśli program się nieoczekiwanie wywali to destruktor się wykona?
Jeśli program nieoczekiwanie się wywali to już żaden kod się nie wykona - ale o to się nie martw, bo system zwolni wszystkie zasoby sam (oczywiście niezapisane zmiany zostaną utracone).

Offline ison

  • Użytkownik

  • +1
# Marzec 16, 2014, 19:07:01
I tak, jest to narzut.
Narzut zerowy.

Odwołania do smart pointerów nie nakładają żadnego dodatkowego narzutu wydajnościowego. Jedynym problemem jest ich tworzenie - np. wyciąganie shared_ptr z weak_ptr lub przekazywanie smart pointerów przez wartość.

Offline Xirdus

  • Redaktor

# Marzec 16, 2014, 19:40:27
Narzut zerowy.
Rzeczywiście - zwracam honor.

Offline karol57

  • Użytkownik

  • +1
# Marzec 16, 2014, 19:54:51
Tak z ciekawości. Nie przejdzie:
Kod: (C++) [Zaznacz]
std::shared_ptr<SDL_Window> window (SDL_CreateWindow("SharedPtr Test", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, SDL_WINDOW_SHOWN), SDL_DestroyWindow);
coś takiego?

//EDIT: Bo z tego co widzę błąd jest w tym, że shared robi delete na SDL_Window, więc jak mu się powie, żeby robił SDL_DestroyWindow to powinno być cacy.
//EDIT2: Nigdy nie robiłem w SDL więc nie wiem czy to zadziała i nie mam jak tego sprawdzić.
« Ostatnia zmiana: Marzec 16, 2014, 19:59:31 wysłana przez karol57 »

Offline Xirdus

  • Redaktor

# Marzec 16, 2014, 20:08:39
@karol57: nie wiem jak w Visualu, ale u mnie na MinGW GCC 4.8.0 std::shared_ptr ma static_asserta na kompletność typu, więc się nawet nie kompiluje. W każdym razie lepiej na tym nie polegać.

Offline karol57

  • Użytkownik

# Marzec 16, 2014, 20:30:27
@Xirdus Aż sprawdziłem z ciekawości.
U mnie (GCC 4.8.1) shared_ptr ma static_asseta, ale tylko wtedy gdy nie podamy deletera:
Kod: (C++(shared_ptr_base.h)) [Zaznacz]
      template<typename _Tp1>
explicit __shared_ptr(_Tp1* __p)
        : _M_ptr(__p), _M_refcount(__p)
{
  __glibcxx_function_requires(_ConvertibleConcept<_Tp1*, _Tp*>)
  static_assert( sizeof(_Tp1) > 0, "incomplete type" );
  __enable_shared_from_this_helper(_M_refcount, __p, __p);
}

      template<typename _Tp1, typename _Deleter>
__shared_ptr(_Tp1* __p, _Deleter __d)
: _M_ptr(__p), _M_refcount(__p, __d)
{
  __glibcxx_function_requires(_ConvertibleConcept<_Tp1*, _Tp*>)
  // TODO requires _Deleter CopyConstructible and __d(__p) well-formed
  __enable_shared_from_this_helper(_M_refcount, __p, __p);
}

[...]W każdym razie lepiej na tym nie polegać.
A dlaczego? Po to jest możliwość dodania deletera, aby z niej korzystać. Jedyne problemy jakie tutaj widzę, to niektóre windowsowe funkcje które zwracają INVALID_HANDLE_VALUE zamiast 0.

// EDIT: Z tego co widać Visual nie ma static asserta, bo program OP się skompilował i wywali dopiero gdy shared_ptr zrobił delete na czymś co było wcześniej usunięte (i do tego powinno być usuwane inną funkcją).
« Ostatnia zmiana: Marzec 16, 2014, 20:36:58 wysłana przez karol57 »

Offline kwonitf

  • Użytkownik

# Marzec 16, 2014, 20:36:39
Jeśli program nieoczekiwanie się wywali to już żaden kod się nie wykona - ale o to się nie martw, bo system zwolni wszystkie zasoby sam (oczywiście niezapisane zmiany zostaną utracone).
Dzięki za odp.
W takim razie nie do końca rozumiałem ideę wrapperów. Myślałem, że zabezpieczają jakoś przed tym, że program się zamyka nieoczekiwanie więc pamięć zostaje zwolniona przez wrappery/inteligentne wskaźniki.
Teraz rozumiem to tak, że zostały one stworzone tylko po to żeby zabezpieczyć przed tym, że ktoś zapomni napisać delete'a. Czy ja wiem, czy to tak łatwo zapomnieć?
« Ostatnia zmiana: Marzec 16, 2014, 21:13:03 wysłana przez kwonitf »

Offline Xirdus

  • Redaktor

# Marzec 16, 2014, 20:41:48
U mnie (GCC 4.8.1) shared_ptr ma static_asseta, ale tylko wtedy gdy nie podamy deletera:
OK, my bad - w takim razie stosowanie takiego shared_ptr ma sens. Z tym że i tak wypadałoby owrappować tego shared_ptra (czy to przez dziedziczenie, czy to przez fabrykę, czy to jak tam się standardowo do tego podchodzi) żeby zlikwidować podawanie deletera za każdym razem gdy tworzy się obiekt.

Teraz rozumiem to tak, że zostały one stworzone tylko po to żeby zabezpieczyć przed tym, że ktoś zapomni napisać delete'a. Czy ja wiem, czy to tak łatwo zapomnieć?
A byś się zdziwił.

Offline kwonitf

  • Użytkownik

# Marzec 16, 2014, 21:13:17
Rozwiązanie podane przez karol57 działa więc zadam jeszcze jedno pytanie:
Kiedy jest sens używania inteligentnego wskaźnika lub wrappera a kiedy lepiej jest pozostać przy zwykłym?
Skoro narzut jest zerowy, to mój pomysł podmiany wszystkich wskaźników na shared_ptr ma sens?

Offline Dab

  • Redaktor
    • blog

# Marzec 16, 2014, 21:25:20
Odwołania do smart pointerów nie nakładają żadnego dodatkowego narzutu wydajnościowego.

To jest ogólnie rzecz biorąc nieprawda. Pośredni wskaźnik na obiekt to dodatkowy cache miss. Operacje manipulujące wskaźnikiem są jeszcze droższe (atomowy reference counting, wirtualne funkcje do operowania licznikami i destruktorem).