Autor Wątek: Opakowanie referencji/wskaźnika do tablicy  (Przeczytany 1729 razy)

Offline karol57

  • Użytkownik

# Czerwiec 01, 2016, 17:35:11
Napisałem klasy opakowujące referencje do dowolnej tablicy udostępniając przy okazji interfejs do tego (typu begin/end, front/back, przypisanie, rozmiar itp) i powstał mały problem, który rozwiązałem hackiem.

Kod: (C++) [Zaznacz]
template<typename T>
struct const_array_ref {
    using value_type = T;
    using const_pointer = const value_type *;
    using const_iterator = const_pointer;
    using size_type = ::std::size_t;
    // + inne typdefy znane z std::vector

    const_array_ref() = delete;
    constexpr const_array_ref(const_iterator b, const_iterator e) noexcept : m_p1(const_cast<void*>((const void*)b)),
                                                                             m_p2(const_cast<void*>((const void*)e)) {}
    constexpr const_array_ref(const T* t, size_type n) : const_array_ref(t, t + n) {}
    template<size_type N>
    constexpr const_array_ref(const T (&t)[N]) : const_array_ref(t, N) {}
    constexpr const_array_ref(const const_array_ref &) noexcept = default;
    constexpr const_array_ref(const_array_ref &&) noexcept = default;

    // + metody znane z std::vector typu begin, size, at, operator==, data, front, ...
    protected:
        void * m_p1;
        void * m_p2;

        constexpr const_pointer _begin() const { return (const_pointer)m_p1; }
        constexpr const_pointer _end() const { return (const_pointer)m_p2; }
};

template<typename T>
struct array_ref : const_array_ref<T> {
public:
    // typedefy jak wyżej

    // konstruktory jak wyżej tylko przyjmują tablice nie const

    // + metody nie const, operator=, itp
protected:
    constexpr pointer _begin() const { return (pointer)const_array_ref<T>::m_p1; }
    constexpr pointer _end() const { return (pointer)const_array_ref<T>::m_p2; }
};

Napisałem analogiczne klasy dla łańcuchów znaków (str_ref) z dodatkowymi metodami.

Cytat: Pytania
Czemu 2 klasy (const i nie-const)?
Załóżmy, że mamy tylko array_ref. Jak napisać konstruktor, który będzie istniał tylko w przypadku deklaracji const array_ref<int> my_ref(some_const_array)? No nie da się, a jak napiszę zwykły konstruktor to przejdzie również array_ref<int> my_ref(some_const_array)

Czemu dziedziczenie?
Aby funkcja przyjmująca const_array_ref przyjęła również array_ref.

Narzut?
Dla x64 domyślną konwencją wywołania jest fastcall. Jeżeli przekażemy referencje w parametrach funkcji to zostaną one wrzucone do rejestrów, natomiast tutaj mamy klasę, więc wyląduje ona na stosie :C
Cała reszta zostanie zoptymalizowana przez kompilator, ew. można się pokusić o force_inline.

Dlaczego osobne klasy dla stringów, a nie jakaś magia z std::enable_if?
array_ref("abc") jest referencją do { 'a', 'b', 'c', '\0' }, a str_ref("abc") do { 'a', 'b', 'c' }

Gdzie jest problem?
Załóżmy, że:
A - const_array_ref
B - array_ref

i wprowadzamy
C - const_str_ref
D - str_ref

oczywiście
'B' konwertuje się do 'A' (dziedziczenie)
'D' konwertuje się do 'C' (dziedziczenie)

Jednak fajnie by było mieć kolejną konwersję, tzn.
'C' --> 'A'
'D' --> 'B'

Której już kolejnym dziedziczeniem nie rozwiążę.
Na razie rozwiązałem to w ten sposób, że nie wprowadziłem 'D', bo go aktualnie nie potrzebuje :)
// --- tutaj był opis jak bym ten problem rozwiązał, ale już rozkminiłem, że przeładowanie opratorów rzutowania powinno pomóc --- //

Niby problem rozwiązany, jednak teraz zamierzam wprowadzić
array_ptr, const_array_ptr, str_ptr, const_str_ptr
Które będą pozwalały na trzymanie niezainicjowanych wartości, dodatkowo operator*, operator-> które zwrócą odpowiednie referencje.
Tutaj przeładowanie operatora rzutowania nie jest takie trywialne, ponieważ fajnie by było, aby zwracał on referencję, a nie obiekt. Rozwiązanie jakie widzę to hack typu
Kod: (C++) [Zaznacz]
template<typename T>
str_ptr<T>::operator array_ptr<T>& () { return *reinterpret_cast<array_ptr<T>*>(this);  }

Podsumowując:
1. Co ogólnie o tym sądzicie, bo coś mi się wydaje, że trochę przekombinowałem?
2. Czy taki hack byłby strasznie zły? W końcu rozmiar i układ zmiennych w pamięci jest ten sam, więc chyba nie jest tak źle. No ale... UB chyba, nie?
2.1 Jak jest strasznie źle, to w jaki inny sposób pogodzić ze sobą to wszystko?

Jeżeli boost coś takiego ma to się pochlastam... Na swoją obronę mam, że nie wiedziałem co wpisać do google oprócz 'boost array reference' :)

Offline Mr. Spam

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

Offline Xion

  • Moderator
    • xion.log

  • +2
# Czerwiec 02, 2016, 03:20:40
Cytuj
1. Co ogólnie o tym sądzicie, bo coś mi się wydaje, że trochę przekombinowałem?
Pierwsze dwie struktury wyglądają do rzeczy: kopiujesz po prostu slice'y z Rusta (&[T] i &str). Pogubiłem się dopiero przy tych *_ptr, bo nie za bardzo widzę, do czego byłyby przydatne.

Cytuj
Jeżeli boost coś takiego ma to się pochlastam... Na swoją obronę mam, że nie wiedziałem co wpisać do google oprócz 'boost array reference' :)
Dlaczego zaraz Boost? 'c++ array slice' informuje o std::slice i std::slice_array, które aczkolwiek nie są tym czego szukasz, bo dotyczą numerycznych tablic std::valarray. Nieopodal aczkolwiek można znaleźć array_view, który jest - z tego co widzę - wynalazkiem MS, ale istnieje przynajmniej jeden jego port. Na oko wydaje się robić mniej więcej to, co twoja wersja.

Ewentualnie, skoro tak bardzo chcesz używać Boosta, jest jeszcze boost::range.

Offline karol57

  • Użytkownik

# Czerwiec 02, 2016, 13:16:33
*_ptr potrzebne mi po to, ponieważ *_ref tworzyłem analogicznie jak referencje z C++, więc:
1. Nie mogą być niezainicjowane
2. Nie mogą wskazywać na null
3. Nie mogą być zmienione

Swoją drogą zaimplementowałem już podstawy *_ptr i nie ma on dodatkowych funkcji, głównie opakowanie na dwa wskaźniki, operatory przypisania jako zmiany wskazywanej tablicy oraz przeciążane operatory * i -> które korzystają niestety z brzydkiego hacka.

O Boost wspomniałem, ponieważ i tak go używam i już kilka razy zdarzyło mi się implementować coś, co było tam zaimplementowane lepiej i wydajniej.

Dzięki za array_view, nie skorzystam w całości, ponieważ chcę mieć podział na str_* i array_*, ale przyda się aby podejrzeć niektóre rzeczy :)

Offline Xion

  • Moderator
    • xion.log

# Czerwiec 02, 2016, 20:39:47
*_ptr potrzebne mi po to, ponieważ *_ref tworzyłem analogicznie jak referencje z C++, więc:
1. Nie mogą być niezainicjowane
2. Nie mogą wskazywać na null
3. Nie mogą być zmienione
No i super! Dlaczego chcesz porzucić te jakże przydatne gwarancje? (1) i (2) ma same zalety, a (3) to żaden problem: zamiast zmieniać referencję, po prostu stwórz nową. Bonus: const vs. nie-const załatwisz typem zmiennej która rzeczoną referencję przechowuje.

Offline karol57

  • Użytkownik

# Czerwiec 02, 2016, 22:35:42
Wiem wiem. Po prostu lubię mieć nad wszystkim kontrolę. Jeżeli piszę int zmienna; to piszę z pełną świadomością tego, że będzie ona zawierać śmieci, nie lubię jak mi podstawowe konstruktory np. zerują taką zmienną (oczywiście w nieuzasadnionych przepadkach).
Tak samo jak korzystam ze wskaźnika, to korzystam z niego z pełną świadomością tego co może się zepsuć. Lubię się bawić na niskim poziomie i dopóki piszę sam to będę się bawić (nawet jakbym miał potem debugować instrukcję po instrukcji przez pół dnia w asmie dowiadując się, że gdzieś skorzystałem z niezainicjowanej zmiennej) :)

*Bonus
No właśnie const/nie-const nie wiem jak załatwić :/ Dajmy na to, że rozwiązuję to w 1. klasie (dla prostoty dla int[]).

Kod: (c++11) [Zaznacz]
struct int_arr_ref {
    private:
        int* _begin; // Hmmm... const, czy nie const... :/ No nie, bo zależy od tego czy klasa jest const
        int* _end;
    public:
        // Tutaj metody dostępowe const/nie-const prościzna
       
        template<size_t N>
        int_arr_ref(int (&arr)[N]); // No ok. A jak ktoś zrobi const int_arr_ref(tablica) to
                                    // w sumie ten konstruktor nie musi istnieć
       
        template<size_t N>
        int_arr_ref(const int (&arr)[N]); // Hmm... ale _begin i _end nie są const
                                          // Dodatkowo ten konstruktor nie powinien działać dla
                                          // int_arr_ref(const_tablica) bo klasa pozwalałaby
                                          // na modyfikowanie stałej tablicy
};

Teraz wpadłem na rozwiązanie, że skoro robię to na szablonach to w sumie informacje o tym czy jest const mogę przekazywać do typename T i wtedy (chyba) musiałbym powalczyć z enable_ifami, ale w sumie nie wiem czy moje rozwiązanie z dwiema klasami nie jest prostsze, muszę przemyśleć to.

Offline Xion

  • Moderator
    • xion.log

# Czerwiec 03, 2016, 01:31:20
Cytuj
No właśnie const/nie-const nie wiem jak załatwić :/
Wszystko const. Pola, konstruktory, parametry. Obiekt całkowicie immutable. Chcesz coś zmienić, robisz nowy. Metody "modyfikujące" zwracają kopie. Nadmiar obiektów usuwany przez RVO i move semantics.