Handle-Kapselung: Methoden const?
-
Hi!
Mich interessiert, welchen Stil ihr besser findet.
Angenommen, man möchte ein WinAPI void* handle oder ein unsigned int socket kapseln. Sollten dann so Funktionen wie Handle::Close() { CloseHandle(handle) } oder Socket::Shutdown() { shutdown(socket, SD_BOTH); } const sein, also für konstante Instanzen aufgerufen werden können? Immerhin wird ja nicht die Klasse selbst verändert.
-
non-const.
Constness bedeutet nicht Byteidentität der direkten Member der Klasse, sondern Identität bezüglich des logischen Inhalts*. Und zum logischen Inhalt dieser Klasse gehört der handle/socket - und somit auch dessen Zustand - eindeutig dazu. Dafür ist der Wrapper schließlich da!
Sonst könnte man auch argumentieren, dass Zugriffsfunktionen von Containerklassen const sein sollten, da über sie Objekte verändert werden, die im Container gespeichert sind, aber nicht der Container selber. Ich denke, diese Argumentation würdest du auch als absurd ansehen, oder?
*: Man kann es auch so formulieren: Wann ist Operation A const? Wenn sämtliche anderen Operationen, die auf dem Objekt möglich sind, vor und nach dem Ausführen von Operation A in jedem(!) Fall das gleiche Ergebnis haben, dann ist A eine konstante Methode. Das wäre hier nicht der Fall, da vor dem Shutdown eine typische Zugriffsoperation erfolgreich wäre, nach dem Shutdown aber scheitern muss. Daher kann der Shutdown nicht const sein.
-
@loboo
Eine Methode wieHandle::Close() { CloseHandle(handle) }
sollte es IMO gar nicht geben.
Weil sie das "Smart Handle" in einem ungültigen Zustand zurücklässt (handle != INVALID_HANDLE_VALUE, aber schon geschlossen).Ich mach das immer mit Reset Methoden ala
void Handle::Reset() { HANDLE old = handle; handle = INVALID_HANDLE_VALUE; if (old != INVALID_HANDLE_VALUE) ::CloseHandle(old); }
Die ist dann natürlich nicht const, da das Objekt dabei ja selbst verändert wird.
Ansonsten sind Handles konzeptuell Zeiger, und so behandle ich sie auch: eine
WriteFile
Methode einerFileHandle
Klasse istconst
.
Genau so wie ich mit einemFILE* const
die Funktionfwrite
aufrufen kann, oder mit einemstd::shared_ptr<std::fstream> const
die Funktionwrite
.Wobei ich solche Funktionen genaugenommen gar nicht implementiere. Nicht in der
SmartHandle
Klasse, dafür ist die nicht zuständig. Die Aufgabe vonSmartHandle
ist für mich RAII - nicht das Kapseln der ganzen API Calls.Das Kapseln der API Calls macht dann eine andere Klasse, sagen wir
File
, die einSmartHandle
als Member hat.
Und in derFile
Klasse trifft dann SeppJ's Argumentation für mich zu. In derSmartHandle
Klasse dagegen nicht.
-
Stimmt, danke!
-
Aha, hmm
-
hustbaer schrieb:
Das Kapseln der API Calls macht dann eine andere Klasse, sagen wir
File
, die einSmartHandle
als Member hat.
Und in derFile
Klasse trifft dann SeppJ's Argumentation für mich zu. In derSmartHandle
Klasse dagegen nicht.Ja, hier hätte man klären müssen, was die Klasse überhaupt genau macht. Wenn sie wirklich nur den Zeiger hält, dann passt deine Erklärung. Ich bin nach dem, was ich gesehen habe, davon ausgegangen, dass das Ding die ganze API reimplementiert. Denn nur dann macht ein Close Sinn, ansonsten bräuchte man ein Reset, wie du es beschreibst.
-
Nur mal so nebenbei, warum nicht
void Handle::Close() { if(handle != INVALID_HANDLE_VALUE) { CloseHandle(handle); handle = INVALID_HANDLE_VALUE; } }
? Wozu old? Und Close oder Reset? Meint ihr, nur wegen der Bedeutung des Namens?
-
Reset, weil man üblicherweise noch nen Overload macht der nen HANDLE Parameter nimmt. Der gibt dann das alte Handle frei und setzt das neue rein. (Bzw. man kann natürlich auch nen Default-Wert für den Parameter machen.)
Und dann ist Reset ("re set") eben passender als CloseBzw. auch einfach: weil das bei std::shared_ptr und std::unique_ptr auch so heisst
Und deine Variante ist in diesem Fall auch OK, da die WinAPI CloseHandle Funktion keine Exception werfen kann. Im allgemeinen Fall ist meine IMO besser, da dann auch im Fall wenn die "Close" Funktion eine Exception wirft (was sie nicht tun sollte, aber manche tun es halt trotzdem), das Objekt danach "leer" ist. Und damit ein zweiter (dritter, vierter, ...) Aufruf von "Close" auf das selbe Handle verhindert wird.
Wie gesagt in diesem Fall unnötig, aber wenn man es sich mal angewöhnt hat...
Wobei... die noch "schönere" Variante wäre vermutlich copy & swap zu verwenden.
Also
class Handle : private noncopyable { public: explicit Handle(HANDLE handle = INVALID_HANDLE_VALUE) : m_handle(handle) { } ~Handle() { if (IsValid()) CloseHandle(m_handle); } HANDLE Get() const { return m_handle; } bool IsValid() const { return m_handle != INVALID_HANDLE_VALUE; } void Reset(HANDLE handle = INVALID_HANDLE_VALUE) { assert(handle != m_handle || handle == INVALID_HANDLE_VALUE); Handle(handle).Swap(*this); } void Swap(Handle& other) { std::swap(m_handle, other.m_handle); } };
Noch besser natürlich wenn man noch Move-Semantik einbaut, aber ich bin grad zu faul das runterzutippen
ps: Sinnvollerweise implementiert man das auch nur 1x, und packt die "Close" und "IsValid" Funktionen, sowie den INVALID_HANDLE_VALUE Wert in eine Traits-Klasse. Und HANDLE wird zum Template-Parameter.
Dann muss man bloss noch zwei triviale Funktionen implementieren und Typ und "Invalid" Wert angeben, und hat ne passende HANDLE Klasse.
Also CloseHandle/h != INVALID_HANDLE_VALUE && h != 0/HANDLE/INVALID_HANDLE_VALUE für KERNEL32 Handles, DeleteObject/h != 0/HGDIOBJ/0 für GDI Handles usw.EDIT: Fehler korrigiert (siehe Beitrag von TyRoXx)
-
hustbaer schrieb:
void Reset(HANDLE handle = INVALID_HANDLE_VALUE) { if (handle != m_handle) Handle(handle).Swap(*this); }
Gibt es einen guten Grund dafür, dass da nicht
assert(handle != m_handle)
steht?
-
Nein, gibt es nicht.
Es macht natürlich keinen Sinn das zu erlauben.
Die implizite Precondition für Reset muss ja sein dass der Aufrufer "ownership" des Handles hat. Denn diese wird ja mit Reset transferiert.
Wenn aber handle == m_handle, dann hat ja *this schon "ownership", und daher kann der Aufrufer auch nicht "ownership" haben.Ich hab' den Code schnell für's Forum runtergetippt, und mir dann auf einmal eingebildet da müsste ein if hin - "self assignment" und so. Nur dass wir hier ja gar keine Assignment-Semantics haben. *patsch*
Danke für den Hinweis! Hab's korrigiert.
ps: es muss natürlich
assert(handle != m_handle || handle == INVALID_HANDLE_VALUE);
heissen