Windows mit C++

Visual C++ 2015 ergänzt Legacycode mit modernem C++

Kenny Kerr

Kenny KerrBei der Systemprogrammierung mit Windows spielen nicht transparente Handles eine wichtige Rolle, die versteckte Objekte hinter APIs im C-Format darstellen. Außer wenn Sie auf einer relativ hohen Ebene programmieren, ist es wahrscheinlich, dass Sie mit Handles verschiedener Arten zurechtkommen müssen. Das Handlekonzept ist in vielen Bibliotheken und auf vielen Plattformen vorhanden und sicherlich nicht nur auf das Windows-Betriebssystem beschränkt. 2011 schrieb ich erstmals über eine intelligente Handleklassenvorlage (msdn.microsoft.com/magazine/hh288076), als in Visual C++ einige anfängliche C++-11-Sprachfunktionen eingeführt wurden. In Visual C++ 2010 war es möglich, praktische und semantisch richtige Handlewrapper zu schreiben, aber ihre Unterstützung für C++ 11 war minimal, und es war weiterhin viel Zeit und Mühe erforderlich, um eine solche Klasse ordnungsgemäß zu schreiben. Mit der Einführung von Visual C++ 2015 in diesem Jahr möchte ich dieses Thema erneut aufgreifen und einige weitere Ideen zur Verwendung von modernem C++ zum Beleben einiger alter Bibliotheken im C-Format mit Ihnen teilen.

Die besten Bibliotheken ordnen keine Ressourcen zu und benötigen daher nur eine minimale Umschließung. Mein bevorzugtes Beispiel ist "Windows Slim Reader/Writer (SRW) Lock". Hier ist alles, was benötigt wird, um eine einsatzbereite SRW-Sperre erstellen und zu initialisieren:

SRWLOCK lock = {};

Die SRW-Sperrenstruktur enthält nur einen einzigen leeren "*"-Zeiger, und es gibt nichts zu bereinigen! Sie muss vor der Verwendung initialisiert werden, und die einzige Einschränkung besteht darin, dass sie nicht verschoben oder kopiert werden kann. Freilich hat jede Modernisierung mehr mit Ausnahmesicherheit zu tun, während die Sperre aktiviert ist, statt mit Ressourcenverwaltung. Modernes C++ kann dennoch sicherstellen, dass diese einfachen Anforderungen erfüllt werden. Erstens kann ich die Fähigkeit nutzen, nicht statische Datenmember dort zu initialisieren, wo sie zum Vorbereiten der SRW-Sperre für die Verwendung deklariert sind:

class Lock
{
  SRWLOCK m_lock = {};
};

Damit ist die Initialisierung sichergestellt, doch die Sperre kann weiterhin kopiert und verschoben werden. Dafür muss ich den standardmäßigen Kopierkonstruktor und Kopierzuweisungsoperator löschen:

class Lock
{
  SRWLOCK m_lock = {};
public:
  Lock(Lock const &) = delete;
  Lock & operator=(Lock const &) = delete;
};

Dies verhindert Kopier- und Verschiebevorgänge. Ihre Deklarierung im öffentlichen Teil der Klasse führt meist zu besseren Fehlermeldungen des Compilers. Freilich muss ich nun einen Standardkonstruktor bereitstellen, da keiner mehr angenommen wird:

class Lock
{
  SRWLOCK m_lock = {};
public:
  Lock() noexcept = default;
  Lock(Lock const &) = delete;
  Lock & operator=(Lock const &) = delete;
};

Trotz der Tatsache, dass ich keinen Code geschrieben haben, generiert der Compiler per se alles für mich, und ich kann jetzt eine Sperre ganz einfach erstellen:

Lock lock;

Der Compiler wird alle Versuche des Kopierens oder Verschiebens der Sperre unterbinden:

Lock lock2 = lock; // Error: no copy!
Lock lock3 = std::move(lock); // Error: no move!

Ich kann dann einfach Methoden zum Aktivieren und Freigeben der Sperre auf verschiedene Weisen hinzufügen. Die SRW-Sperre bietet, wie der Name schon sagt, gemeinsam genutzte Lese- und exklusive Schreibsperrsemantik. Abbildung 1 zeigt einen minimalen Satz von Methoden für einfache exklusive Sperren.

Abbildung 1: Einfache und effiziente SRW-Sperre

class Lock
{
  SRWLOCK m_lock = {};
public:
  Lock() noexcept = default;
  Lock(Lock const &) = delete;
  Lock & operator=(Lock const &) = delete;
  void Enter() noexcept
  {
    AcquireSRWLockExclusive(&m_lock);
  }
  void Exit() noexcept
  {
    ReleaseSRWLockExclusive(&m_lock);
  }
};

In "Die Entwicklung der Synchronisierung in Windows und C++" (msdn.microsoft.com/magazine/jj721588) finden Sie weitere Informationen zum Hindergrund dieser unglaublich einfachen Sperrmethode. Alles, was noch bleibt, ist das Bereitstellen von ein wenig Ausnahmesicherheit für den Besitz der Sperre. Dafür würde ich etwas wie das Folgende schreiben:

lock.Enter();
// Protected code
lock.Exit();

Stattdessen möchte ich, dass ein Sperrenwächter sich um das Aktivieren und Freigeben der Sperre für einen bestimmten Bereich kümmert:

Lock lock;
{
  LockGuard guard(lock);
  // Protected code
}

Dieser Sperrwächter kann einfach auf einen Verweis auf die zugrunde liegende Sperre zurückgreifen:

class LockGuard
{
  Lock & m_lock;
};

Wie bei der "Lock"-Klasse selbst empfiehlt es sich, dass die "Guard"-Klasse keine Kopier- oder Verschiebevorgänge zulässt:

class LockGuard
{
  Lock & m_lock;
public:
  LockGuard(LockGuard const &) = delete;
  LockGuard & operator=(LockGuard const &) = delete;
};

Ein Konstruktor muss dann nur noch die Sperre aktivieren, und der Desktruktor muss sie deaktivieren. Abbildung 2 fasst dieses Beispiel zusammen.

Abbildung 2: Einfacher Sperrenwächter

class LockGuard
{
  Lock & m_lock;
public:
  LockGuard(LockGuard const &) = delete;
  LockGuard & operator=(LockGuard const &) = delete;
  explicit LockGuard(Lock & lock) noexcept :
    m_lock(lock)
  {
    m_lock.Enter();
  }
  ~LockGuard() noexcept
  {
    m_lock.Exit();
  }
};

Fairerweise sollte erwähnt werden, dass die Windows SRW-Sperre ein echtes kleines Juwel ist, und die meisten Bibliotheken benötigen ein wenig Speicher oder eine Art von Ressource, die explizit verwaltet werden muss. In "Intelligente COM-Zeiger die Zweite" (msdn.microsoft.com/magazine/dn904668) habe ich bereits gezeigt, wie COM-Schnittstellenzeiger am besten verwaltet werden. Nun möchte mich näher mit nicht transparenten Handles beschäftigen. Wie ich zuvor geschrieben habe, muss eine Handleklassenvorlage eine Möglichkeit bieten, nicht nur den Typ des Handles zu parametrisieren, sondern auch die Weise, in der der Handle geschlossen wird, und sogar, was genau einen ungültigen Handle darstellt. Nicht alle Bibliotheken verwenden einen NULL-Wert oder 0 zum Darstellen ungültiger Handles. Meine ursprüngliche Handleklassenvorlage setzte voraus, dass der Aufrufer eine "HandleTraits"-Klasse bereitstellt, die die erforderlichen Semantik- und Typinformationen bietet. Nach dem Schreiben überaus vieler "Traits"-Klassen im Lauf der Jahre stellte ich fest, dass die große Mehrheit dieser Klassen einem ähnlichen Muster folgt. Und wie jeder C++-Entwickler Ihnen erklären wird, sind Muster am besten mit Vorlagen zu beschreiben. Neben einer Handleklassenvorlage nutze ich jetzt auch eine "HandleTraits"-Klassenvorlage. Die "HandleTraits"-Klassenvorlage ist nicht erforderlich, vereinfacht jedoch die meisten Definitionen. Hier ihre Definition:

template <typename T>
struct HandleTraits
{
  using Type = T;
  static Type Invalid() noexcept
  {
    return nullptr;
  }
  // Static void Close(Type value) noexcept;
};

Beachten Sie, was die "HandleTraits"-Klassenvorlage bietet und was sie insbesondere nicht bietet. Ich habe so viele ungültige Methoden geschrieben, die "nullptr"-Werte zurückgegeben haben, dass dies mir wie eine offensichtliche Fahrlässigkeit vorkommt. Auf der anderen Seite muss jede konkrete "Traits"-Klasse aus offensichtlichen Gründen ihre eigene "Close"-Methode bereitstellen. Der Kommentar wird lediglich als zu befolgendes Muster beibehalten. Der Typalias ist ebenfalls optional und lediglich eine bequeme Möglichkeit zum Definieren meiner eigenen "Traits"-Klassen anhand dieser Vorlage. Daher kann ich, wie hier gezeigt, eine "Traits"-Klasse für Dateihandles definieren, die von der Windows-Funktion "CreateFile" zurückgegeben werden:

struct FileTraits
{
  static HANDLE Invalid() noexcept
  {
    return INVALID_HANDLE_VALUE;
  }
  static void Close(HANDLE value) noexcept
  {
    VERIFY(CloseHandle(value));
  }
};

Die "CreateFile"-Funktion gibt den Wert INVALID_HANDLE_VALUE zurück, wenn die Funktion keinen Erfolg hat. Andernfalls muss der resultierende Handle mithilfe der "CloseHandle"-Funktion geschlossen werden. Dies ist zugegebenermaßen ungewöhnlich. Die Windows-Funktion "CreateThreadpoolWork" gibt einen PTP_WORK-Handle zurück, der das Arbeitsobjekt darstellt. Dies ist lediglich ein nicht transparenter Zeiger, und ein "nullptr"-Wert wird üblicherweise bei einem Fehler zurückgegeben. Daher kann eine "Traits"-Klasse für Arbeitsobjekte die "HandleTraits"-Klassenvorlage nutzen, wodurch ich etwas weniger tippen muss:

struct ThreadPoolWorkTraits : HandleTraits<PTP_WORK>
{
  static void Close(Type value) noexcept
  {
    CloseThreadpoolWork(value);
  }
};

Wie sieht die tatsächliche Handleklassenvorlage also aus? Sie kann einfach auf der angegebenen "Traits"-Klasse basieren, den Typ des Handles ableiten und je nach Bedarf die "Close"-Methode aufrufen. Die Ableitung hat die Form eines "decltype"-Ausdrucks, um den Typ des Handles zu bestimmen:

template <typename Traits>
class Handle
{
  using Type = decltype(Traits::Invalid());
  Type m_value;
};

Dadurch muss der Autor der "Traits"-Klasse keinen Typalias bzw. keine Typdefinition hinzufügen, um den Typ explizit und redundant anzugeben. Das Schließen des Handles muss erfolgen, und eine sichere "Close"-Hilfsmethode wird in den privaten Teil der Handleklassenvorlage eingebunden:

void Close() noexcept
{
  if (*this) // operator bool
  {
    Traits::Close(m_value);
  }
}

Diese "Close"-Methode verwendet einen expliziten booleschen Operator, um zu bestimmen, ob der Handle geschlossen werden muss, bevor die "Traits"-Klasse zum tatsächlichen Ausführen des Vorgangs aufgerufen wird. Der öffentliche explizite boolesche Operator ist eine weitere Verbesserung im Vergleich zu meiner Handleklassenvorlage aus 2011, da er einfach als expliziter Konvertierungsoperator implementiert werden kann:

explicit operator bool() const noexcept
{
  return m_value != Traits::Invalid();
}

Dies löst alle Arten von Problemen und ist sicherlich viel einfacher als herkömmliche Ansätze zu definieren, die einen einem booleschen Operator ähnlichen Operator implementieren und zugleich die gefürchteten impliziten Konvertierungen vermeiden, die der Compiler andernfalls zulassen würde. Eine weitere Sprachverbesserung, die ich in diesem Artikel bereits genutzt habe, ist die Möglichkeit, spezielle Member explizit zu löschen. Dies tue ich jetzt für den Kopierkonstruktor und Kopierzuweisungsoperator:

Handle(Handle const &) = delete;
Handle & operator=(Handle const &) = delete;

Ein Standardkonstruktor kann die "Traits"-Klasse nutzen, um den Handle in einer vorhersagbaren Weise zu initialisieren:

explicit Handle(Type value = Traits::Invalid()) noexcept :
  m_value(value)
{}

Und der Destruktor kann einfach die "Close"-Hilfsmethode verwenden:

~Handle() noexcept
{
  Close();
}

Kopien sind also nicht zulässig, doch abgesehen von der SRW-Sperre kann ich mir keine Handleressource vorstellen, die das Verschieben ihres Handles im Arbeitsspeicher nicht zulässt. Die Möglichkeit des Verschiebens von Handles ist äußerst praktisch. Das Verschieben von Handles umfasst zwei einzelne Vorgänge, die ich als Trennen und Anfügen oder auch Trennen und Zurücksetzen bezeichnen könnte. Das Trennen umfasst das Freigegeben des Besitzes des Handles für den Aufrufer:

Type Detach() noexcept
{
  Type value = m_value;
  m_value = Traits::Invalid();
  return value;
}

Der Wert des Handles wird an den Aufrufer zurückgegeben. Die Kopie des Handleobjekts wird ungültig gemacht, um sicherzustellen, dass sein Destruktor nicht die "Close"-Methode der "Traits"-Klasse aufruft. Der komplementäre Anfüge- oder Zurücksetzungsvorgang umfasst das Schließen aller vorhandenen Handles und anschließende Übernehmen des Besitzes eines neuen Handlewerts:

bool Reset(Type value = Traits::Invalid()) noexcept
{
  Close();
  m_value = value;
  return static_cast<bool>(*this);
}

Die "Reset"-Methode wird standardmäßig auf den ungültigen Wert des Handles festgelegt und dient als einfache Möglichkeit, einen Handle vorzeitig zu schließen. Sie gibt auch aus Gründen der Benutzerfreundlichkeit das Ergebnis des expliziten booleschen Operators zurück. Das folgende Muster habe ich selbst recht häufig geschrieben:

work.Reset(CreateThreadpoolWork( ... ));
if (work)
{
  // Work object created successfully
}

Hier verwende ich den expliziten booleschen Operator zur nachträglichen Überprüfung der Gültigkeit des Handles. Die Möglichkeit, dies in einem einzelnen Ausdruck zu verdichten, kann praktisch sein:

if (work.Reset(CreateThreadpoolWork( ... )))
{
  // Work object created successfully
}

Nachdem ich diesen Handshake eingerichtet haben, kann ich die Verschiebungsvorgänge recht einfach implementieren, wobei ich mit dem "Move"-Konstruktor beginne:

Handle(Handle && other) noexcept :
  m_value(other.Detach())
{}

Die "Detach"-Methode wird für den "rvalue"-Verweis aufgerufen, und der neu erstellte Handle übernimmt effektiv den Besitz vom anderen "Handle"-Objekt. Der Verschiebungszuweisungsoperator ist nur geringfügig komplizierter:

Handle & operator=(Handle && other) noexcept
{
  if (this != &other)
  {
    Reset(other.Detach());
  }
  return *this;
}

Zunächst erfolgt eine Identitätsprüfung, um das Anfügen eines geschlossenen Handles zu vermeiden. Die zugrunde liegende "Reset"-Methode führt diese Art der Überprüfung nicht aus, da dies für jede Verschiebungszuweisung zwei zusätzliche Verzweigungen einschließen würde. Eine ist angemessen. Zwei sind eine zuviel. Verschiebesemantik ist eine tolle Sache, doch noch besser ist Tauschsemantik, insbesondere wenn Sie Handles in Standardcontainern speichern:

void Swap(Handle<Traits> & other) noexcept
{
  Type temp = m_value;
  m_value = other.m_value;
  other.m_value = temp;
}

Aus Gründen der Generizität ist selbstredend eine Nicht-Member-Tauschfunktion (Kleinbuchstaben) erforderlich:

template <typename Traits>
void swap(Handle<Traits> & left, Handle<Traits> & right) noexcept
{
  left.Swap(right);
}

Der letzte Schliff der Handleklassenvorlage kommt in Form eines Paares von "Get"- und "Set"-Methoden. "Get" ist offensichtlich:

Type Get() const noexcept
{
  return m_value;
}

Diese Methode gibt einfach den zugrunde liegenden Wert des Handles zurück, der für die Übergabe an verschiedene Bibliotheksfunktionen notwendig sein kann. "Set" ist vielleicht nicht ganz so offensichtlich:

Type * Set() noexcept
{
  ASSERT(!*this);
  return &m_value;
}

Dies ist ein indirekter "Set"-Vorgang. Die Assertion unterstreicht dies. Ich habe in der Vergangenheit diese "GetAddressOf"-Methode aufgerufen, doch dieser Name verschleiert den bzwl widerspricht dem tatsächlichen Zweck. Ein solcher indirekter "Set"-Vorgang ist in Fällen erforderlich, in denen die Bibliothek einen Handle als "Out"-Parameter zurückgibt. Die "WindowsCreateString"-Funktion ist nur ein Beispiel von vielen:

HSTRING string = nullptr;
HRESULT hr = WindowsCreateString( ... , &string);

Ich könnte "WindowsCreateString" auf diese Weise aufrufen und dann den resultierenden Handle an ein "Handle"-Objekt anfügen. Oder ich kann einfach die "Set"-Methode verwenden, um den Besitz direkt zu übernehmen:

Handle<StringTraits> string;
HRESULT hr = WindowsCreateString( ... , string.Set());

Dies ist sehr viel zuverlässiger und gibt deutlich die Richtung an, in die die Daten fließen. Die Handleklassenvorlage bietet außerdem die üblichen Vergleichsoperatoren, aber dank der Unterstützung der Sprache für explizite Konvertierungsoperatoren sind diese nicht mehr notwendig, um die implizite Konvertierung zu vermeiden. Sie sind einfach praktisch, aber ich überlasse es Ihnen, dies auszuprobieren. Die Handleklassenvorlage ist lediglich ein weiteres Beispiel aus "Modernes C++ für die Windows-Runtime" (moderncpp.com).


Kenny Kerr ist Programmierer aus Kanada sowie Autor bei Pluralsight und Microsoft MVP. Er veröffentlicht Blogs unter kennykerr.ca, und Sie können ihm auf Twitter unter twitter.com/kennykerr folgen.