2017-06-26 99 views
7

Wprowadzam dwuwymiarowy kontener tablicy (np. boost::multi_array<T,2>, głównie do ćwiczeń). Aby użyć notacji podwójnego indeksu (a[i][j]), wprowadziłem klasę proxy row_view (i const_row_view, ale tutaj nie chodzi mi o stałą), która utrzymuje wskaźnik na początku i końcu wiersza.Ważność wskaźnika zwróconego przez operatora->

Chciałbym również, aby móc iteracyjne wierszy i ponad elementów w rzędzie oddzielnie:

matrix<double> m; 
// fill m 
for (row_view row : m) { 
    for (double& elem : row) { 
     // do something with elem 
    } 
} 

Teraz klasa matrix<T>::iterator (która jest przeznaczona do iteracyjne nad rzędami) prowadzi prywatną row_view rv; wewnętrznie śledzić wiersz, na który wskazuje iterator. Naturalnie iterator realizuje również funkcje dereferenciation:

  • dla operator*(), należałoby zazwyczaj chcą zwrócić odwołanie. Zamiast tego, tutaj odpowiednia rzecz wydaje się zwrócić wartość row_view (tj. Zwrócić kopię prywatnego row_view). Zapewnia to, że gdy iterator jest zaawansowany, row_view nadal wskazuje poprzedni wiersz. (W pewnym sensie row_view działa jak referencja).
  • dla operator->(), nie jestem tego taki pewien. Widzę dwie opcje:

    1. zwracają wskaźnik do prywatnej row_view z iteracyjnej:

      row_view operator->() const { return &rv; } 
      
    2. zwracają wskaźnik do nowego row_view (kopia prywatnej jeden). Ze względu na czas przechowywania, musiałoby to zostać przydzielone na stercie. W celu zapewnienia Clean-up, ja owinąć go w unique_ptr:

      std::unique_ptr<row_view> operator->() const { 
          return std::unique_ptr<row_view>(new row_view(rv)); 
      } 
      

Oczywiście, 2 jest bardziej poprawna. Jeśli iterator jest zaawansowany po wywołaniuoperator->, zmieni się row_view wskazany w 1. Jednak tylko w ten sposób mogę myśleć, gdzie byłoby to znaczenia, jeżeli jest operator-> został nazwany przez jego pełnej nazwy i zwrócony wskaźnik był związany:

matrix<double>::iterator it = m.begin(); 
row_view* row_ptr = it.operator->(); 
// row_ptr points to view to first row 
++it; 
// in version 1: row_ptr points to second row (unintended) 
// in version 2: row_ptr still points to first row (intended) 

jednak nie jest to w jaki sposób chcesz zazwyczaj korzystają operator-> . W takim przypadku należy prawdopodobnie zadzwonić pod numer operator* i zachować odniesienie do pierwszego wiersza. Zwykle można by natychmiast użyć wskaźnika do wywołania funkcji składowej row_view lub uzyskania dostępu do elementu, np. it->sum().

Moje pytanie teraz brzmi: Biorąc pod uwagę, że składnia -> proponuje natychmiastowe użycie, to ważność wskaźnika zwracanego przez operator-> uważany jest ograniczona do takiej sytuacji, czy będzie bezpieczne konto wdrożenie do powyższej „nadużycie” ?

Oczywiście rozwiązanie 2 jest znacznie droższe, ponieważ wymaga przydziału sterty. Jest to oczywiście bardzo niepożądane, ponieważ dereferencja jest dość powszechnym zadaniem i nie ma takiej potrzeby: użycie unikatowej metody pozwala uniknąć takich problemów, ponieważ zwraca kopię przypisaną do stosu row_view.

+0

Jestem prawie pewny, że musisz zwrócić referencję dla 'operatora *' i wskaźnik dla 'operatora ->': https://stackoverflow.com/questions/37191290/iterator-overload-member-selection-vs -indirection-operator – NathanOliver

+0

Według [cppreference] (http://en.cppreference.com/w/cpp/language/operators): "Przeciążenie operatora -> musi albo zwrócić surowy wskaźnik, albo zwrócić obiekt (przez odniesienie lub według wartości), dla których operator -> jest z kolei przeciążony. " – Jonas

+0

Jeśli chodzi o 'operator *', nie znalazłem żadnych ograniczeń. Kompilator na pewno nie narzeka. – Jonas

Odpowiedz

3

Jak wiadomo, operator-> jest stosowany rekurencyjnie w typach zwracanych przez funkcję, aż napotkany zostanie wskaźnik raw. Jedynym wyjątkiem jest sytuacja, gdy jest wywoływana przez nazwę, tak jak w przykładowym kodzie.

Możesz użyć tego na swoją korzyść i zwrócić niestandardowy obiekt proxy. Aby uniknąć tego scenariusza w ostatnim fragmencie kodu, obiekt ten musi spełniać kilka wymagań:

  1. Jego nazwa typu powinny być prywatne do matrix<>::iterator, więc kod na zewnątrz nie może odnosić się do niego.

  2. Jego budowa/kopiowanie/przypisanie powinno być prywatne. matrix<>::iterator będzie miał do nich dostęp z racji bycia przyjacielem.

Implementacja będzie wyglądać mniej więcej tak:

template <...> 
class matrix<...>::iterator { 
private: 
    class row_proxy { 
    row_view *rv_; 
    friend class iterator; 
    row_proxy(row_view *rv) : rv_(rv) {} 
    row_proxy(row_proxy const&) = default; 
    row_proxy& operator=(row_proxy const&) = default; 
    public: 
    row_view* operator->() { return rv_; } 
    }; 
public: 
    row_proxy operator->() { 
    row_proxy ret(/*some row view*/); 
    return ret; 
    } 
}; 

Realizacja operator-> zwraca nazwie obiektu, aby uniknąć luki ze względu na gwarantowaną kopiowania elizji w C++ 17. Kod korzystający z operatora inline (it->mem) będzie działał jak poprzednio. Jednak każda próba wywołania operator->() według nazwy bez odrzucenia wartości zwracanej nie zostanie skompilowana.

Live Example

struct data { 
    int a; 
    int b; 
} stat; 

class iterator { 
    private: 
     class proxy { 
     data *d_; 
     friend class iterator; 
     proxy(data *d) : d_(d) {} 
     proxy(proxy const&) = default; 
     proxy& operator=(proxy const&) = default; 
     public: 
     data* operator->() { return d_; } 
     }; 
    public: 
     proxy operator->() { 
     proxy ret(&stat); 
     return ret; 
     } 
}; 


int main() 
{ 
    iterator i; 
    i->a = 3; 

    // All the following will not compile 
    // iterator::proxy p = i.operator->(); 
    // auto p = i.operator->(); 
    // auto p{i.operator->()}; 
} 

Po dalszego przeglądu mojego na zaproponowane rozwiązanie, zdałem sobie sprawę, że nie jest aż tak idiotoodporny jak myślałem. Nie można utworzyć obiekt klasy proxy poza zakresem iterator, ale wciąż można wiązać odniesienie do niej:

auto &&r = i.operator->(); 
auto *d = r.operator->(); 

umożliwiając zastosowanie operator->() ponownie.

Natychmiastowym rozwiązaniem jest zakwalifikowanie operatora obiektu proxy i ustawienie go tylko dla wartości r. Podobnie jak dla mojego żywego przykładu:

data* operator->() && { return d_; } 

Spowoduje to dwie linie nad emitować kolejny błąd, podczas gdy właściwe wykorzystanie iteracyjnej nadal działa. Niestety, wciąż nie chroni przed nadużyciami API, ze względu na dostępność odlewania, głównie:

auto &&r = i.operator->(); 
auto *d = std::move(r).operator->(); 

który jest śmiertelny cios całemu przedsięwzięciu. Nie można temu zapobiec.

Podsumowując, nie istnieje żadna ochrona przed wywoływaniem kierunku do operator-> na obiekcie iteratora. Co najwyżej możemy sprawić, że interfejs API będzie naprawdę trudny do użycia, a prawidłowe użycie będzie łatwe.

Jeśli tworzenie kopii row_view jest ekspansywne, może to być wystarczająco dobre. Ale to należy rozważyć.

Kolejnym punktem do rozważenia, którego nie dotknąłem w tej odpowiedzi, jest to, że proxy można wykorzystać do wykonania kopii przy zapisie.Ale ta klasa może być tak samo bezbronna jak proxy w mojej odpowiedzi, chyba że zostanie podjęta wielka ostrożność i zastosowany zostanie dość konserwatywny projekt.

+0

Po prostu mam to prawo: wywołanie 'operator->' bez odrzucenia wartości zwracanej spowodowałoby błąd kompilatora, ponieważ typ zwracany ('row_proxy') jest prywatny? – Jonas

+0

@ Jonas - Nie tylko. Uczynienie typu "prywatny" tylko zapobiega jednemu "atakowi". Ukrywanie konstruktora i operatora przypisania uniemożliwia przechwytywanie go z dedukcją typu 'auto p = ...'. – StoryTeller

+0

Dzięki za szczegóły i dodatkowe informacje. Przypuszczam, że jestem zadowolony z twojej odpowiedzi i prawdopodobnie wkrótce ją zaakceptuję. Nie do końca tego się spodziewałem. Praca z problemem i zapewnienie, że 'operator->' nie może być "złośliwie" (na różne stopnie sukcesu) nawet mi nie przyszło. Moje pierwotne pytanie zmierzało bardziej do tego, co byłoby uważane za idiomatyczne. – Jonas

Powiązane problemy