Wie kann ich einen Wrapper vom Typ Const Propagating Pointer schreiben?

Oct 05 2020

Um das System vom Typ C ++ besser zu verstehen, habe ich mich bemüht, eine Zeiger-Wrapper-Klasse zu schreiben, die die Konstanz ähnlich wie std :: experiment :: propagate_const weitergibt:

template <typename Pointee> class Ptr {
public:
  Ptr() = delete;

  explicit Ptr(Pointee *);

  Ptr(const Ptr<Pointee> &) = delete;
  Ptr(Ptr<Pointee> &&);

  Ptr<Pointee> &operator=(const Ptr<Pointee> &) = delete;
  Ptr<Pointee> &operator=(Ptr<Pointee> &&);

  ~Ptr()  = default;

  const Pointee *operator->() const;
  Pointee *operator->();
  const Pointee &operator*() const;
  Pointee &operator*();

private:
  Pointee *mPtr;
};

Der Wrapper soll ein nahezu rohes zeigerähnliches Verhalten bieten und gleichzeitig eine Art "tiefe" Konstantenkorrektheit erzwingen und vor unbeabsichtigtem Aliasing schützen.

Zu diesem Zweck werden der Kopierkonstruktor und der Kopierzuweisungsoperator gelöscht:

  1. Um ein unbeabsichtigtes Aliasing des Objekts, auf das verwiesen wird, durch Kopieren von einem Ptr zu verhindern.
  2. So verhindern Sie den nicht konstanten Zugriff auf ein const-Objekt, auf das verwiesen wird, indem Sie ein const Ptr in ein nicht const-Ptr kopieren.

Das obige Design hat jedoch zwei unglückliche Konsequenzen.

  1. Ein const Ptr kann weder in ein const noch in ein non-const Ptr verschoben werden. Wenn das obligatorische RVO von C ++ 17 nicht gilt, bedeutet dies, dass ein const Ptr-Objekt nicht von einer Funktion zurückgegeben werden kann.
  2. Aufgrund der obligatorischen Kopier- / Verschiebungselision von C ++ 17 kann in bestimmten Situationen ein nicht konstanter Ptr aus einem konstanten Ptr konstruiert werden, obwohl kein solcher brauchbarer Konstruktor existiert. Zum Beispiel wird der folgende Code einwandfrei kompiliert (ignorieren Sie den Speicherverlust / Raw New zum Zwecke der Demonstration):
const Ptr<int> allocateImmutableInt(int val) { return Ptr<int>(new int(val)); }


void foo() {
  Ptr<int> immutableInt = allocateImmutableInt(0); // Initializes non-const Ptr from const Ptr 
  *immutableInt = 100;  // Oops, changed value of 'immutable' object
}

Das erste Problem kann teilweise gelöst werden, indem ein Verschiebungsfaktor eingeführt wird, der eine konstante Wertreferenz akzeptiert (obwohl sich dies etwas seltsam und nicht idiomatisch anfühlt):

  Ptr(const Ptr<Pointee> &&);

Dies verschlimmert jedoch tatsächlich das zweite Problem. Jetzt kann ein const Ptr auch ohne obligatorische Verschiebung / Kopierelision in ein nicht const Ptr konstruiert werden. Soweit ich das beurteilen kann, benötigen wir einen sogenannten 'const-Konstruktor', dh einen Konstruktor, der nur aufgerufen werden kann, um ein const-Objekt zu erzeugen:

  Ptr(const Ptr<Pointee>&&) const;

Selbst wenn c ++ einen solchen Konstruktor unterstützt, bleibt das zweite Problem bestehen, da c ++ 17 die Lebenslaufqualifizierung und die Lebensfähigkeit von Konstruktoren bei der Entscheidung, ob beim Initialisieren eines Objekts eine obligatorische Verschiebung / Kopierelision angewendet werden kann, ausdrücklich ignoriert. Derzeit scheint es keine Möglichkeit zu geben, c ++ zu bitten, zu prüfen, ob eine Kopie / Verschiebung möglich ist, bevor die obligatorische Kopier- / Verschiebungselision auf die Objektinitialisierung angewendet wird.

Soweit ich das beurteilen kann, leidet std :: experiment :: propagate_const unter denselben Problemen. Ich frage mich, ob ich auf eine grundlegende Einschränkung von c ++ gestoßen bin oder ob ich den Ptr-Wrapper falsch gestalte. Mir ist bewusst, dass diese Probleme wahrscheinlich behoben werden können, indem zwei Typen erstellt werden, ein Ptr für den Zugriff ohne Konstante und ein ConstPtr für den Zugriff nur mit Konstante. Dies macht jedoch den Zweck zunichte, in erster Linie einen const-propagierenden Wrapper zu erstellen.

Vielleicht bin ich gerade auf den Grund gestoßen, warum sowohl ein Iteratortyp als auch ein const_iterator-Typ existieren.

Antworten

1 JaMiT Oct 06 2020 at 04:35

Sie betrachten Probleme, die nicht wirklich existieren.

  1. So verhindern Sie den nicht konstanten Zugriff auf ein const-Objekt, auf das verwiesen wird, indem Sie ein const Ptr in ein nicht const-Ptr kopieren.

Dies sollte kein Ziel sein, da es gegen die Idee der Verbreitung verstößt const.

Die Verbreitung hat zwei Aspekte const. Erstens, wenn der Zeiger const-qualifiziert ist, wird das Objekt auch const-qualifiziert. Diesen Aspekt haben Sie behandelt. Zweitens verwendet das Objekt seine natürliche Qualifikation , wenn der Zeiger nicht const qualifiziert ist. Das heißt, wenn Sie ein const Ptrin ein Nicht- const PtrObjekt kopieren können , wird diese Änderung auf das Objekt übertragen, wodurch das Objekt möglicherweise auch Nicht-Objekt wird const. Dies ist eine gewünschte Ausbreitung, die nicht verhindert werden kann.

Beachten Sie einen wichtigen Anwendungsfall für die constWeitergabe: Klassenmitglieder. Das Propagieren constfür einen Elementzeiger hilft dabei, die Richtigkeit der Konstanten sicherzustellen, indem die Daten, auf die verwiesen wird, constin qualifizierten constElementfunktionen festgelegt werden. Ihre imaginären Probleme sind auf diesen Anwendungsfall nicht anwendbar. Machen Sie die Situation nicht komplexer als nötig.

Mir ist bewusst, dass diese Probleme wahrscheinlich behoben werden können, indem zwei Typen erstellt werden, ein Ptr für den Zugriff ohne Konstante und ein ConstPtr für den Zugriff nur mit Konstante.

Dies ist nicht erforderlich. Wenn das Objekt constauch dann erhalten bleiben soll, wenn der Zeiger nicht vorhanden ist const, sollte der Typ Ptr<const T>anstelle von sein Ptr<T>. Als Beispiel sollte Ihr "unveränderliches int" eher wie folgt aussehen.

Ptr<const int> allocateImmutableInt(int val) { return Ptr<const int>(new int(val)); }
    ^^^^^                                                 ^^^^^

Das constwurde verschoben, um das zu qualifizieren int, was das intUnveränderliche unabhängig von der constQualifikation des macht Ptr.

Außerdem stellen Sie möglicherweise fest, dass new int(val)ein int*Wert zurückgegeben wird, in den const int*Ihr Konstruktor implizit konvertiert wird . Möglicherweise möchten Sie diese implizite Konvertierung für replizieren Ptr. Ein Konstruktor wie Ptr(Ptr<std::remove_const<Pointee>> &&)würde den Trick machen, solange er nur definiert ist, wenn er qualifiziert Pointeeist const(um einen Konflikt mit dem regulären Verschiebungskonstruktor zu vermeiden).

1 JHBonarius Oct 06 2020 at 02:50
Ptr<int> const allocateImmutableInt(int val) {
    return Ptr<int>(new int(val));
}

Tut nicht das, was du denkst. Es wird kein const-qualifiziertes Objekt erstellt, das dann an den Konstruktor von übergeben wird immutableInt. -Das wäre nicht einmal möglich, wenn Sie den Kopierkonstruktor gelöscht hätten.- Stattdessen wird der Compiler daraus schließen

Ptr<int> immutableInt = allocateImmutableInt(0);

wie

Ptr<int> immutableInt(new int(val));

Wenn Sie Ihre Hauptfunktion schreiben als:

void foo() {
    Ptr<int> const immutableInt = allocateImmutableInt(0);
    Ptr<int> mutableInt = std::move(immutableInt);  // doesn't compile
    *mutableInt = 100;
}

Sie werden das richtige Verhalten sehen: Das moveist aufgrund von const nicht möglich und der Kopierkonstruktor wird gelöscht.

edit: Übrigens Ptrsieht deine Klasse sehr ähnlich aus std::unique_ptr. So könnte man einfach schreiben:

#include <memory>
void foo() {
    std::unique_ptr<int> const immutableInt = std::make_unique<int>(0);
    std::unique_ptr<int> mutableInt = std::move(immutableInt);  // doesn't compile
    *mutableInt = 100;
}

Und wenn Sie einen const-qualifizierten Zeiger möchten, schreiben Sie

std::unique_ptr<int const>