Autor Wątek: Problem ze złożonym dziedziczeniem  (Przeczytany 2203 razy)

Offline Icy Tower

  • Użytkownik

# Marzec 17, 2016, 13:25:54
Hej,
mocuję się z tym problemem od tygodni. Chciałbym stworzyć klasę tekstury, która miałaby cechy takie jak w Direct3D11 czyli:

Usage: Immutable, Dynamic, Default.
Binding: ShaderResource, RenderTarget, DepthStencil, UnorderedAccess

i cechy te mogłyby być sprawdzane na etapie kompilacji (weryfikacja argumentów funkcji itp).
Przy czym tekstura może mieć jeden Usage oraz jeden lub więcej Binding na raz.

Coś w stylu: Texture2D_Immutable_ShaderResource,  Texture2D_Default_ShaderResource_RenderTarget.

A jednocześnie chciałbym móc w metodzie zarządać tekstury, która ma tylko wybrane cechy - np. ma Usage Immutable (Binding nieważny), lub ma Binding ShaderResource_RenderTarget (Usage nieważny) lub po prostu jest teksturą o dowolnych cechach.

----------------------------

Niestety nie wiem jak to osiągnąć w elegancki sposób :(

Moje próby (opcjonalne do przeczytania):

Próbowałem najpierw użyć wzorców biorących enumy: Texture2D< Immutable, RenderTarget > oraz std::enable_if żeby udostępnić metody (np. getRenderTargetView) i konstruktory tylko dla określonych konfiguracji. Niestety przy takim rozwiązaniu każda konfiguracja jest traktowana jako niezależna klasa i nie ma między nimi relacji.

Próbowałem też dwóch wzorców: Texture2DBind< RenderTarget > i Texture2DUsage< Immutable > i jeszcze jednego, który po nich obu dziedziczy zależnie od podanych cech (poprzez specjalizację). To pozwala podać Texture2D< Immutable, RenderTarget > tam gdzie wymagany jest Texture2DUsage< Immutable > lub Texture2DBind< RenderTarget >. Ale wciąż nie pozwala podać Texture2D< Immutable, ShaderResource_RenderTarget > tam gdzie wymagany jest Texture2DBind< RenderTarget > (nie ma relacji między klasami związanymi z bindingiem).

Na koniec myślę o stworzeniu całej hierarchi klas, które by po sobie dziedziczyły. W większości byłaby to tylko definicja w jednej linijce. Byłoby to 9 klas związanych z Bindingiem, 3 klasy związane z Usage, jedna klasa bazowa i aż 27 klas kompletnych tekstur! A każda z kompletnych tekstur musiałaby mieć konstuktory, których mam 4. I w zasadzie, gdyby nie konieczność pisania tych 27*4 konstruktorów to rozwiązanie byłoby ok.

Jak mógłbym ten problem rozwiązać?

Offline Mr. Spam

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

Offline Risist

  • Użytkownik

# Marzec 17, 2016, 21:37:01
Ja bym nie szedł w dziedziczenie, a w komponenty. Pomysł z hierarchią jest dobry, jedynie co zamiast tworzyć klasy jak "Texture2D_Immutable_ShaderResource" zrób jedną klasę Texture z tablicami komponentów, które można dodawać ( co więcej dynamicznie nawet będzie można). Jeżeli jakiejś kombinacji nie będziesz używać to nie będzie ci niepotrzebnie zalegać w kodzie, a ponadto jak coś zmienisz w hierarchii nie musisz edytować tych 39 klas. A i czas kompilacji się zmniejszy ;) (zakładam, że korzystasz z cpp )

Ja z takiego rozwiązania korzystam często i zazwyczaj jest ok.

Na ogół hierarchia wygląda mniej więcej tak:

Klasa bazowa z definicją interface'u + jakieś podstawowe zachowania
Klasa przechowująca wiele elementów, dziedzicząca po bazowej
Konkretne klasy realizujące wymagane funkcjonalności ( te z Usage i Binding)
No i konkretna klasa ( u ciebie Texture)

w ten oto sposób zrobisz nie kilkadziesiąt, nie kilkanaście, a poniżej 10 w miarę prostych klas, z swobodnymi możliwościami rozwoju systemu o dodatkowe komponenty bez zawrotów głowy przy wielogodzinnym, mozolnym pisaniu.

Ewentualnie możesz napisać sobie skrypt w bashu, który wygeneruje ci te klasy xD

Offline lethern

  • Użytkownik

# Marzec 17, 2016, 22:53:10
Nie napisałeś do końca co chcesz uzyskać, i jakie masz wymagania co do kodu, ale opis Risista brzmi jak rozwiązanie ;p
(jeśli nie jest dobrym, to pewnie dlatego, że są jakieś ukryte oczekiwania o których nie wspomniałeś jeszcze)

Offline Icy Tower

  • Użytkownik

# Marzec 18, 2016, 11:27:17
Nie wiem czy do końca zrozumiałem koncepcję komponentów, ale wydaje mi się, że sprawdzenie poprawności typu tekstury (w sensie Usage i Binding) następowałoby w run-time a mi zależy na sprawdzaniu na etapie kompilacji.

W dużym skrócie chodzi mi o to żeby nie można było w kodzie przekazywać obiektów tekstury w dowolny sposób, tylko żeby można było już na etapie kompilacji prawdzić zgodność ich "typów".

Dobrze rozumiem?

Offline Risist

  • Użytkownik

# Marzec 18, 2016, 13:20:24
Miło było by jakbyś opisał dokładniej jak ma działać to sprawdzanie poprawności.
Jeśli rozumiem niektóre kombinacje binding i usage mają być niedozwolone? To się kłóci z twoimi wyliczeniami.

Przy dobrze napisanej klasie bazowej, każda kombinacja będzie poprawna. Ponadto będziesz korzystać jedynie z funkcji wirtualnych interfaceu. Pewnie masz na myśli, że niektóre z komponentów mają funkcje które istnieją jedynie w ich obrębie. Cała sztuka to jedynie zaprojektować dobrze tą klasę bazową, co czasami nie jest łatwe.

W każdym razie skrypt piszący klasy, jeżeli mają być bardzo schematyczne nadal pozostaje dosyć dobrą opcją.

Ewentualnie widzę jeszcze jedno rozwiązanie. W wersji debug sprawdzało by poprawność ( cokolwiek to oznacza), a w release było by wycięte.
« Ostatnia zmiana: Marzec 18, 2016, 13:26:41 wysłana przez Risist »

Offline hashedone

  • Użytkownik

# Marzec 18, 2016, 13:23:27
Mnie się wydaje że przekombinowujesz. Chcesz osiągnąć jakąś funkcjonalność i próbujesz to zaimplementować, a później wychodzi Ci interfejs-potworek. Najpierw zaprojektuj sobie proste API jakie Cię interesuje i potem się można zastanowić jak je osiągnąć.

Offline lethern

  • Użytkownik

# Marzec 18, 2016, 13:38:49
W wersji debug sprawdzało by poprawność ( cokolwiek to oznacza), a w release było by wycięte.
pewnie masz na myśli "assert"
rozumiem że masz kod, który raz może przyjmować klasę typu A, inny może przyjmować typu B, jeszcze inny może A lub C ale nie B, i tak dalej. Brzmi jak problem do rozwiązania przez "interfejsy", tylko nie w przypadku kiedy masz 27 różne kombinacje
Pytanie, czy napisaleś już kod i chcesz go uporządkować, czy jedynie przewidujesz jak to będzie?
Wyobrażasz sobie, że masz kod, który np. renderuje tekstury typu X, a przekazujesz mu jako argument teksturę nie kompatybyliną z X? W jaki sposób byś to niby przekazał, pojedyńczo, Jako element innego obiektu, Jako element listy, Po IDku? Dlaczego nie przekazujesz tego do odpowiedniej funkcji, dlaczego w ogóle mialby zajść ten mismatch? Dalej, czy dobrze rozumiem, że możesz też mieć specjalne funkcje, które są wywoływane dla niekórych tekstur o określonych (dozwolonych) typach?

Może ktoś dostrzega w tym "powszechny problem, który rozwiązuje się w oczywisty sposób" i spodziewane tu jest to oczywiste rozwiązanie, ale z drugiej strony, może wcale to nie jest oczywisty problem i nie ma tu żadnego oczywistego rozwiązanie ;p
« Ostatnia zmiana: Marzec 18, 2016, 13:45:49 wysłana przez lethern »

Offline Icy Tower

  • Użytkownik

# Marzec 18, 2016, 19:07:21
Spróbuję wytłumaczyć na przykładzie o co mi chodzi. Wcześniej miałem tylko klasę Texture2D i ona mogła mieć dowolny Usage i Binding i można było ją przekazać do dowolnej funkcji. Jeśli okazała się ona w praktyce nieodpowiednia - np. trzeba było teksturę w metodzie zbindować jako RenderTarget i to się nie powiodło, bo tekstura nie miała typu RenderTarget to wyrzucany był wyjątek. A teraz chciałbym żeby ta funkcja brała jako argument teksturę typu Texture2D_RenderTarget co daje teoretyczna pewność, że jej zbindowanie się powiedzie i jednocześnie jest to jasna informacja dla wołającego jakiej tekstury się oczekuje. Jednocześnie dla tej metody nieważne jest czy tekstura ma Usage Immutable, Dynamic lub Default, bo nie zamierza jej modyfikować (akurat RenderTarget musi mieć chyba Usage Default, ale to tylko przykład).

Drugi przykład to metoda, która modyfikuje teksturę i dlatego jej Usage musi być Dynamic. Bierze więc ona teksturę Texture2D_Dynamic.

A inna metoda może po prostu chcieć sprawdzić wymiary tekstury i nie obchodzi jej Usage ani Binding tekstury, więc ona bierze jako argument po prostu Texture2D.

Tworzenie tekstury o niewspieranej kombinacji może się po prostu kończyć wyjątkiem i nie trzeba tu nic szczególnego robić.

Jedyne metody jakie zależą od Bindingu to: getRenderTargetView(), getDepthStencilView(), getUnorderedAccessView(), getShaderResourceView().
Ale to nie jest nawet konieczne. Mogą te metody być dostępne dla każdego typu tekstury, tylko będą rzucać wyjątek, gdy nie są wspierane przez teksturę.

Od Usage nie zalezy istnienie żadnych metod. Ale Usage wpływa na to jak działa metoda loadCpuToGpu. Dla Immutable i Default tworzy ona od nowa teksturę a dla Dynamic modyfikuje ona teksturę poprzez mapowanie jej.

Chodzi tylko o to żeby już na etapie kompilacji było jasne, że podało się dobry lub zły typ tekstury a nie w runtime.
« Ostatnia zmiana: Marzec 18, 2016, 19:55:33 wysłana przez Icy Tower »

Offline albireo

  • Użytkownik

# Marzec 18, 2016, 20:54:24
Jeśli działałaby ta wersja z ręcznym stworzeniem 27 klas (27 wynika zapewne z tego, że dla każdego z 3 usage robisz wersję z każdym z 9 bindingów), to zamiast tworzyć je ręcznie mógłbyś użyć template (zakładając, że te konstruktory byłyby dla wszystkich 27 klas takie same, ewentualne różnice załatwiłaby specjalizacja). Wyglądałoby to jakoś tak:
template<typename Binding, typename Usage> class CompleteTexture: public Binding, public Usage {
  // ...
};
Z tym że i tak nie zadziała ci to od kopa jeśli funkcja będzie np chciała ShaderResource+Immutable, a ty będziesz miał ShaderResource+RenderTarget+Immutable, da się to obejść, ale trzeba będzie dodać częściowe specjalizacje z konwersją Bindingów (na szczęście Usage będzie mogło być nadal parametryczne, więc będzie ich tylko kilka).

Offline lethern

  • Użytkownik

# Marzec 18, 2016, 21:11:06
Ale z tego co widzę, to tylko w jednym miejscu potrzebujesz tego "sprawdzania typu w compile-time"

np. do metody, która modyfikuje teksturę, nie widzę za bardzo sytuacji gdzie byś przekazał teksturę, która nie powinna być modyfikowana.. a jeśli nawet tak zrobisz, możesz zostawić asserta po stronie debug / wyjść z funkcji po stronie produkcji. Choć tu by chyba dużo wyjaśniło sposób w jaki te metody wołasz i tekstury przechowujesz?
no własnie, chyba nie napisałeś w jaki sposób chcesz wykonywać metody i jak te tekstury rozróżniasz, bo czy jakaś automagia może Ci w ogóle pomóc w compile-time wyłapywać typy (bo jeśli trzymasz te rzeczy w kolekcji, to nie ma o czym rozmawiać przecież ;p)
« Ostatnia zmiana: Marzec 18, 2016, 21:48:57 wysłana przez lethern »