Własny kontener (vector) - w celach edukacyjnych; ocena + kilka pytań

0

W celach edukacyjnych chciałem sobie napisać własny kontener w stylu vectora. Najpierw chciałem wsio napisać dla typu int, żeby skupic sie na samym kontenerze, a później przerobić gotowca na szablon (wczesniej nie bawilem sie szablonami). Dlatego też wszędzie gdzie owy int ma być zastąpiony innym typem daję TYP, żeby łatwiej odnaleźć miejsca, gdzie trzeba to zmienic przy robieniu z tego szablonu :)

Generalnie to co zrobiłem działa, jednak mam kilka pytań oraz prosiłbym o ogólną ocenę mojego kodu i rady, co mozna zrobic lepiej itp.

Pierwsze pytanie, to czy zawsze blok catch musi byc od razu po try? Po przejrzeniu Pasji C++ na wykładach nie zauwazylem, zeby to bylo wymagane, ale jak chcialem uzyc mechanizmu obsługi wyjątków to okazało się, że inaczej kompilator mi nie przepuści kodu.

Drugie, to czy da się szybciej/lepiej powiększyć tablicę dynamiczną? Czy lepiej użyć memcpy czy przypisanie element po elemencie, czy moze jeszcze inaczej?

Kolejna zagwozdka, czy jest sens zerowania każdego nowego obszaru tablicy, czy moze lepiej po prostu nie pozwalać odwoływać się/wypisywać elementów o indeksie wiekszym niz ilosc wpisanych elementow-1? Chodzi wciąż o odwoływanie się w zakresie zarezerwowanego obszaru, nie poza nim.

Nie uzywalem jeszcze pojemników, w ogole doswiadczenie mam niewielkie, a w czasie semestru nie mialem za wiele czasu na kodzenie (teraz czekam na wyniki ostatniego egzaminu w tej sesji, wiec troche wiecej czasu juz mam :) ).

W załączniku jest kod, dwa pliki cpp i jeden hpp. :)

0

Patrząc na szybko:

  1. Dlaczego domyślnie wielkość wektora to 128? Trochę dużo jak na wartość domyślną. Niby można explicite podać mniej, ale według mnie domyślnie powinno być 1.
  2. Co do powiększania, najczęściej alokuje się zawsze dwa razy więcej pamięci, czyli np. jeśli masz tablicę 100-elementową i wstawiasz nowy element, do którego potrzeba relokacji, to realokujesz do 200 elementów. Aktualny sposób jest kiepski, bo zależny od tego, ile na początek poda się w rozmiarze – jeśli ktoś poda 1, to masz realokację przy każdym dodaniu.
  3. Przydałaby się metoda, która zmienia rozmiar wektora (resize()) oraz „przycinająca” wielkość pamięci zaalokowanej do używanej (fit()) – zauważ, że teraz wektor tylko się powiększa.
  4. Konwencja nazw ssie – pomijając zbytnią opisowość metod, niektóre nazwy wprowadzają w błąd (Wypisz() nie wypisuje, tylko pobiera i zwraca element, Rozmiar() powinien robić to, co niepotrzebna metoda Ile_Jest(), a to, co aktualnie zwraca Rozmiar() należałoby nazwać – według konwencji – Ile_Zarezerwowano()).
  5. Z podstawowych błędów: brakuje konstruktora kopiującego i operatora przypisania.
    Polecam przyjrzeć się interfejsowi std::vector i spróbować zrobić coś podobnego. Na początek możesz darować sobie iteratory. :)
    Edit Jeszcze pytania:

Pierwsze pytanie, to czy zawsze blok catch musi byc od razu po try?

Koniecznie, na takiej samej zasadzie, jak else musi być od razu po if.

Drugie, to czy da się szybciej/lepiej powiększyć tablicę dynamiczną? Czy lepiej użyć memcpy czy przypisanie element po elemencie, czy moze jeszcze inaczej?

Szybsze od przepisywania po elemencie jest memcpy(), ale jeśli planujesz przerobić klasę na szablonową, i ma prawidłowo działać dla obiektów dowolnej klasy, to lepiej jest przepisywać. Już samo to, że w wektorze możesz mieć klasę z polem wskaźnika – przy memcpy() skopiujesz wartość wskaźnika, który wciąż będzie wskazywał na ten sam blok pamięci. Paskudne.

Kolejna zagwozdka, czy jest sens zerowania każdego nowego obszaru tablicy, czy moze lepiej po prostu nie pozwalać odwoływać się/wypisywać elementów o indeksie wiekszym niz ilosc wpisanych elementow-1? Chodzi wciąż o odwoływanie się w zakresie zarezerwowanego obszaru, nie poza nim.

Użytkownika nie obchodzi, ile zaalokowałeś pamięci, tylko ile on sobie zarezerwował. Nie zachęcaj go do łamania jego własnych reguł. Jeśli użytkownik chce 10 elementów, a ty zarezerwujesz 50, to mimo to próba odwołania do elementu 11. powinna rzucać wyjątek. Co za tym idzie – nie przejmuj się inicjalizacją elementów powyżej faktycznie używanego zakresu.

A tak w ogóle, daj sobie spokój z inicjalizacją, niech tym martwi się użytkownik. W większości przypadków i tak potrzebne mu są inne wartości początkowe niż ty domyślnie ustawisz, więc nie ma to sensu, bo user i tak od razu je nadpisze. Strata czasu.

0

Według mnie minimalny interfejs, taki w sam raz do nauczenia się przy nim czegoś, powinien wyglądać mniej więcej tak:

template<typename T>
class Vector
{
public:
    Vector();
    explicit Vector(size_t size, T init = T());
    Vector(const Vector& rhs);
    ~Vector();
    
    Vector& operator=(const Vector& rhs);
    
    T  operator[](size_t pos) const;
    T& operator[](size_t pos);
    
    T get(size_t pos) const; // zwraca element pod indeksem
    void set(T elem, size_t pos); // ustawia element undeksem
    
    void push(T elem); // dodaje element na koniec
    void add(T elem, size_t pos); // wstawia element po elemencie na podanej pozycji
    void pop(); // usuwa element z konca
    void del(size_t pos); // usuwa element o podanym indeksie
    
    void resize(size_t new_size); // zmienia rozmiar (moze byc mniejszy od aktualnego)
    void fit(); // zmniejsza obszar zaalokowany do minimum koniecznego do zachowania elementow
    
    size_t size() const; // zwraca rozmiar wektora jako ilosc elementow zapisanych
    size_t buffer_size() const; // zwraca wielkosc zaalokowanej tablicy
};

Szczegóły implementacyjne pozostawiam do Twojej dyspozycji – w końcu na tym polega enkapsulacja. ;)

0

W praktyce domyślny rozmiar robi się mniejszy - np. 16. Gdy brakuje miejsca, alokuje się 2x więcej miejsca niż poprzednio - i odwrotnie, przy usuwaniu elementów, gdy wektor staje się zajęty w mniej niż 1/4, zmniejsza się go o połowę.

0

Dzięki za posty, w najbliższej wolnej chwili poprawie i dopracuje to co jest no i dorobie nowa funkcjonalnosc :) Pewne rzeczy mialem w planach, bo to co wrzucilem nie bylo oczywiscie skonczonym projektem.

0

Tak teraz pomyślałem, że z tym brakiem inicjalizacji w pierwszym poście byłem zbyt radykalny. ;) Więc uściślę:

  1. W konstruktorze ustawiasz rozmiar tablicy podany przez użytkownika i każdy element inicjalizujesz na wartość init konstruktora (domyślnie T(), czyli konstruktor domyślny; dla typów prostych, np. int, automatycznie się zeruje).
  2. Przy resize() do większego rozmiaru nowo dodane elementy inicjalizują się na wartość domyślną T() (można też pomyśleć o podobnym myku jak przy konstruktorze, czyli drugi parametr domyślny, na który ustawiane są nowo dostępne elementy).
    Poza tym żadna inicjalizacja nie ma sensu i jest redundantna.

Warto też dodać metodę reserve(size_t), która zmienia rozmiar wewnętrznego bufora do zadanej wartości – dzięki temu np. push(T) może działać szybciej, jeśli wiesz, ile elementów będziesz dokładał, bo od razu realokujesz tyle pamięci ile trzeba, bez kilku realokacji przy size() == buffer_size(). Jeśli parametr w reserve() jest mniejszy niż size(), to rzucasz wyjątek. I powtarzam – dopóki elementy bufora nie są dostępne dla usera (czyli te o indeksie >= size()), to inicjalizacja ich to strata czasu.

Edit Z czasem widzę coraz więcej problemów do rozwiązania. ;) Na przykład co zrobić w przypadku, gdy T jest klasą bez konstruktora bezparametrowego – wtedy nie da się realokować bufora operatorem new[], bo nie wiadomo, jakie parametry przekazać konstruktorowi. To przypadki brzegowe, ale jeśli chcesz to zrobić dobrze, to warto je obsłużyć. Dlatego powinno się przydzielać pamięć za pomocą malloc()/calloc(), które tylko alokują pamięć, bez konstruowania obiektów, a potem przypisywać właściwe wartości – init z konstruktora, elem z push(), etc. Dodatkowy argument za tym, żeby nie dawać użytkownikowi dostępu do bufora ponad size()-1.

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