April 2016

Band 31, Nummer 4

Visual C++ – Mit Microsoft hat C++ eine Zukunft

Von Kenny Kerr | April 2016

Visual C++ eilt der Ruf voraus, nicht auf der Höhe der Zeit zu sein. Wenn Sie die aktuellen und besten C++-Features benötigen, sollten Sie einfach Clang oder GCC verwenden, wird häufig behauptet. Meiner Meinung nach hat sich dieser Status quo geändert – sozusagen eine Störung in der Matrix, eine Erschütterung in der Macht. Es stimmt, dass der Visual C++-Compiler eine unglaublich alte Codebasis besitzt, die es dem C++-Team bei Microsoft sehr erschwert hat, neue Features schnell hinzuzufügen (goo.gl/PjSC7v). Dies beginnt sich jedoch zu ändern, weil Visual C++ den Ausgangspunkt für viele neue Vorschläge für die C++-Sprache und die -Standardbibliothek darstellt. Ich werde einige neue oder optimierte Features in Visual C++ Update 2 vorstellen, die ich besonders überzeugend finde und die zeigen, dass noch Leben in diesem in die Jahre gekommenen Compiler steckt.

Module

Einige Entwickler bei Microsoft, insbesondere Gabriel Dos Reis und Jonathan Caves, arbeiten an einem Entwurf zum direkten Hinzufügen von Komponentenunterstützung zur C++-Sprache. Ein zweites Ziel besteht in der Verbesserung des Builddurchsatzes, ähnlich einem vorkompilierten Header. Dieser Entwurf, der als Modulsystem für C++ bezeichnet wird, wurde für C++ 17 vorgeschlagen. Der neue Visual C++-Compiler weist die Machbarkeit nach und markiert den Beginn einer funktionierenden Implementierung für Module in C++. Module werden so entworfen, dass sie direkt und auf natürliche Weise zum Erstellen und Nutzen von jedem Entwickler mithilfe von Standard-C++ verwendet werden können. Stellen Sie sicher, dass Visual C++ Update 2 installiert ist, öffnen Sie eine Entwicklereingabeaufforderung, und befolgen Sie dann einfach meine Anleitungen. Da das Feature noch recht experimentell ist, fehlt ihm jede IDE-Unterstützung. Daher ist es bei den ersten Schritten empfehlenswert, den Compiler direkt über die Eingabeaufforderung zu verwenden.

Angenommen, ich verfüge über eine vorhandene C++-Bibliothek, die ich als Modul verteilen möchte. Vielleicht eine anspruchsvolle Bibliothek wie in diesem Beispiel:

C:\modules> type animals.h
#pragma once
#include <stdio.h>
inline void dog()
{
  printf("woof\n");
}
inline void cat()
{
  printf("meow\n");
}

Außerdem verfüge ich über eine überzeugende Beispiel-App, die meine domestizierte Bibliothek begleitet:

C:\modules> type app.cpp
#include "animals.h"
int main()
{
  dog();
  cat();
}

Druck von C++-Aktivisten hat bewirkt, dass ich mich für die Verwendung von „printf“ schäme. Ich kann die unvergleichliche Leistung dieses Befehls jedoch nicht verleugnen. Daher entscheide ich mich dafür, die Bibliothek in ein Modul zu verwandeln, um die Wahrheit zu verschleiern, dass ich „printf“ anderen Formen der E/A vorziehe. Ich kann beginnen, indem ich die Modulschnittstelle schreibe:

C:\modules> type animals.ixx
module animals;
#include "animals.h"

Ich könnte natürlich einfach die cat- und dog-Funktionen direkt in der Modulschnittstellendatei definieren. Sie einzuschließen funktioniert jedoch genau so gut. Die Moduldeklaration informiert den Compiler, dass der folgende Code Teil des Moduls ist. Dies bedeutet jedoch nicht, dass nachfolgende Deklarationen als Teil der Schnittstelle des Moduls exportiert werden. Bis jetzt exportiert dieses Modul nur dann etwas, wenn der stdio.h-Header, den „animals.h“ einschließt, eigenständig etwas exportiert. Davor kann ich mich sogar schützen, indem „stdio.h“ vor der Moduldeklaration eingeschlossen wird. Wenn also diese Modulschnittstelle keine öffentlichen Namen deklariert, wie kann ich dann etwas exportieren, das von anderen Elementen verbraucht wird? Ich muss das Schlüsselwort „export“ verwenden. Dieses – und die Schlüsselwörter „module“ und „import“ – sind die einzigen Ergänzungen der C++-Sprache, die ich berücksichtigen muss. Dies spricht für die schöne Einfachheit dieses neuen Sprachfeatures.

Im ersten Schritt kann ich die cat- und dog-Funktionen exportieren. Dies beinhaltet das Aktualisieren des animals.h-Headers und das Einleiten beider Deklarationen mit dem export-Spezifizierer wie im folgenden Beispiel gezeigt:

C:\modules> type animals.h
#pragma once
#include <stdio.h>
export inline void dog()
{
  printf("woof\n");
}
export inline void cat()
{
  printf("meow\n");
}

Jetzt kann ich die Modulschnittstellendatei mithilfe der experimentellen Option „module“ des Compilers kompilieren:

C:\modules> cl /c /experimental:module animals.ixx

Beachten Sie, dass ich auch die Option „/c“ eingeschlossen habe, um den Compiler anzuweisen, den Code nur zu kompilieren und nicht zu linken. In dieser Phase macht es keinen Sinn, dass der Linker versucht, eine ausführbare Datei zu erstellen. Die Option „module“ weist den Compiler an, eine Datei zu generieren, die Metadaten mit einer Beschreibung der Schnittstelle und die Implementierung des Moduls in einem Binärformat enthält. Diese Metadaten sind kein Computercode, sondern eine binäre Darstellung für C++-Sprachkonstrukte. Es handelt sich jedoch auch nicht um Quellcode. Dies kann je nach Sichtweise gut oder schlecht sein. Gut daran ist, dass der Builddurchsatz verbessert werden sollte, weil Apps, die das Modul ggf. importieren, den Code nicht erneut analysieren müssen. Andererseits bedeutet dies jedoch auch, dass nicht notwendigerweise Quellcode für traditionelle Tools wie Visual Studio und das IntelliSense-Modul zum Visualisieren und Analysieren vorhanden ist. Dies führt dazu, dass Visual Studio und andere Tools angewiesen werden müssen, wie Code in einem Modul untersucht und visualisiert wird. Die gute Nachricht: Der Code oder die Metadaten in einem Modul werden in einem offenen Format gespeichert, und Tools können aktualisiert werden, um dieses zu verarbeiten.

Im nächsten Schritt kann die App nun das Modul importieren, anstatt den Bibliotheksheader direkt zu importieren:

C:\modules> type app.cpp
import animals;
int main()
{
  dog();
  cat();
}

Die import-Deklaration weist den Compiler an, nach einer passenden Modulschnittstellendatei zu suchen. Er kann diese Datei dann zusammen mit anderen Includedateien, die ggf. in der App vorhanden sind, zum Auflösen der dog- und cat-Funktionen verwenden. Dankenswerterweise exportiert das animals-Modul einige „pelzige“ Funktionen, und die App kann mithilfe der gleichen Modulbefehlszeilenoption erneut kompiliert werden:

C:\modules> cl /experimental:module app.cpp animals.obj

Beachten Sie, dass ich dieses Mal zulasse, dass der Compiler den Linker aufruft, weil ich nun wirklich eine ausführbare Datei generieren möchte. Die experimentelle Option „module“ ist noch immer erforderlich, weil das Schlüsselwort „import“ noch nicht offiziell ist. Außerdem erfordert der Linker, dass die Objektdatei beim Kompilieren des Moduls generiert wird. Dies deutet erneut auf die Tatsache hin, dass das neue Binärformat, das die Metadaten des Moduls enthält, nicht der eigentliche „Code“ ist, sondern nur eine Beschreibung der exportierten Deklarationen, Funktionen, Klassen, Vorlagen usw. Wenn Sie die App tatsächlich erstellen möchten, die das Modul verwendet, benötigen Sie weiterhin die Objektdatei, damit der Linker die Assemblierung des Codes in eine ausführbare Datei erledigen kann. Wenn alles geklappt hat, verfüge ich nun über eine ausführbare Datei, die ich wie jede andere ausführbare Datei ausführen kann – das Endergebnis unterscheidet sich nicht von der ursprünglichen App, die die Nur-Header-Bibliothek verwendet hat. Anders gesagt: Ein Modul ist keine DLL.

Nun möchte ich an einer recht großen Bibliothek arbeiten. Der Gedanke, „export“ jeder Deklaration hinzufügen zu müssen, gefällt mir überhaupt nicht. Glücklicherweise kann die export-Deklaration mehr als nur Funktionen exportieren. Eine Option besteht im Exportieren mehrerer Deklarationen mit einem Klammerpaar. Das folgende Beispiel zeigt dies:

C:\modules> type animals.h
#pragma once
#include <stdio.h>
export
{
  inline void dog()
  {
    printf("woof\n");
  }
  inline void cat()
  {
    printf("meow\n");
  }
}

Durch diesen Vorgang entsteht kein neuer Bereich. Er wird nur verwendet, um enthaltene Deklarationen für den Export zu gruppieren. Natürlich würde kein C++-Programmierer mit Selbstachtung eine Bibliothek mit Deklarationen im globalen Bereich schreiben. Wahrscheinlich würde mein animals.h-Header eher die dog- und cat-Funktionen in einem Namespace deklarieren. Der Namespace als Ganzes kann recht einfach exportiert werden:

C:\modules> type animals.h
#pragma once
#include <stdio.h>
export namespace animals
{
  inline void dog()
  {
    printf("woof\n");
  }
  inline void cat()
  {
    printf("meow\n");
  }
}

Ein weiterer spitzfindiger Vorteil des Umstiegs von einer Nur-Header-Bibliothek auf ein Modul besteht darin, dass die App nicht mehr zufällig eine Abhängigkeit für „stdio.h“ annehmen kann, weil dieses Element nicht Bestandteil der Schnittstelle des Moduls ist. Was geschieht, wenn meine Nur-Header-Bibliothek einen geschachtelten Namespace mit Implementierungsdetails enthält, die nicht für die direkte Verwendung durch Apps gedacht sind? Abbildung 1 zeigt ein typisches Beispiel für eine solche Bibliothek.

Abbildung 1: Nur-Header-Bibliothek mit einem Implementierungsnamespace

C:\modules> type animals.h
#pragma once
#include <stdio.h>
namespace animals
{
  namespace impl
  {
    inline void print(char const * const message)
    {
      printf("%s\n", message);
    }
  }
  inline void dog()
  {
    impl::print("woof");
  }
  inline void cat()
  {
    impl::print("meow");
  }
}

Ein Consumer dieser Bibliothek weiß, dass keine Abhängigkeit von Elementen im Implementierungsnamespace angenommen werden darf. Der Compiler kann natürlich nicht verhindern, dass ruchlose Entwickler genau dies ausführen:

C:\modules> type app.cpp
#include "animals.h"
using namespace animals;
int main()
{
  dog();
  cat();
  impl::print("rats");
}

Können Module in diesem Fall hilfreich sein? Natürlich. Denken Sie jedoch daran, dass der Entwurf von Modulen darauf basiert, das Feature so klein oder einfach wie möglich zu halten. Sobald eine Deklaration exportiert wurde, werden daher alle Elemente bedingungslos exportiert:

C:\modules> type animals.h
#pragma once
#include <stdio.h>
export namespace animals
{
  namespace impl
  {
    // Sadly, this is exported, as well
  }
  // This is exported
}

Wie Abbildung 2 zeigt, können Sie glücklicherweise den Code so neu anordnen, dass der Namespace „animals::impl“ separat unter Beibehaltung der Namespacestruktur der Bibliothek deklariert wird.

Abbildung 2: Beibehalten der Namespacestruktur der Bibliothek

C:\modules> type animals.h
#pragma once
#include <stdio.h>
namespace animals
{
  namespace impl
  {
    // This is *not* exported -- yay!
  }
}
export namespace animals
{
  // This is exported
}

Nun benötigen wir nur noch Visual C++, um die geschachtelten Namespacedefinitionen zu implementieren. Das Ergebnis sieht schon viel besser aus und ist erheblich einfacher für Bibliotheken mit zahlreichen geschachtelten Namespaces zu verwalten:

C:\modules> type animals.h
#pragma once
#include <stdio.h>
namespace animals::impl
{
  // This is *not* exported -- yay
}
export namespace animals
{
  // This is exported
}

Dieses Feature wird hoffentlich in Visual C++ Update 3 vorhanden sein. Ich drücke jedenfalls die Daumen! Nach Lage der Dinge bewirkt der animals.h-Header, dass vorhandene Apps nicht funktionieren, die einfach den Header einschließen und möglicherweise mit einem Compiler erstellt wurden, der noch keine Module unterstützt. Wenn Sie vorhandene Bibliothekbenutzer unterstützen müssen, während ein langsamer Übergang in das Modulmodell erfolgt, können Sie den gefürchteten Präprozessor verwenden, um den Übergang reibungsloser zu gestalten. Diese Vorgehensweise ist jedoch nicht ideal. Der Entwurf von vielen der neueren C++-Sprachfeatures (auch von Modulen) deutet darauf hin, dass das Programmieren von C++ ohne Makros zunehmend plausibler wird. Bis jedoch Module tatsächlich in C++ 17 verfügbar sind und kommerzielle Implementierungen für Entwickler zur Verfügung stehen, kann ich einige Präprozessortricks anwenden, um die Tierbibliothek als Nur-Header-Bibliothek und als Modul zu erstellen. In meinem animals.h-Header kann ich bedingt das Makro „ANIMALS_­EXPORT“ als nichts definieren und allen Namespaces voranstellen, die ich exportieren möchte, wenn dies ein Modul ist (siehe Abbildung 3).

Abbildung 3: Erstellen einer Bibliothek als Nur-Header-Bibliothek und als Modul

C:\modules> type animals.h
#pragma once
#include <stdio.h>
#ifndef ANIMALS_EXPORT
#define ANIMALS_EXPORT
#endif
namespace animals { namespace impl {
// Please don't look here
}}
ANIMALS_EXPORT namespace animals {
// This is all yours
}

Nun kann jeder Entwickler, der nicht mit Modulen vertraut ist oder dem eine adäquate Implementierung fehlt, einfach den animals.h-Header einschließen und dann wie jede andere Nur-Header-Bibliothek verwenden. Ich kann jedoch die Modulschnittstelle aktualisieren, damit „ANIMALS_EXPORT“ so definiert wird, dass dieser gleiche Header eine Sammlung von exportierten Deklarationen wie folgt generieren kann:

C:\modules> type animals.ixx
module animals;
#define ANIMALS_EXPORT export
#include "animals.h"

Wie viele der heutigen C++-Entwickler mag ich Makros nicht und würde lieber in einer Welt ohne Makros leben. Dennoch stellen Sie eine nützliche Technik bei der Umstellung einer Bibliothek auf Module dar. Das Beste daran ist, dass zwar die App, die den animals.h-Header einschließt, das gutartige Makro sieht, es jedoch für alle nicht sichtbar ist, die das Modul einfach importieren. Das Makro wird vor der Erstellung der Metadaten des Moduls entfernt. Auf diese Weise beeinflusst es nie die App oder andere Bibliotheken oder Module, dies es ggf. verwenden.

Module sind eine willkommene Ergänzung für C++. Ich freue mich schon auf ein zukünftiges Update des Compilers mit vollständiger kommerzieller Unterstützung. Im Moment können Sie mit uns zusammen experimentieren, während wir den C++-Standard mit der Aussicht auf ein Modulsystem für C++ optimieren. Weitere Informationen zu Modulen finden Sie in der technischen Spezifikation (goo.gl/Eyp6EB). Sie können sich auch einen Vortrag von Gabriel Dos Reis ansehen, den er im letzten Jahr auf der CppCon gehalten hat (youtu.be/RwdQA0pGWa4).

Coroutinen

Coroutinen (früher als „fortsetzbare Funktionen“ bezeichnet) sind in Visual C++ schon länger bekannt. Ich freue mich noch immer über die Aussicht auf echte Coroutinenunterstützung in der C++-Sprache – mit tiefen Wurzeln im stapelbasierten Sprachentwurf von C. Als ich darüber nachdachte, was ich schreiben soll, dämmerte es mir, dass ich nicht nur einen, sondern mindestens vier Artikel zu diesem Thema für MSDN Magazine verfasst habe. Ich schlage vor, dass Sie zuerst den aktuellen Artikel in der Oktober 2015-Ausgabe lesen (goo.gl/zpP0AO). Dort stelle ich die in Visual C++ 2015 bereitgestellte Coroutinenunterstützung vor. Anstatt die Vorteile von Coroutinen noch einmal zu wiederholen, sehen wir sie uns lieber etwas näher an. Eine der Herausforderungen bei der Aufnahme von Coroutinen in C++ 17 besteht darin, dass der Standardisierungsausschuss die Vorstellung nicht mochte, dass sie automatische Typableitungen bereitstellen könnten. Der Typ der Coroutine kann vom Compiler so abgeleitet werden, dass der Entwickler sich keine Gedanken darum machen muss, um welchen Typ es sich handeln könnte:

auto get_number()
{
  await better_days {};
  return 123;
}

Der Compiler kann einen geeigneten Coroutinentyp generieren. Diese Generierung war wohl durch C++ 14 inspiriert. Dort wurde konstatiert, dass der Rückgabetyp für Funktionen abgeleitet werden kann:

auto get_number()
{
  return 123;
}

Der Standardisierungsausschuss ist jedoch noch nicht einverstanden, dass dieses Konzept auf Coroutinen erweitert wird. Das Problem besteht außerdem darin, dass die C++-Standardbibliothek auch keine geeigneten Kandidaten bereitstellt. Die größte Annäherung ist das schwerfällige „std::future“ mit häufig umfangreicher Implementierung und einem äußerst unpraktischen Entwurf. Auch hinsichtlich asynchroner Datenströme, die von Coroutinen generiert werden, die Werte ausgeben, anstatt asynchron einen einfachen Wert zurückzugeben, ist dies nicht hilfreich. Wenn also der Compiler keinen Typ bereitstellen kann und die C++-Standardbibliothek keinen geeigneten Typ bereitstellt, muss ich mir die Sache etwas näher ansehen, um herauszufinden, wie dies tatsächlich funktioniert, damit ich Fortschritte bezüglich der Coroutinen machen kann. Angenommen, der folgende awaitable-Dummytyp wird verwendet:

struct await_nothing
{
  bool await_ready() noexcept
  {
    return true;
  }
  void await_suspend(std::experimental::coroutine_handle<>) noexcept
  {}
  void await_resume() noexcept
  {}
};

Dieser Typ führt keinerlei Aktionen aus, ermöglicht mir jedoch das Konstruieren einer Coroutine durch Warten auf diese:

coroutine<void> hello_world()
{
  await await_nothing{};
  printf("hello world\n");
}

Wenn ich nicht davon ausgehen kann, dass der Compiler den Rückgabetyp der Coroutine automatisch ableitet, und ich „std::future“ nicht verwenden möchte, wie kann ich dann diese Coroutinenklassenvorlage definieren?

template <typename T>
struct coroutine;

Weil dieser Artikel schon jetzt zu lang wird, sehen wir uns nur das Beispiel für eine Coroutine an, die „nothing“ oder „void“ zurückgibt. Die folgende Spezialisierung wird angewendet:

template <>
struct coroutine<void>
{
};

Die erste Aktion, die der Compiler ausführt, besteht im Suchen nach einem „promise_type“ für den Rückgabetyp der Coroutine. Es gibt weitere Möglichkeiten, dies zu implementieren, insbesondere dann, wenn Sie nachträglich Coroutinenunterstützung in eine vorhandene Bibliothek integrieren müssen. Da ich diese Coroutinenklassenvorlage jedoch schreibe, kann die Deklaration auch dort erfolgen:

template <>
struct coroutine<void>
{
  struct promise_type
  {
  };
};

Im nächsten Schritt sucht der Compiler nach einer return_void-Funktion für die Coroutinenzusage. Dies gilt zumindest für Coroutinen, die keinen Wert zurückgeben:

struct promise_type
{
  void return_void()
  {}
};

„return_void“ muss keine Aktionen ausführen, kann aber von verschiedenen Implementierungen als ein Signal der Zustandsänderung verwendet werden, dass das logische Ergebnis der Coroutine bereit für eine Untersuchung ist. Der Compiler sucht außerdem nach einem Paar aus initial_suspend- und final_suspend-Funktionen:

struct promise_type
{
  void return_void()
  {}
  bool initial_suspend()
  {
    return false;
  }
  bool final_suspend()
  {
    return true;
  }
};

Der Compiler verwendet diese Funktionen, um anfänglichen und abschließenden Code in die Coroutine einzufügen, der den Scheduler informiert, ob die Coroutine in einem angehaltenen Zustand begonnen bzw. vor dem Abschluss angehalten werden soll. Dieses Funktionspaar kann tatsächlich awaitable-Typen zurückgeben. Der Compiler könnte also wie folgt auf beide Typen warten:

coroutine<void> hello_world()
{
  coroutine<void>::promise_type & promise = ...;
  await promise.initial_suspend();
  await await_nothing{};
  printf("hello world\n");
  await promise.final_suspend();
}

Ob gewartet und demnach ein Haltepunkt eingefügt wird, hängt davon ab, was Sie erreichen möchten. Insbesondere dann, wenn Sie die Coroutine nach ihrem Abschluss abfragen müssen, möchten Sie sicherstellen, dass sie angehalten wird. Andernfalls wird die Coroutine zerstört, bevor Sie die Gelegenheit erhalten, einen Wert abzufragen, der von der Zusage erfasst wurde.

Im nächsten Schritt sucht der Compiler nach einer Möglichkeit, das Coroutinenobjekt aus der Zusage abzurufen:

struct promise_type
{
  // ...
  coroutine<void> get_return_object()
  {
    return ...
  }
};

Der Compiler stellt sicher, dass „promise_type“ als Teil des Coroutinenrahmens zugewiesen wird. Er benötigt dann eine Möglichkeit, den Rückgabetyp der Coroutine aus dieser Zusage zu generieren. Dieser wird dann an den Aufrufer zurückgegeben. An dieser Stelle muss ich mich auf eine Low-Level-Hilfsklasse verlassen, die vom Compiler bereitgestellt wird. Sie heißt „coroutine_handle“ und steht zurzeit im Namespace „std::experimental“ zur Verfügung. Ein „coroutine_handle“ stellt einen Aufruf einer Coroutine dar. Daher kann ich dieses Handle als Member meiner Coroutinenklassenvorlage speichern:

template <>
struct coroutine<void>
{
  // ...
  coroutine_handle<promise_type> handle { nullptr };
};

Ich initialisiere das Handle mit einem „nullptr“, um anzugeben, dass die Coroutine zurzeit nicht aktiv ist. Ich kann jedoch auch einen Konstruktor hinzufügen, um ein Handle explizit einer neu erstellten Coroutine zuzuordnen:

explicit coroutine(coroutine_handle<promise_type> coroutine) :
  handle(coroutine)
{}

Der Coroutinenrahmen ähnelt einem Stapelrahmen, ist jedoch eine dynamisch zugeordnete Ressource und muss zerstört werden. Daher verwende ich den Destruktor für diese Aufgabe:

~coroutine()
{
  if (handle)
  {
    handle.destroy();
  }
}

Ich sollte auch die Kopiervorgänge löschen und Verschiebesemantik zulassen. Sie verstehen jedoch, worauf ich hinaus will. Nun kann ich die get_return_object-Funktion des „promise_type“ so implementieren, dass sie als Factory für Coroutinenobjekte fungiert:

struct promise_type
{
  // ...
  coroutine<void> get_return_object()
  {
    return coroutine<void>(
      coroutine_handle<promise_type>::from_promise(this));
  }
};

Dieser Code sollte für den Compiler ausreichend sein, damit er eine Coroutine generiert und aktiviert. Hier sehen Sie nochmals die Coroutine, gefolgt von einer einfachen main-Funktion:

coroutine<void> hello_world()
{
  await await_nothing{};
  printf("hello world\n");
}
int main()
{
  hello_world();
}

Bis jetzt habe ich noch keine Aktionen mit dem Ergebnis von „hello_world“ ausgeführt. Das Ausführen dieses Programms bewirkt jedoch, dass „printf“ aufgerufen wird und die vertraute Meldung an die Konsole ausgegeben wird. Bedeutet dies, dass die Coroutine tatsächlich abgeschlossen wurde? Nun, ich kann der Coroutine diese Frage stellen:

int main()
{
  coroutine<void> routine = hello_world();
  printf("done: %s\n", routine.handle.done() ? "yes" : "no");
}

Dieses Mal führe ich keine Aktionen mit der Coroutine aus, sondern frage nur, ob sie abgeschlossen wurde. Natürlich ist dies der Fall:

hello world
done: yes

Erinnern Sie sich daran, dass die initial_suspend-Funktion des „promise_type“ den Wert FALSE zurückgibt. Die Coroutine selbst wird daher nicht angehalten aktiviert. Denken Sie außerdem daran, dass die await_ready-Funktion von „await_nothing“ den Wert TRUE zurückgibt. Hier wird also auch kein Haltepunkt eingefügt. Das Endergebnis ist eine Coroutine, die synchron abgeschlossen wird, weil ich keine Anweisungen erteilt habe, anders zu reagieren. Der Vorteil besteht darin, dass der Compiler Coroutinen optimieren kann, die sich synchron verhalten, und die gleichen Optimierungen anwenden kann, die Straight-Line-Code so schnell machen. Dies ist noch nicht sehr aufregend. Fügen wir also Anhalteverhalten oder wenigstens einige Haltepunkte hinzu. Dies kann einfach sein, indem der await_nothing-Typ so geändert wird, dass immer angehalten wird, auch wenn er keine Aktion ausführt:

struct await_nothing
{
  bool await_ready() noexcept
  {
    return false;
  }
  // ...
};

In diesem Fall erkennt der Compiler, dass dieses awaitable-Objekt nicht bereit ist, und es erfolgt eine Rückgabe an den Aufrufer, bevor der Vorgang fortgesetzt wird. Nun kehre ich zu meiner einfachen Hello World-App zurück:

int main()
{
  hello_world();
}

Ich bin enttäuscht, dass dieses Programm keine Ausgabe besitzt. Die Ursache sollte offensichtlich sein: Die Coroutine wurde vor dem Aufruf von „printf“ angehalten, und der Aufrufer, der Besitzer des Coroutinenobjekts ist, hat keine Anweisung zum Fortsetzen ausgegeben. Natürlich ist das Fortsetzen einer Coroutine einfach und besteht im Aufrufen der vom Handle bereitgestellten resume-Funktion:

int main()
{
  coroutine<void> routine = hello_world();
  routine.handle.resume();
}

Die hello_world-Funktion kehrt nun erneut ohne den Aufruf von „printf“ zurück, die resume-Funktion bewirkt jedoch den Abschluss der Coroutine. Zur weiteren Veranschaulichung kann ich die done-Methode des Handles wie folgt vor und nach dem Fortsetzen verwenden:

int main()
{
  coroutine<void> routine = hello_world();
  printf("done: %s\n", routine.handle.done() ? "yes" : "no");
  routine.handle.resume();
  printf("done: %s\n", routine.handle.done() ? "yes" : "no");
}

Die Ergebnisse zeigen deutlich die Interaktion zwischen dem Aufrufer und der Coroutine:

done: no
hello world
done: yes

Dies kann sehr praktisch sein, insbesondere in eingebetteten Systemen, die keine anspruchsvollen Betriebssystemscheduler und -threads aufweisen, weil ich auf recht einfache Weise ein einfaches kooperatives Multitaskingsystem schreiben kann:

while (!routine.handle.done())
{
  routine.handle.resume();
  // Do other interesting work ...
}

Coroutinen sind kein Zauberwerk und benötigen auch keine komplexe Zeitplanungs- oder Synchronisierungslogik, um zu funktionieren. Die Unterstützung von Coroutinen mit Rückgabetypen umfasst das Ersetzen der return_void-Funktion des „promise_type“ durch eine return_value-Funktion, die einen Wert annimmt und diesen in der Zusage speichert. Der Aufrufer kann dann den Wert bei Abschluss der Coroutine abrufen. Coroutinen, die einen Datenstrom von Werten ausgeben, erfordern eine ähnliche yield_value-Funktion für den „promise_type“, sind aber ansonsten im Wesentlichen identisch. Die vom Compiler für Coroutinen bereitgestellten Hooks sind recht einfach und doch erstaunlich flexibel. In diesem kurzen Überblick habe ich nur etwas an der Oberfläche gekratzt. Ich hoffe jedoch, dass Sie dieses faszinierende neue Sprachfeature nun besser einschätzen können.

Gor Nishanov, ein weiterer Entwickler im C++-Team bei Microsoft, bemüht sich weiter darum, Coroutinen demnächst einer Standardisierung zu unterziehen. Er arbeitet sogar daran, dem Clang-Compiler Unterstützung für Coroutinen hinzuzufügen. Weitere Informationen zu Coroutinen finden Sie in der technischen Spezifikation (goo.gl/9UDeoa). Sie können sich auch einen Vortrag von Gor Nishanov ansehen, den er im letzten Jahr auf der CppCon gehalten hat (youtu.be/_fu0gx-xseY). James McNellis hat ebenfalls einen Vortrag über Coroutinen beim Meeting C++ (youtu.be/YYtzQ355_Co) gehalten.

Bezüglich C++ finden bei Microsoft zahlreiche weitere Aktivitäten statt. Wir fügen neue C++-Sprachfeatures hinzu, z. B. Variablenvorlagen aus C++ 14, die für das Definieren einer Variablenfamilie verwendet werden können (goo.gl/1LbDJ2). Neil MacIntosh arbeitet an neuen Vorschlägen für die C++-Standardbibliothek für grenzsichere Ansichten von Zeichenfolgen und Sequenzen. Weitere Informationen zu „span<>“ und „string_span“ finden Sie unter goo.gl/zS2Kau und goo.gl/4w6ayn. Eine Implementierung dieser Elemente finden Sie unter (GitHub.com/Microsoft/GSL).

Bezüglich des Back-Ends ist mir vor Kurzem aufgefallen, dass der C++-Optimierer wesentlich intelligenter als gedacht ist, wenn es um die Wegoptimierung von strlen- und wcslen-Aufrufen mit Zeichenfolgenliteralen geht. Dieses Feature ist nicht wirklich neu, sondern nur ein gut gehütetes Geheimnis. Neu ist hingegen, dass Visual C++ endlich die vollständige leere Basisoptimierung implementiert, die seit mehr als einem Jahrzehnt fehlte. Wenn „__declspec(empty_bases)“ auf eine Klasse angewendet wird, führt dies dazu, dass das Layout aller direkten leeren Basisklassen am Offset Null erfolgt. Dies ist noch nicht das Standardverhalten, weil dafür ein Hauptversionsupdate des Compilers erforderlich wäre, um eine so gravierende Änderung einzuführen. Außerdem gehen einige Typen der C++-Standardbibliothek noch vom alten Layout aus. Bibliotheksentwickler können dennoch endlich diese Optimierung nutzen. Insbesondere die moderne C++ für Windows-Runtime (moderncpp.com) profitiert von diesem Feature. Es ist in der Tat der Grund, warum dieses Feature dem Compiler endlich hinzugefügt wurde. Wie ich bereits in der Ausgabe aus Dezember 2015 erwähnte, bin ich seit kurzer Zeit Mitglied im Windows-Team bei Microsoft und beschäftige mich mit dem Erstellen einer neuen Sprachprojektion für die Windows-Runtime, die auf moderncpp.com basiert. Auch dies trägt zur Beförderung von C++ bei Microsoft bei. Lassen Sie sich nicht täuschen, Microsoft beschäftigt sich ernsthaft mit C++.


Kenny Kerrist Softwareentwickler im Windows-Team bei Microsoft. Er veröffentlicht Blogs unter kennykerr.ca, und Sie können ihm auf Twitter folgen: @kennykerr.

Unser Dank gilt dem folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: Andrew Pardoe