• Najnowsze pytania
  • Bez odpowiedzi
  • Zadaj pytanie
  • Kategorie
  • Tagi
  • Zdobyte punkty
  • Ekipa ninja
  • IRC
  • FAQ
  • Regulamin
  • Książki warte uwagi

Obiekty w C+ — pytanie o koncept na podstawie problemów w C++

Aruba Cloud - Virtual Private Server VPS
0 głosów
162 wizyt
pytanie zadane 8 grudnia 2024 w C i C++ przez overcq Pasjonat (22,440 p.)
edycja 9 grudnia 2024 przez overcq

Zwykle programuję w C (a właściwie w jego rozszerzeniu C+), ale zastanawiam się nad obiektowością języka C++.

Wygodnie było by mieć możliwość definiowania obiektów (w C++ nazywanych klasami lub strukturami) w blokach strukturalnych programu, tak by były zwalniane przez kompilator automatycznie po opuszczeniu bloku strukturalnego. Jednak ze względu na sposób, w jaki kompilator obsługuje w programie wynikowym te obiekty, skłaniam się do ich nieużywania. Mianowicie są dwa problemy:

  1. Obiekty są umieszczane na stosie, co zaburza optymalizację rozmiaru stosu, jego ograniczenia do zmiennych tymczasowych, adresów powrotu z procedur — i przewidywalności.
  2. Obiekty są zwalniane przy opuszczaniu bloku strukturalnego programu, co jest naturalne w programie jednozadaniowym (jednowątkowym), ale przeszkadza w programie, w którym następują nieliniowe przełączenia wykonywania. Wiem, że w C++ są operatory “new”/“delete”, ale nie dają mi one nic więcej ponad to, co już mam w C+.

Wymieniłem powyżej dwa problemy (które teraz już zauważam) z obiektami, ale nie te problemy są najważniejsze. One tylko wskazują, iż brakuje innej koncepcji użycia (implementacji przez kompilator) obiektów w języku programowania.

Natomiast teraz zastanowię się nad sensownością użycia obiektów w dotychczasowy sposób.

Można zakładać, że obiekt umieszczony na stosie w bloku strukturalnym programu (na przykład ograniczonym klamrami “{” i “}”) zawiera wewnątrz zmienne, które i tak byłyby użyte, gdyby implementować tę funkcjonalność bez użycia klasy C++. Jednak z moich obserwacji wynika, że klasy zawierają uniwersalne funkcjonalności, które są czasem nadmiarowe względem potrzeb bieżących, więc logiczne, że zawierają wiele zmiennych, które są niepotrzebne w danym kontekście użycia, niewykorzystane. W C+ (opartym bezpośrednio na C, ale bez użycia biblioteki standardowej) rozwiązuję ten problem przez definiowanie obiektu podstawowego i obiektu rozszerzającego, który nie musi być tworzony. Na przykład mam w postaci procedur języka C — definiujących konstruktora, destruktora i metody — obiekt tablicy dynamicznej (zdaje się podobnie jak w C++ “vector”) oraz obiekt rozszerzający: ‘iterator’ elementów tablicy dynamicznej zachowujący poprawny i postępujący stan niezależnie od zmieniana zawartości tablicy. Drugi obiekt korzystając ze zmiennych pierwszego obiektu tworzy własne zmienne tylko wtedy, gdy sam zostanie utworzony, i usuwa je po jego wyrzuceniu (z pamięci), tutaj procedurą destruktora, lub zakończeniu ‘iteracji’.

Powyższy problem mógłby być rozwiązany w C++ przez przydzielanie wewnątrz klasy danych na stercie w przypadku użycia nadmiarowej funkcjonalności. Ale czy tak jest powszechnie robione? Oraz czy to nie wymaga ręcznego zarządzania tworzeniem/wyrzucaniem obiektów wewnątrz takiej klasy, ponieważ nie ma odpowiednich konstrukcji języka programowania?

Ponadto w C+ program wykonuje się w tzw. ‹zadaniach›, które są w powszechnym rozumieniu ograniczonymi wątkami (wirtualnymi lub rzeczywistymi). To powoduje konieczność definiowania zmiennych używanych przez więcej niż jedno ‹zadanie› — w przestrzeni globalnej (poza procedurami). Oczywiście pogrupowałem takie zmienne według zastosowania i umieszczam je w strukturach globalnych. W tym przypadku kompletnie nie mam koncepcji, jak w programie miało by być pomyślane użycie takich zmiennych, w jego składni.

Pytania są następujące. O czym w ogóle piszę: czy taki koncept jest już gdziekolwiek realizowany w języku programowania (w naturalny sposób) i co to za koncept? Jak w hipotetycznej składni programu umieszczać obiekty, które są współdzielone pomiędzy ‹zadaniami›? Jak realizować rozszerzalność obiektów nie w postaci rozbudowy (w C++ dziedziczenie), ale w postaci rozszerzania w przypadku użycia — a to wszystko automatyzowane w składni języka? No i oczywiście pytania z poprzednich akapitów.

Wiem mniej więcej, co miałem na myśli w tym pytaniu. Zamiast bloków strukturalnych języka programowania potrzebne mi są bloki wykonawcze, w których dane byłyby przechowywane pomiędzy procedurami, bez względu na hierarchię strukturalną programu, i usuwane na końcu. Program wykonuje się zmieniając stany obiektów. W pewnym stanie obiektów jest początek bloku wykonawczego, a w innym stanie obiektów jest koniec tego bloku wykonawczego. Ponieważ program wykonuje się centrycznie wokół wejścia danych (np. “socket”) lub wyjścia danych (np. rysowanie obiektów graficznych w oknie), a nie wokół obiektu tworzonego pobocznie, nie jest możliwe użycie bloku strukturalnego. Pewną próbą podejścia do tej sytuacji (powiązania bloku strukturalnego z wykonawczym) jest znany z języka Python blok strukturalny “with”, ale on umożliwia tylko proste użycia, które u mnie nie mają zastosowania. Globalne kolekcje obiektów, których używam (np. kolekcja otwartych plików) są tylko wąskim zastosowaniem bloku wykonawczego, tzn. tylko dla obiektu otwartego pliku. Pełne zastosowanie bloku wykonawczego wymagałoby tworzenia tymczasowych globalnych (by były dostępne pomiędzy procedurami) obiektów, w których byłyby zmienne bloku wykonawczego.

2 odpowiedzi

+1 głos
odpowiedź 8 grudnia 2024 przez WojAbuk Gaduła (3,000 p.)
Nie do konica jest to ten koncept, ale jest coś takiego jak leniwa ocena i może to spełniać podobną funkcję jest używane na przykład w Haskell i F#.
Ogólnie wątpię by twoja koncepcja była stosowana w jakimś języku, bo jak na moje jest to za bardzo optymalizowanie programu pod kontem pamięci kosztem czasu dodatkowo wydaje mi się że to będzie prowadzić do fragmentacji pamięci i strat pamięci związanych z offsetem.
komentarz 8 grudnia 2024 przez overcq Pasjonat (22,440 p.)
Zastanawiam się nad tą ‘lazy evaluation’. Jak zaimplementować ją w języku imperatywnym. Składa się z dwóch elementów: • obliczeń tylko wtedy, gdy są potrzebne, • zapamiętywanie wyników obliczeń dla określonych parametrów. To ostatnie pominę, ponieważ nie muszę mieć funkcji ‘pure’, by to miało sens, ale to pierwsze jest ciekawe. Można to połączyć z zapamiętywaniem przy określonym stanie programu.

By to zaimplementować potrzebowałbym mieć obiekty (np. struktury danych), których konstruktor będzie się wykonywał tylko wtedy, gdy w określonym stanie programu (takim jak wcześniej) dane struktury są jeszcze nie zainicjowane (np. wskaźnik “null”). Natomiast nie wiadomo, jak umieszczać te obiekty w pamięci programu. Musiałyby być w przestrzeni globalnej…



Jeśli chodzi o równowagę pomiędzy ilością pamięci a czasem wykonania, to wydaje się, że w powyższym rozwiązaniu byłaby w języku C lub C++ tylko jedna instrukcja warunkowa (sprawdzenie, czy “null”) sprowadzająca się w kodzie maszynowym do skoku warunkowego po instrukcji testu. Natomiast jeśli chodzi o równowagę pomiędzy ilością pamięci użytą dla kompilacji całościowego obiektu a użytą dla kompilacji obiektu z danymi tworzonymi warunkowo, to jeśli funkcjonalność dodatkowa wymagałaby wystarczająco dużej ilości pamięci, to byłoby opłacalne ich nie przydzielać na starcie. Co więcej – używam specyficznego menedżera pamięci przydzielanej na stosie (czyli jak w C++ “new”/“delete”), który domyślnie nie wyrównuje adresów przydzielanych bloków pamięci do jakiejś dużej wartości, ale do rozmiaru słowa naturalnego CPU, więc tak jak to jest w strukturach danych. Przechowuje on ponadto dwie wartości: adres i rozmiar bloku, czyli dwa razy rozmiar słowa procesora. Jeśli funkcjonalność dodatkowa wymagałaby więcej pamięci niż dwa razy słowo CPU plus jedno słowo CPU na rozmiar wskaźnika do danych w obiekcie, to byłoby opłacalne.
komentarz 8 grudnia 2024 przez WojAbuk Gaduła (3,000 p.)
Z tego co rozumiem twoja metoda wymaga relokacji pamięci, a to jest dość kosztowna operacja.
0 głosów
odpowiedź 8 grudnia 2024 przez adrian17 Mentor (352,580 p.)
edycja 8 grudnia 2024 przez adrian17

Przepraszam, ale duża część tego co napisałeś się nie skleja? Mam wrażenie że masz jakieś fundamentalnie złe zrozumienie czym są "obiekty" w C++ie.

  1. Obiekty są umieszczane na stosie, co zaburza optymalizację rozmiaru stosu, jego ograniczenia do zmiennych tymczasowych, adresów powrotu z procedur — i przewidywalności.

Jedno, że nie wiem w jaki sposób to "zaburza" i w jaki sposób nie jest przewidywalne - to nigdy nie było przewidywalne, również w C. W dodatku kod postaci

// C++
struct Point { int x; int y; };
Point f(){ Point point = {5, 6}; return point; }
// C++
class Point { public: int x; int y; };
Point f(){ Point point = {5, 6}; return point; }
// C
struct Point { int x; int y; };
struct Point f(){ struct Point point = {5, 6}; return point; }

Działa dokładnie analogicznie w każdym przypadku - generuje dosłownie te same instrukcje procesora.

Nie ma tutaj też żadnego "zaburzenia" dotyczącego zmiennych tymczasowych, adresów powrotu etc - wszystko działa analogicznie, niezależnie czy mówimy o adresie zmiennej typu `int` czy `Point`.

Więc nie rozumiem w jaki sposób pierwszy punkt aplikuje się do C++a, ale nie do C.

2. Obiekty są zwalniane przy opuszczaniu bloku strukturalnego programu, co jest naturalne w programie jednozadaniowym (jednowątkowym), ale przeszkadza w programie, w którym następują nieliniowe przełączenia wykonywania.

To też się nie skleja. Albo masz zmienne na stosie i wtedy wszystko działa analogicznie między C i C++em (tylko w C++ie masz dodatkową możliwość zdefiniowania destruktora, co jest zaletą a nie wadą), albo używasz `malloc()` w C / `new` w C++ - i wtedy ręcznie zarządzasz pamięcią. Znowu tutaj nie ma jakiejś fundamentalnej różnicy, szczególnie nie wiem co wielowątkowość ma tutaj do rzeczy. Musiałbyś dać jakiś konkretny przykład, bo bez tego nie jestem w stanie sobie wyobrazić, o czym mówisz.

Jednak z moich obserwacji wynika, że klasy zawierają uniwersalne funkcjonalności, które są czasem nadmiarowe względem potrzeb bieżących, więc logiczne, że zawierają wiele zmiennych, które są niepotrzebne w danym kontekście użycia, niewykorzystane

Kombinujesz. To że masz

struct Point { int x; int y; };
int funkcja() {
    Point p1;
    Point p2;
    p1.x = 5;
    p2.x = 6;
    return p1.x + p2.x;
}

Nikt nie powiedział że "kompilator niepotrzebnie alokuje pamięć na stosie na cały obiekt, mimo że nie potrzebuję zmiennej y". Tutaj nie ma żadnego "marnowania zasobów" a "problemem" o którym mówisz nikt się nie przejmuje, bo on nie istnieje.

komentarz 8 grudnia 2024 przez overcq Pasjonat (22,440 p.)
Nie było moim celem porównywanie języków C i C++, ale proponowanie użycia obiektów C++ w nowy sposób, co wydaje się niemożliwe z użyciem obecnej składni C++. To było intencją wymienienia tych dwóch problemów. Innymi słowy zastanawiałem się, czy użycie C++ umożliwiłoby mi programowanie w nowy sposób, jego składnia by to umożliwiła.

Co do pierwszego, to w przypadku prostych obiektów, tak się dzieje, jak piszesz, ale w przypadku złożonych obiektów, w których używa się ręcznego przydzielania pamięci lub stanu obiektu nadrzędnego, już tak nie musi się dziać. W moim projekcie w C używam trochę innego podejścia do obiektowości: tworzę kolekcje obiektów (tablica dynamiczna) i tzw. ‚metody’ obiektu (tutaj zwykłe procedury tylko odpowiednio nazwane) operują na takiej kolekcji. Nie wiem, jaki to może mieć wpływ na wynik kompilacji, ale na pewno te ‚metody’ zależą od stanu całości. To jest w obszarze ·projektowania nowych funkcjonalności·, a nie ·znanego użycia dotychczasowych·.

Jeśli chodzi o przewidywalność, to miałem na myśli konieczność użycia sterty do składowania kolekcji obiektów, która może mieć ich dowolną liczbę. Jeśli by składować na stosie, to byłby on nieprzewidywalny (zależny od stanu programu).

Co do drugiego, to te tzw. ‹zadania›, których używam są synchronizowane (jednocześnie wykonuje się tylko jedno), chyba że któreś z nich jest uruchomione w osobnym wątku systemowym, to wtedy ma wewnątrz fragmenty synchronizowane. Zastanawiałem się w związku z tym, czy można by zrobić jakąś konstrukcję składniową do użycia zmiennych pomiędzy tymi zadaniami w takim samym sensie, jak obecnie używane są zmienne w pojedynczym bloku strukturalnym (obejmowanym “{” i “}”).
komentarz 8 grudnia 2024 przez adrian17 Mentor (352,580 p.)

Przepraszam, ale efektywnie mówisz innym językiem, więc nikt poza Tobą nie wie dokładnie o czym mówisz, szczególnie z tym drugim punktem. Najlepiej by było, gdybyś pokazał konkretny (ale minimalny) przykład.

Może się okazać że to co robisz to jest super znane, tylko pod inną nazwą (np to co mówisz o metodach operujących na kolekcjach odrobinę brzmi jak ECS, patrz np biblioteka `entt`, ale to mój strzał i mogłem Ciebie zupełnie źle zinterpretować), albo nikt tego nie robi bo nie ma sensu.

Jeszcze boczne uwagi

Oczywiście pogrupowałem takie zmienne według zastosowania i umieszczam je w strukturach globalnych. 

Umieszczanie czegokolwiek w zmiennych globalnych nie jest "oczywiste", wręcz przeciwnie.

Powyższy problem mógłby być rozwiązany w C++ przez przydzielanie wewnątrz klasy danych na stercie w przypadku użycia nadmiarowej funkcjonalności.

Jeśli mowa o

class C {
    int x, y, z;
    string a, b, c;
    // ... other fields ...

    SomeType *some_data; // created lazily if needed
    // (or unique_ptr, or shared_ptr, or std::optional<...> etc)
}

to tak, jak najbardziej się zdarza. Losowe przykłady z Firefoxa (ich RefPtr to mniej więcej std::shared_ptr):

https://searchfox.org/mozilla-central/source/dom/fetch/Request.h#135

https://searchfox.org/mozilla-central/source/dom/base/Document.h#4554

https://searchfox.org/mozilla-central/source/dom/base/Document.h#4627

komentarz 8 grudnia 2024 przez overcq Pasjonat (22,440 p.)

Spojrzałem na to ECS i to byłoby coś podobnego z tą różnicą, że u mnie jest zawsze konkretyzacja, na przykład: kolekcja otwartych plików, kolekcja obiektów graficznych w oknie. Czyli nie mam wyszukiwania obiektów mających jakieś cechy w sensie obiektowym, lecz zawsze jest kolekcja obiektów jednego rodzaju, ale z możliwymi stanami.

Nie chciałem podawać źródeł, ponieważ są napisane w innym rodzaju składni, nazewnictwa, ale ok: tutaj jest na przykład menedżer struktur danych albo tutaj jest menedżer plików.

ECS to jest coś bardziej ogólnego, a u mnie jest konkretyzacja w celu wydajności. Obiekty są zawsze tego samego typu, tylko różnie rozbudowane.

komentarz 8 grudnia 2024 przez adrian17 Mentor (352,580 p.)
edycja 8 grudnia 2024 przez adrian17

ECS to jest coś bardziej ogólnego, a u mnie jest konkretyzacja w celu wydajności.

ECS jest, jaki jest, właśnie dla wydajności dla gier. Twój wariant brzmi jak... zwykłe C++owe obiekty? Chyba?

Bo ten kod, tak samo jak to co pisałeś tutaj, wygląda jak napisany we własnym dialekcie, żeby nikt poza Tobą nie był w stanie o tym rozmawiać :( Może faktycznie wymyśliłeś coś innowacyjnego, ale póki nie będziesz w stanie przetłumaczyć tego na ludzki, to raczej nikogo tym nie przekonasz.

Na razie jedyne co byłem w stanie wywnioskować z Twojego kodu, jest że napisałeś od zera własny alokator (pół-alokator, pół-GC z przenoszeniem pamięci, i to mega niebezpieczny)... z jakiegoś powodu z globalnym lockiem, co brzmi smutno.

komentarz 8 grudnia 2024 przez adrian17 Mentor (352,580 p.)
I tak jak patrzę, to już od 6 lat ludzie Ci na 4programmers mówili że to co robisz, zbyt logiczne nie jest.

Podobne pytania

0 głosów
1 odpowiedź 364 wizyt
0 głosów
2 odpowiedzi 304 wizyt
+1 głos
2 odpowiedzi 1,007 wizyt

93,324 zapytań

142,323 odpowiedzi

322,390 komentarzy

62,653 pasjonatów

Motyw:

Akcja Pajacyk

Pajacyk od wielu lat dożywia dzieci. Pomóż klikając w zielony brzuszek na stronie. Dziękujemy! ♡

Oto polecana książka warta uwagi.
Pełną listę książek znajdziesz tutaj

Wprowadzenie do ITsec, tom 1 Wprowadzenie do ITsec, tom 2

Można już zamawiać dwa tomy książek o ITsec pt. "Wprowadzenie do bezpieczeństwa IT" - mamy dla Was kod: pasja (użyjcie go w koszyku), dzięki któremu uzyskamy aż 15% zniżki! Dziękujemy ekipie Sekuraka za fajny rabat dla naszej Społeczności!

...