Polimorfizm i przechowanie typu

0

Zastanawia mnie, czy można jakoś "przechować" i potem "odtworzyć" typ polimorficzny. Coś takiego:

struct Base{}
struct D1 : public Base{}
struct D2 : public Base{}

List<Base> list(2); //Tworzy tablicę wskaźników Base* Data[2]
list[0].Allocate<D1>(); //Wywołuje Data[0] = new T, czyli D1;
list[1].Allocate<D2>(); //Wywołuje Data[1] = new T, czyli D2;
list.Free(); //Zwalnia wszystkie obiekty, wywołując dla nich delete w pętli

No i teraz załóżmy, że chciałbym wywołać metodę typu ReallocateAll(), żeby List[0] i List[1] znowu zostały utworzone, z wykorzystaniem tych samych typów polimorficznych, co ostatnio. Da się to jakoś osiągnąć?

Kombinowałem z generatorem indywidualnych identyfikatorów dla typów i pakowaniem ich w krotkę (tuple), ale ciągle mi się się nie zgadzały wymagania co do compile-time. Będę wdzięczny za wszelkie sugestie.

1

Musisz to jakoś inaczej opisać problem.
Albo podaj kawałek prostego działającego kodu z problemem.
co to za klasa List ?

0

O coś takiego (sklecone na szybko):

template<typename T>
class List
{
private:
	T** Data = nullptr;
	int Size;
public:
	List(int iSize) : 
		Size(iSize)
	{
		Data = new T*[Size]{ nullptr };
	}
	
 	virtual ~List()
	{
		Free();
		if (Data) delete[] Data;
	}
 
	template<typename T>
	void Allocate(int iIndex)
	{
		Data[iIndex] = new T;
	}
	
	void Free()
	{
		for (int I = 0; I < Size; I++)
		{
			if (Data[I])
			{
				delete Data[I];
				Data[I] = nullptr;
			}
		}
	}
};


int main()
{
	List<Base> list(2);
	list.Allocate<D1>(0);
	list.Allocate<D2>(1);
	list.Free();

	return 0;
}
0

to się nawet nie kompiluje, zakładam że przeczytałeś co wyrzuca kompilator w sprawie tego kawałka?

template<typename T>
class List
......
	template<typename T>
	void Allocate(int iIndex)
	{
		Data[iIndex] = new T;
	}

nie wiem dokładnie co chcesz osiągnąć. Bo to jest coś bardzo dziwnego, poniżej też napiszę coś dziwnego bo nie rozumiem twojego toku myślenia ale spróbuję podążyć za nim.

jeśli obiekt List zarządza cyklem życia obiektu to nasuwają się proste wnioski w najprostszym wydaniu(zaznaczam nie budowałem tego wiec nie wiem czy będzie działać)

	void Allocate(int iIndex, ENUMTYPE type)
	{
      // prosta "fabryka" na switch
	}

 	List<Base> list(2);
	list.Allocate(0, ENUMTYPE::D1);
	list.Allocate(1, ENUMTYPE::D2);

 	void ReAllocate()
	{
      // no cóż
      std::is_same -> i tu już zostawiam ci to jako zadanie domowe.
	}
 

BTW. widzisz problem w Allocate z wyciekami pamięci?
Ogólnie rzuciłem najprostszy i prostackimpomysł będący bez sensu i jakaś dziwna logika. Określ problem pogadamy.

0

U mnie, na VS 2022 CE i C++20 się kompiluje, a kod to wklejka.

Zwykły enum nie wystarczy, bo to zakłada sztywną, odgórną listę typów polimorficznych. U mnie czegoś takiego nie ma, bo lista jest "otwarta".

Poza tym is_same porównuje typy, więc musisz mieć co porównywać. Ja potrzebuję zapisać sobie typ i móc go użyć później do allokacji obiektu.

0

U mnie, na VS 2022 CE i C++20 się kompiluje, a kod to wklejka.

i nie dostajesz nic o przesłonięciu typów?

Czym w twoim przykładzie jest ENUMTYPE?

wymyślonym enumtype myślałem że po nazwie będzie dość jasne(szczególnie jak napisałem w komentarzu o fabryce).

Poza tym is_same porównuje typy, więc musisz mieć co porównywać.

Sam napisałeś że twoja Lista ma robić realokację prawda? A z tego co widzę to ona coś przechowuje.

Już widzę że masz jakiś wydumany problem. Określ go po czym doczytaj o tym c++.

ale nie przeczę mój tok rozumowania i kod jest bez sensu nie znam problemu opisz go.

1

Ale czego nie rozumiesz w moim problemie? Prościej się go opisać nie da. Mam kontener z tablicą wskaźników na typ bazowy. Każdy wskaźnik może zostać polimorficzne alokowany na dowolny typ potomny. Po zwolnieniu pamięci wskaźnik znów staje się pusty i nie zostaje żaden ślad po tym, jaki typ wcześniej był tam allokowany, więc nie da się wywołać funkcji w stylu: allokuj ten sam typ polimorficzny co ostatnio

cóż problem bez sensu. Dealokacja właśnie po to jest kolego żeby zniszczyć ten typ. Problem wydumany bo co to ma niby rozwiązać„?

allokuj ten sam typ polimorficzny co ostatnio

no to się zdecyduj, bo jak ten sam jak ostatnio to w czym problem?


#include <iostream>
#include <typeindex>

struct Base{};
struct D1 : public Base{};
struct D2 : public Base{};

class test 
{
    public:
    std::type_index type;
    Base* arr[1];
    test() : type(typeid(Base)) {}
};

void createLast(auto* ptr) {
  
    if (ptr->type == typeid(D1)) {
        std::cout << "Type d1" << std::endl;
        ptr->arr[0] = new D1;
    } else if (ptr->type == typeid(D2)) {
        std::cout << "Type d2" << std::endl;
        ptr->arr[0] = new D2;
    } else 
    {
        std::cout << "eror";
    }

}

using namespace std;

int main()
{
    
    test* my = new test();
    my->arr[0] = new D1;
    my->type =  typeid(D2);
    delete my->arr[0];
    createLast(my);

    return 0;
}


0

Powtarzam, to nie może bazować na zamkniętej liście typów, więc nie możesz zbudować prostej funkcji, która po prostu porówna jedno z drugim. Lista jest dynamiczna, to może być każdy typ, który przejdzie test is_base_of.

0

Nie, celem jest dynamiczne "zaplanowanie" struktury i późniejsza allokacja lub usunięcie z pamięci (wielokrotnie), w zależności od potrzeb. Np.:

struct Shape{};
struct Triangle : public Shape{};
struct Rectangle : public Shape{};
//itd., może ich być nieskończenie wiele

//Pseudo kod, to poniższe normalnie ma być opakowane w klasę

Shape* Array[10];
Array[0] = new Triangle;
Array[1] = new Rectangle;
//itd., może tu trafić dowolna klasa potomna dziedzicząca po Shape, tj. spełniająca warunek is_base_of<>

A jak powyższa struktura (powtarzam, normalnie upakowana w klasę!) przestaje być potrzebna, to wywołuję funkcję pokroju Free(), dzięki czemu poszczególne komórki Array[] zostają zwolnione, ale "zapamiętują" na jaki typ je uprzednio allokowano. Dzięki temu w dowolnym momencie, gdy znowu muszę mieć tablicę w pamięci, mogę wywołać funkcję w stylu Reallocate() i komórki znów zostają allokowane takimi typami, jakie im "przypisano" w pierwszej allokacji, bez konieczności ponownego, ręcznego przypisywania typu każdej komórce z osobna. Taki jest cel. Prościej się tego wyjaśnić nie da.

1

"zostają zwolnione, ale "zapamiętują" na jaki typ je uprzednio allokowano"

Tego nie wyczarujesz musisz gdzieś zapisać info o typach jakie stworzono czy to jak pokazałem wyżej przez typeid(przecież to ci się w runtime wypełni!!!!). nie bardzo wiem czego nie kumasz.
Tworzysz serię danych która jest w tablicy, obok jest druga w której zpaisano typy id -> usuwasz te elementy z tablicy -> wołasz funkcję która na podstawie tego co jest w tablicy z typami odtworzy wygląd tej że klasy. Można by próbować użyć statycznego polimorfizmu ale tu jak mówiłeś w runtime moga się pojawiać nie z tego ni z owego obiekty.

1
struct Base {
  virtual std::unique_ptr<Base> create() const = 0;
  virtual ~Base() = default;
};

struct D1 : Base {
  std::unique_ptr<Base> create() const override {
    return std::make_unique<D1>();
  }
};
1
revcorey napisał(a):

nie wiem dokładnie co chcesz osiągnąć. Bo to jest coś bardzo dziwnego, poniżej też napiszę coś dziwnego bo nie rozumiem twojego toku myślenia ale spróbuję podążyć za nim.

Zgadzam się w 100%
Ani to lista, ani obiecanych funkcjonalnosci (realokacji)

Mnie razi m. Allocate
Mocno się sprzeciwia filozofii "projektuj API, które nie da się źle użyć". Jak List to zbiór konkretnych obiektów, a nie przypadkowych zaślepek. Jaką wartość ma taka lista ?
Co więcej, wymaga zero-argumentowego konstruktora. Jeśli by jednak, to powinno "rozepchać" sąsiednie komórki. Coraz gorzej ...

Przeczuwam, ale mam podobnie do @revcorey głebokie poczucie że nie rozumiem , nieczyste założenia projektowe i potem grzebiemy z jakimś XY problemem.

0

Rozwiązanie to jest mi potrzebne do "kontenera kontenerów", czyli dynamicznego kolektora RÓŻNYCH typów (coś jak std::vector, tylko dla wielu różnych typów jednocześnie).

template<typename... Objects>
class Level
{
	static constexpr uint32_t ObjectCount = sizeof...(Objects);
private:
	Object*** Data = nullptr;
	uint32_t ObjectID = 0;
	uint32_t ObjectSize[ObjectCount] = { 0 };

	template<typename T>
	constexpr uint32_t ID()
	{
		static const uint32_t ID = ObjectID++;
		return ID;
	}
public:
	Level()
	{
		Data = new Object**[ObjectCount]{ nullptr };
	}
	
	virtual ~Level()
	{
		for (uint32_t I = 0; I < ObjectCount; I++)
		{
			if (Data[I])
			{
				for (uint32_t N = 0; N < ObjectSize[I]; N++) if (Data[I][N]) delete Data[I][N];
				delete[] Data[I];
			}
		}
		if (Data) delete[] Data;
	}

	template<typename T>
	T& Get(const uint32_t& uiIndex = 0)
	{
		return *static_cast<T*>(Data[ID<T>()][uiIndex]);
	}

	template<typename T>
	void AllocateObjectGroup(const uint32_t& uiSize)
	{
		Data[ID<T>()] = new Object*[uiSize]{ nullptr };
	}

	template<typename T>
	void AllocateObject(const uint32_t& uiIndex)
	{
		Data[ID<T>()][uiIndex] = new T;
	}
};

Przykład użycia:

struct Object{};

struct Car : public Object
{
	int Wheels;
};

struct Plane : public Object
{
	int Wings;
 	float Speed;
};

struct Ship : public Object
{
	char Weight;
	long long Height;
}

int main()
{
	//Allokacja

	Level<Plane, Car, Ship> level;	
	level.AllocateObjectGroup<Plane>(3); //Allokuje pamięć w tablicy dla 3 obiektów typu Plane
	level.AllocateObject<Plane>(0); //Allokuje pamięć dla rekordu o typie Plane
 	level.AllocateObject<Plane>(1); //jw.
  	level.AllocateObject<Plane>(2); //jw.

	level.AllocateObjectGroup<Car>(1);
	level.AllocateObject<Car>(0);

	level.AllocateObjectGroup<Ship>(2);
	level.AllocateObject<Ship>(0);
 	level.AllocateObject<Ship>(1);

	//Dostęp do zawartości

	level.Get<Plane>(2).Speed = 321.56f;  
 	level.Get<Ship>(1).Weight = 92;
   	level.Get<Car>(0).Wheels = 6;  
}

Całość działa o tyle "zmyślnie", że nadaje każdemu typowi indywidualny, liczbowy identyfikator, dzięki czemu wiadomo, że Plane to Object**[0], Car to Object**[1], a Ship to Object**[2] i takie właśnie wskaźniki zostają zwrócone. Nie ma z tym problemu, bo całość dzieje się w run-time.

Tylko że gdybym chciał teraz zwolnić pamięć tej struktury, a potem odtworzyć ją ponownie zdalnie (taki jest cel), to informacja o typach zostanie utracona i nie będzie możliwości ich "automatycznego" odbudowania (tak, żeby grupy obiektów "wiedziały", do jakiego typu należą), na czym mi zależy. Dlatego właśnie kombinowałem z upchaniem tych typów w tuple, co jednak nie działa, bo żeby wydostać coś z tuple przy pomocy szablonowego get<>(), trzeba już w compile-time znać indeks (warstwę) poszukiwanego obiektu wewnątrz krotki.

Jeżeli da się to rozwiązać inaczej, to jestem otwarty na sugestie.

1

może dawno nie pisałem w C++ i już nie wiem, z jakimi dzikimi problemami się mierzą programiści tego języka, że muszą wszystko naokoło robić, ale tak na chłopski rozum:

  • nie możesz po prostu trzymać w tej tablicy informacji i zamiast tablicy obiektów zrobić np. tablicę krotek (typ, obiekt)? (albo opakować dane w dodatkową strukturę). Typ mógłby być nawet liczbowy i jakieś switch/case, żeby wiedzieć, który obiekt utworzyć
  • nie możesz zrobić metody wirtualnej klasy Base, która ci zwróci typ czy utworzy na nowo ten obiekt (czyli rozwiązanie z posta @eleventeen)?
0
LukeJL napisał(a):
  • nie możesz po prostu trzymać w tej tablicy informacji i zamiast tablicy obiektów zrobić np. tablicę krotek (typ, obiekt)? (albo opakować dane w dodatkową strukturę). Typ mógłby być nawet liczbowy i jakieś switch/case, żeby wiedzieć, który obiekt utworzyć

A da się stworzyć switcha zwracającego obiekty danego typu (na zasadzie: podaj liczbę jako parametr, otrzymaj obiekt konkretnego typu), przy pomocy szablonów? Bo pisanie osobnej funkcji zwracającej typy, dla każdego kontenera z osobna, jest mało optymalne.

  • nie możesz zrobić metody wirtualnej klasy Base, która ci zwróci typ czy utworzy na nowo ten obiekt (czyli rozwiązanie z posta @eleventeen)?

Ale jak?

Jeżeli mam tablicę wskaźników Object***, która chwilowo będzie przechowywać jakiś typ polimorficzny, np. Ship, jako Object*** Data[1][0] = new Ship, to po zwolnieniu pamięci, nie zostanie po tym żaden ślad pozwalający odczytać ten typ i utworzyć go ponownie.

2
Crow napisał(a):

Dlatego właśnie kombinowałem z upchaniem tych typów w tuple, co jednak nie działa, bo żeby wydostać coś z tuple przy pomocy szablonowego get<>(), trzeba już w compile-time znać indeks (warstwę) poszukiwanego obiektu wewnątrz krotki.

A może jednak?

template<typename... Ts>
class Level
{
  using Types = std::tuple<Ts...>;
  static constexpr auto TypesNum = std::tuple_size_v<Types>;
  std::array<std::vector<Object*>, TypesNum> Objects;

  public:
   template<typename T>
   T* Get(int objectIndex)
   {
     constexpr auto idx = tuple_element_index_v<T, Types>;
     return static_cast<T*>(Objects[idx][objectIndex]);
   }
};

Jak wyciągnąć indeks typu z tuple masz m.in tutaj: Get index of a tuple element type i tutaj Mundane std::tuple tricks: Finding a type in a tuple

0

@tajny_agent: Tak, to też jest fajne podejście (bo nie wymaga generowania ID), ale zauważ, że nie rozwiązuje problemu i dotyczy tego, co już mam, a więc drogi od typu do niestatycznego numerycznego indeksu. A ja poszukuję rozwiązania dla czegoś odwrotnego, drogi od niestatycznego numerycznego indeksu do typu:

  1. Weź tablicę wskaźników, wskazującą na tablicę wskaźników, wskazującą na wskaźnik typu bazowego (tutaj Object***).
  2. Utwórz tyle tablic typów (Object**[X]), ile typów podanych w szablonie Level<>.
  3. Przypisz (zapisz, utrwal, przechowaj - zwał jak zwał) do poszczególnych tablic typy podane w szablonie (np. Object**[0] = Plane, Object**[1] = Ship, Object**[2] = Car)
  4. Utwórz dowolną ilość polimorficznych typów, w przypisanych im tablicach.
  5. Zwolnij pamięć dla wszystkich elementów w tablicy.

[WSZYSTKO POWYŻSZE JUŻ MAM]

  1. Ponownie wypełnij tablice przypisanymi im uprzednio typami, ale tym razem bez ponownego wskazywania typu jako statycznego argumentu funkcji, a więc np. level.ReallocateAll(). <---- Tego nie mam.
//PSEUDO-KOD

level.AllocateObjectGroup<Plane>(3);
level.AllocateObject<Plane>(1);
level.AllocateObject<Plane>(2);
level.AllocateObject<Plane>(3);

level.AllocateObjectGroup<Ship>(5);
level.AllocateObject<Ship>(0);
level.AllocateObject<Ship>(1);
level.AllocateObject<Ship>(2);
level.AllocateObject<Ship>(3);
level.AllocateObject<Ship>(4);

level.AllocateObjectGroup<Car>(3);
level.AllocateObject<Car>(0);
level.AllocateObject<Car>(1);
level.AllocateObject<Car>(2);

level.Free(); //Zwalnia pamięć dla wszystkich obiektów
level.Reallocate(); //Ponownie allokuje 3 obiekty typu Plane, 5 obiektów typu Ship i 3 obiekty typu Car, w przypisanych im tablicach.

Wszystkie zasugerowane do tej pory metody w zasadzie zataczają koło, bo koniec końców wymagają statycznego podania (w szablonowym <>) albo typu obiektu, albo nadanego mu w jakiejś formie, numerycznego identyfikatora, co wyklucza automatyzację tego procesu.

Nie mam pojęcia, czy to co chcę uzyskać jest w ogóle możliwe, biorąc pod uwagę charakterystykę języka. Może to z góry przegrana walka, albo są lepsze sposoby, dlatego właśnie pytam na forum.

Ewentualnie mogę spróbować to całkiem przeprojektować, żeby nie była to jedna tablica wskaźników, tylko różne tablice, trzymane osobno w tuple.

2

Pytasz o dynamiczną refleksję, której to póki co w C++ nie ma, dopiero rodzi się w bólach. Póki co można korzystać z ułomnych protez pokroju rttr:

#include <iostream>
#include <vector>
#include <memory>

#include <rttr/registration>
using namespace rttr;

struct Base{
    virtual ~Base() {}

    RTTR_ENABLE()
};

struct D1 : public Base {
    D1() { std:: cout << "D1()\n"; }
    ~D1() { std:: cout << "~D1()\n"; }

    RTTR_ENABLE(Base)
};

struct D2 : public Base {
    D2() {  std:: cout << "D2()\n"; }
    ~D2() { std:: cout << "~D2()\n"; }

    RTTR_ENABLE(Base)
};

RTTR_REGISTRATION
{
    registration::class_<D1>("D1").constructor<>()(policy::ctor::as_std_shared_ptr);
    registration::class_<D2>("D2").constructor<>()(policy::ctor::as_std_shared_ptr);
}

int main() {
    std::vector<std::shared_ptr<Base>> elements;
    std::cout << "\tConstruct\n";
    elements.emplace_back(std::make_shared<D1>());
    elements.emplace_back(std::make_shared<D2>());
    elements.emplace_back(std::make_shared<D1>());
    elements.emplace_back(std::make_shared<D2>());

    // save types
    std::vector<std::string> types;
    for(auto& e: elements) {
        types.push_back(std::string(type::get(*e.get()).get_name()));
    }

    std::cout << "\tClear\n";
    elements.clear();

    std::cout << "\tRecreate\n";
    for(auto& type_name: types) {
        type t = type::get_by_name(type_name);
        elements.emplace_back(t.create().get_value<std::shared_ptr<Base>>());
    }

    std::cout << "\tClear\n";
    elements.clear();
}
$ g++ test.cpp -o test -lrttr_core 
$ ./test 
	Construct
D1()
D2()
D1()
D2()
	Clear
~D1()
~D2()
~D1()
~D2()
	Recreate
D1()
D2()
D1()
D2()
	Clear
~D1()
~D2()
~D1()
~D2()
0
Crow napisał(a):

Jeżeli mam tablicę wskaźników Object***, która chwilowo będzie przechowywać jakiś typ polimorficzny, np. Ship, jako Object*** Data[1][0] = new Ship, to po zwolnieniu pamięci, nie zostanie po tym żaden ślad pozwalający odczytać ten typ i utworzyć go ponownie.

Czemu utozsamiasz destrukcje obiektu ze zwolnieniem pamieci? OK, jesli dane ktore przechowuje obiekt zajmuja kawalek byte to rzeczywiscie pojedyncze wskazniki tez kosztuja. Ale w innych przypadkach spokojnie mozna zostawic obiekt i zwolnic pamiec. Daleko nie szukajac np:

std::unique_ptr<Data> ptr;

Owszem, minimalna pamiec dla unique_ptr jest przydzielona, ale Data nie zajmuje nic.

0

@eleventeen: To może inaczej... Popatrz na ten kod:

std::unique_ptr<Base> PTR; //Wskaźnik wskazujący na typ bazowy Base;
PTR = std::make_unique<D1>(); //Pamięć zostaje allokowana na typ polimorficzny D1;
PTR.reset(); //Pamięć wskaźnika zostaje zwolniona;

Zagadka: Jak ponownie allokować powyższy wskaźnik na typ D1, ale NIE PODAJĄC szablonowego, statycznego argumentu (<D1>), tylko pobrać go z jakiegoś nie-statycznego źródła, które ów typ "zapamiętało"? Chciałbym móc wywołać funkcję w stylu MakeTheSamePointerAsBefore(PTR) i w ten sposób uzyskać std::unique_ptr<Base> ponownie allokowany na D1. O to mi właśnie chodzi od samego początku.

0
Crow napisał(a):
std::unique_ptr<Base> PTR; //Wskaźnik wskazujący na typ bazowy Base;
PTR = std::make_unique<D1>(); //Pamięć zostaje allokowana na typ polimorficzny D1;
PTR.reset(); //Pamięć wskaźnika zostaje zwolniona;

Po co piszesz cos co nie zadziala zamiast po prostu przekleic kod ktory dziala? To jeszcze raz to samo, ale minimalnie rozbudowane:

struct D1 : Base {
  void alloc() override {
    impl = std::make_unique<D1Impl>();
  }

  void free() override {
    impl.reset();
  }

private:
  std::unique_ptr<D1Impl> impl;
};

D1Impl moze zajmowac dowolnie duzo pamieci, ale po zawolaniu free() zostanie zwolnione prawie wszystko. Zostana pojedyncze wskazniki na przechowanie D1. Allokacja D1Impl tez nie jest zadnym problemem majac liste Base.

0

Dobrze, to teraz poproszę o kod, w którym allokujesz std::unique_ptr<Base> przy pomocy D1Impl, nie podając D1Impl jako szablonowego argumentu (<D1impl>), tylko pobierając go z utworzonej dynamicznie listy.

Czemu majac dostep do D1 a nie majac dostepu do prywatnego D1Impl koniecznie chcesz wykorzystac to czego nie mozesz? Ale bez problemu mozesz przydzielic pamiec ktora przechowuje D1Impl:

struct Base {
  virtual void alloc() = 0;
  virtual void free() = 0;
  virtual void printPtr() const = 0;
  virtual ~Base() = default;
};
struct D1Impl {
  std::unique_ptr<uint8_t[]> data;
  D1Impl() : data(std::make_unique<uint8_t[]>(1'000'000)) {}
};
struct D1 : Base {
  D1() {
    alloc();
  }
  void alloc() override {
    impl = std::make_unique<D1Impl>();
  }
  void free() override {
    impl.reset();
  }
  void printPtr() const override {
    std::cout << impl.get() << std::endl;
  }
private:
  std::unique_ptr<D1Impl> impl;
};
int main() {
  std::vector<std::unique_ptr<Base>> v;
  v.push_back(std::make_unique<D1>());
  v[0]->printPtr();
  v[0]->free();
  v[0]->printPtr();
  v[0]->alloc();
  v[0]->printPtr();
}
-------------------------
$ ./a.out 
0x560525e73ed0
0
0x560525e73ed0
1

Wciąż nie wiem jaki jest cel, jakie operacje chcesz przeprowadzać na tych obiektach i dlaczego one wszystkie muszą być w jednym kontenerze.
Ale jeśli chcesz mieć możliwość "przywracania" to w jakiś sposób musisz zapamiętać, który typ gdzie był. Może coś na styl type erasure?

class Container
{
 private:
  struct ObjectEntry
  {
   Object* Instance;
   Object*(*Creator)(void);
   ObjectEntry(Object* InInstance, Object*(*Fn)(void)) : Instance{InInstance}, Creator{Fn} {}
  };

  template<typename T>
  static Object* CreatorImpl()
  {
    return new T{};
  }

  std::vector<ObjectEntry> Objects;

public:
 template<typename T>
 void AllocateN(std::uint32_t Num)
 {
   for (auto i = 0u; i < Num; ++i) {
    Objects.emplace_back(new T{}, &CreatorImpl<T>);
   }
 }

 void Clear()
 {
  for (auto& Entry : Objects) {
    delete Entry.Instance;
    Entry.Instance = nullptr;
  }
 }

 void Restore()
 {
   for (auto& Entry : Objects) {
     Entry.Instance = Entry.Creator();
  }
 }
};

void func()
{
  auto Storage = Container{};
  Container.AllocateN<Car>(5);
  Container.Clear();
  Container.Restore();
}

(pominąłem casty/error-check/etc.)

Tylko to wymusza, że typy muszą być default-constructible
Da się pewnie zmniejszyć narzut wyciągając Creator żeby był per-typ a nie per-instance dla obiektów, ale nie w tym rzecz tutaj,

0

Ciekawy problem. Może podejście pana Parenta?

#include <iostream>
#include <memory>
#include <vector>

template <typename Base> class remember_who_you_are {
public:
  template <typename T> void allocate() {
    m_factory = std::make_unique<factory<T>>();
    m_object = m_factory->create();
  }

  void free() {
    m_object.reset();
  }

  void reallocate() {
    m_object = m_factory->create();
  }

  Base *access() {
    return m_object.get();
  }

private:
  class factory_interface {
  public:
    virtual ~factory_interface() = default;

    virtual std::unique_ptr<Base> create() = 0;
  };

  template <typename T> class factory : public factory_interface {
  public:
    std::unique_ptr<Base> create() override {
      return std::make_unique<T>();
    }
  };

  std::unique_ptr<factory_interface> m_factory;
  std::unique_ptr<Base> m_object;
};

struct base {
  virtual ~base() = default;
  virtual std::string foo() = 0;
};

struct d1 : public base {
  std::string foo() override {
    return "d1";
  }
};

struct d2 : public base {
  std::string foo() override {
    return "d2";
  }
};

int main() {
  std::vector<remember_who_you_are<base>> list(2);

  list[0].allocate<d1>();
  list[1].allocate<d2>();

  std::cout << "Remember\n";
  for (auto &entry : list) {
    if (base *obj = entry.access(); obj) {
      std::cout << obj->foo() << '\n';
    }
  }

  std::cout << "\nWho\n";
  list[0].free();

  for (auto &entry : list) {
    if (base *obj = entry.access(); obj) {
      std::cout << obj->foo() << '\n';
    }
  }

  std::cout << "\nYou\n";
  list[0].reallocate();

  for (auto &entry : list) {
    if (base *obj = entry.access(); obj) {
      std::cout << obj->foo() << '\n';
    }
  }

  std::cout << "\nAre\n";
  list[0].allocate<d2>();
  list[1].allocate<d1>();

  for (auto &entry : list) {
    if (base *obj = entry.access(); obj) {
      std::cout << obj->foo() << '\n';
    }
  }

  std::cout << "\nSimba!\n";
}
Remember
d1
d2

Who
d2

You
d1
d2

Are
d2
d1

Simba!

O, @tajny_agent napisał to samo

1

Ale o czym Wy tu dyskutujecie... Przecież @Crow napisał jaki jest cel (podkreślenie moje):

Tylko że gdybym chciał teraz zwolnić pamięć tej struktury, a potem odtworzyć ją ponownie zdalnie (taki jest cel), to informacja o typach zostanie utracona i nie będzie możliwości ich "automatycznego" odbudowania (tak, żeby grupy obiektów "wiedziały", do jakiego typu należą), na czym mi zależy.

@recovery: już wspomniał o serializacji.

1
Bartłomiej Golenko napisał(a):

@recovery: już wspomniał o serializacji.

Prawie czwarta strona wątku, i pojawia sie prawidłowe słowo

Serializacja w C++ nie jest aż tak "natywna" jak w Javie, C# czy Pythonie, ale temat jest dostatecznie dobrze przepracowany.

W tym wątku ZDECYDOWANIE za dużo pary poszło w a) wartość pustą (strasznie dużo energii) b) przechowanie typu w oderwaniu od wartości tego typu
Rażący mnie pusty konstruktor itd... by znalazł dobry zamiennik w new T(stream) czy coś tym stylu, ew fabryka czy innych wzorzec kreacyjny.

A jeśli to obiekty jakiegoś subsystemu, straciłem rachubę o co chodzi, gra czy coś innego, dziedziczenie ze wspołnego przodka czyni ten wręcz szkolnym (prawie) bez jakiejś zbędnej hakerki w chmurach C++ v. 2050
Odwołam się do pakietów GUI C++, gdzie "w gratisie" dają kolekcje na * OurTheBestObject. Brutalnie prymitywne (jak na poziom "hakerki C++") i skutecznie załatwia problemy. Borland C++ Burdel miał serializację takich "swoich obiektów", i/albo MS MFC też,

0

To może po kolei, bo się jakiś tumult zrobił w temacie :). Ten wątek jest niejako powiązany z innym zagadnieniem (Struktura danych bez ECS w silniku gry) i dotyczy sposobu przechowywania danych wewnątrz gry.

U mnie takie paczki danych, będące aktualnie allokowane w pamięci to Levele, stąd nazwa klasy. Generalnie zastanawiałem się, jak zbudować taką strukturę, która przechowuje obiekty różnego typu i jednocześnie nie robić tego przez staroszkolne i statyczne dziedziczenie, w stylu:

class LevelBase
{
protected:
  virtual void Load() = 0;
}

class Level : public LevelBase
{
private:
  void Load() override
  {

  }
public:
  struct
  {
    Obj_Skeleton* Skeleton = nullptr;
    Obj_Orc* Orc = nullptr;
    Obj_Dragon Dragon* = nullptr;
  } Enemies;

  struct
  {
    Obj_Merchant* Merchant = nullptr;
    Obj_Guard* Guard = nullptr;
  } NPC;
}
//itd.

Nie uśmiechało mi się projektowanie wielu takich struktur dla każdej gry z osobna (a jednocześnie nie chciałem korzystać z ECS), więc zacząłem kombinować z czymś bardziej dynamicznym. No i wymyśliłem, klasa szablonowa, która jako argumenty przyjmuje dowolną liczbę typów, a potem tworzy dla każdego własną listę generycznych wskaźników, które można allokować na dowolny typ polimorficzny i potem odpowiednio rzutować:

level.Get<Skeleton>(0) //Dzięki szablonowemu argumentowi <Skeleton> klasa wie na jakiej liście obiektów 
//ma szukać (bo każdy typ ma nadany dynamicznie, własny identyfikator) i dodatkowo na jaki typ ma rzutować zwracany obiekt

Funkcjonalne i całkiem eleganckie, ale zabrakło mi jednej, bardzo ważnej rzeczy - leniwej allokacji pamięci. Tworząc grę, chciałbym móc przygotować poziomy wcześniej, np.:

Level<Orc, Skeleton, Sword> Level01;
Level01.AllocateObjectGroup<Orc>(10) //Allokuje generyczną tablicę (Object*[0]) o wielkości 10 rekordów.
Level01.AllocateObjectGroup<Sword>(2) //Allokuje generyczną tablicę (Object*[1]) o wielkości 2 rekordów.
Level01.AllocateObject<Orc>(0) //Allokuje generyczny wskaźnik (Object*) w pierwszym rekordzie indywidualnej tablicy (Object[0][0]) na typ Orc.
Level01.AllocateObject<Orc>(1) //Allokuje generyczny wskaźnik (Object*) w drugim rekordzie indywidualnej tablicy (Object[0][1]) na typ Orc.
itd.

Potem tak samo dla levelu 2, 3, 5... 176 itd. Tyle że nie będę przecież trzymał tych wszystkich poziomów w pamięci jednocześnie, więc muszę mieć możliwość ich dowolnej allokacji i zwalniania w miarę potrzeb. No ale tablice operują na typach generycznych Object*, więc po zwolnieniu pamięci nie jestem w stanie odtworzyć ich poprzedniego typu i stąd mój problem.

No ale już prawie go rozwiązałem. Zrobiłem wersję, która zamiast generycznych tablic używa twardych typów popakowanych w krotkę (std::tuple). Testuję też możliwość przechowywania typów przy pomocy wrapperów na właściwe typy ObjectWrapper<Object>. Jak skończę, wrzucę kod na jeden i drugi sposób, żeby domknąć temat.

Tak czy inaczej, dzięki wszystkim za sugestie i burzę mózgów, wiele postów posłużyło mi za dobrą wskazówkę jak się z problemem uporać.

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