2017-06-26 3 views
7

Ich implementiere einen zweidimensionalen Array-Container (wie boost::multi_array<T,2>, hauptsächlich für die Praxis). Um die Double-Index-Notation (a[i][j]) zu verwenden, habe ich eine Proxy-Klasse eingeführt row_view (und , aber ich bin nicht besorgt über die Konsistenz hier), die einen Zeiger auf den Anfang und das Ende der Zeile hält.Gültigkeit des von operator zurückgegebenen Zeigers->

ich auch über Zeilen in der Lage sein wiederholen möchte und über die Elemente innerhalb einer Zeile getrennt:

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

nun die matrix<T>::iterator Klasse in eine private row_view rv; hält (was iterieren gemeint ist, über Zeilen) intern Verfolgen Sie die Zeile, auf die der Iterator zeigt. Natürlich setzt iterator auch dereferenciation Funktionen:

  • für operator*(), würde man in der Regel zurückgeben möchten einen Verweis. Stattdessen scheint hier das Richtige zu tun, um einen row_view Wert zurückzugeben (d. H. Eine Kopie des privaten row_view zurückzugeben). Dies stellt sicher, dass beim Weiterschalten des Iterators die row_view immer noch auf die vorherige Zeile zeigt. (In gewisser Weise, row_view wirkt wie eine Referenz würde).
  • für operator->(), bin ich mir nicht so sicher. Ich sehe zwei Möglichkeiten:

    1. einen Zeiger auf die privaten row_view des Iterators:

      row_view operator->() const { return &rv; } 
      
    2. einen Zeiger auf eine neue row_view (eine Kopie der privaten eins). Aufgrund der Speicherlebensdauer müsste dies auf dem Heap zugewiesen werden. Um clean-up zu gewährleisten, würde ich es wickeln in einem unique_ptr:

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

Offensichtlich 2 mehr korrekt ist. Wenn der Iterator nachoperator-> aufgerufen wird, wird die row_view, auf die in 1 gezeigt wird, geändert. der einzige Weg, ich denke, kann jedoch davon, wo diese Rolle würde, ist, wenn die operator-> durch seinen vollständigen Namen und der zurückgegebene Zeiger wurde gebunden hieß:

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) 

Dies ist jedoch nicht, wie Sie in der Regel operator-> verwenden würden, . In solch einem Anwendungsfall würden Sie wahrscheinlich operator* anrufen und einen Verweis auf die erste Zeile behalten. Normalerweise würde man sofort den Zeiger verwenden, um eine Mitgliedsfunktion von row_view aufzurufen oder auf ein Mitglied zuzugreifen, z. it->sum().

Meine Frage ist jetzt, ist dies: Da die -> Syntax sofortigen Gebrauch schon sagt, ist die Gültigkeit des von operator-> zurückgegebene Zeiger berücksichtigt auf diese Situation beschränkt sein, oder würde ein sicher Implementierung Konto für den obigen „Missbrauch“ ?

Offensichtlich ist Lösung 2 viel teurer, da sie Heap-Zuweisung erfordert. Dies ist natürlich sehr unerwünscht, da eine Dereferenzierung eine ziemlich häufige Aufgabe ist und es keinen wirklichen Bedarf gibt: die Verwendung von operator* vermeidet diese Probleme, da sie eine dem Stack zugeordnete Kopie der row_view zurückgibt.

+0

Ich bin ziemlich sicher, dass Sie eine Referenz für 'operator *' und einen Zeiger für 'operator ->' zurückgeben müssen: https://stackoverflow.com/questions/37191290/iterator-overload-member-selection-vs -indirection-operator – NathanOliver

+0

Gemäß [cppreference] (http://en.cppreference.com/w/cpp/language/operators): "Die Überladung von operator -> muss entweder einen rohen Zeiger zurückgeben oder ein Objekt zurückgeben (durch Referenz oder nach Wert), für den der Operator -> wiederum überladen ist. " – Jonas

+0

Was 'operator *' betrifft, habe ich keine Einschränkungen gefunden. Der Compiler beklagt sich sicher nicht. – Jonas

Antwort

3

Wie Sie wissen, wird operator-> rekursiv auf den Rückgabetyp der Funktionen angewendet, bis ein roher Zeiger gefunden wird. Die einzige Ausnahme ist, wenn es wie in Ihrem Codebeispiel namentlich aufgerufen wird.

Sie können das zu Ihrem Vorteil verwenden und ein benutzerdefiniertes Proxy-Objekt zurückgeben. Um das Szenario in Ihrem letzten Code-Schnipsel zu vermeiden, muss dieses Objekt mehrere Anforderungen erfüllen:

  1. Sein Typ Name sollte außerhalb Code an die matrix<>::iterator, so privat sein konnte nicht darauf verweisen.

  2. Die Konstruktion/Kopie/Zuordnung sollte privat sein. matrix<>::iterator haben Zugang zu denen, die ein Freund sind.

Eine Implementierung wird wie folgt aussehen etwas:

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; 
    } 
}; 

Die Implementierung von operator-> gibt ein benanntes Objekt etwaige Lücken zu vermeiden, durch garantierte Kopie elision in C++ 17. Code, der den Operator inline (it->mem) verwendet, funktioniert wie zuvor. Jeder Versuch, den Namen operator->() aufzurufen, ohne den Rückgabewert zu verwerfen, wird jedoch nicht kompiliert.

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->()}; 
} 

Bei der weiteren Überprüfung meiner vorgeschlagene Lösung, erkannte ich, dass es nicht ganz so narrensicher ist, wie ich dachte. Man kann nicht ein Objekt der Proxy-Klasse außerhalb des Anwendungsbereichs der iterator schaffen, aber man kann immer noch einen Verweis auf sie binden:

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

So ermöglicht operator->() wieder anzuwenden.

Die sofortige Lösung besteht darin, den Operator des Proxy-Objekts zu qualifizieren und nur für rvalues ​​anzuwenden. Wie so für meine anschauliches Beispiel:

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

Dadurch werden die beiden Zeilen oben verursachen wieder einen Fehler zu emittieren, während die ordnungsgemäße Verwendung des Iterators noch funktioniert. Leider schützen diese noch nicht die API von Missbrauch, aufgrund der Verfügbarkeit von Gießen, vor allem:

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

, die ein Todesstoß für das ganze Unterfangen ist. Das ist nicht zu verhindern.

Also abschließend gibt es keinen Schutz vor einem Richtungsanruf an auf dem Iterator-Objekt. Im besten Fall können wir die API nur schwer falsch verwenden, während die korrekte Verwendung einfach bleibt.

Wenn die Erstellung von row_view Kopien expansiv ist, kann dies gut genug sein. Aber das ist für Sie in Betracht zu ziehen.

Ein weiterer zu berücksichtigender Punkt, auf den ich in dieser Antwort nicht eingegangen bin, ist, dass der Proxy zum Implementieren von Kopieren beim Schreiben verwendet werden könnte.Aber diese Klasse könnte genauso verletzlich sein wie der Proxy in meiner Antwort, es sei denn, es wird mit großer Sorgfalt vorgegangen und es wird ein ziemlich konservatives Design verwendet.

+0

Nur damit ich das richtig verstehe: Der Aufruf von operator-> 'ohne den Rückgabewert zu verwerfen würde zu einem Compilerfehler führen Der Rückgabetyp ('row_proxy') ist privat? – Jonas

+0

@ Jonas - Nicht nur. Wenn der Rückgabetyp privat gemacht wird, wird nur ein "Angriff" verhindert. Das Ausblenden des Konstruktors und des Zuweisungsoperators verhindert die Erfassung mit dem Typabzug 'auto p = ...'. – StoryTeller

+0

Danke für die Detaillierung und die zusätzlichen Informationen. Ich nehme an, ich bin glücklich mit deiner Antwort und werde es wahrscheinlich bald annehmen. Es ist nicht ganz das, was ich erwartet habe. Das Problem zu umgehen und sicher zu stellen, dass "operator->' nicht "böswillig" (mit unterschiedlichem Erfolg) verwendet werden kann, kam mir gar nicht in den Sinn. Meine ursprüngliche Frage zielte mehr darauf ab, was als idiomatisch betrachtet werden würde. – Jonas

Verwandte Themen