Datentypen mit Concepts prüfen

Concepts sind ein mächtiges und elegantes Werkzeug, um zur Compiletime zu prüfen, ob ein Typ erfüllt ist.

Lesezeit: 4 Min.
In Pocket speichern
vorlesen Druckansicht Kommentare lesen 8 Beiträge
Von
  • Rainer Grimm

Ich erhalte in meinen C++ Schulung oft die folgende Frage: Wie kann ich sicher sein, dass mein Datentyp verschiebbar ist? Nun, man kann entweder die Abhängigkeiten zwischen den Big Six untersuchen oder das Concept Big Six definieren und verwenden. In meinem letzten Beitrag "Datentypen mit Concepts prüfen - The Motivation" habe ich den ersten Teil der Antwort vorgestellt und die sehr komplexen Abhängigkeiten zwischen den Big Six erklärt. Zur Erinnerung: Hier sind die Big Six, einschließlich der Move-Semantik:

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

  • Default-Konstruktor: X()
  • Copy-Konstruktor: X(const X&)
  • Copy-Zuweisungsoperator: operator=(const X&)
  • Move-Konstruktor: X(X&&)
  • Move-Zuweisungsoperator: operator=(X&&)
  • Destruktor: ~X()

Heute möchte ich das Concept BigSix definieren und verwenden.

Bevor ich das tue, habe ich einen kurzen Disclaimer: C++20 unterstützt bereits die Concepts std::semiregular und std::regular.

Ein semiregulärer Datentyp muss die Großen Sechs unterstützen und austauschbar sein:

  • Default-Konstruktor: X()
  • Copy-Konstruktor: X(const X&)
  • Copy-Zuweisungsoperator: operator=(const X&)
  • Move-Konstruktor: X(X&&)
  • Move-Zuweisungsoperator: operator=(X&&)
  • Destruktor: ~X()
  • Austauschbarkeit: swap(X&, X&)

Zusätzlich verlangt std::regular für einen Typ X, dass er das Concept std::semiregular unterstützt sich auf Gleichheit prüfen lässt.

  • Default-Konstruktor: X()
  • Copy-Konstruktor: X(const X&)
  • Copy-Zuweisungsoperator: operator=(const X&)
  • Move-Konstruktor: X(X&&)
  • Move-Zuweisungsoperator: operator=(X&&)
  • Destruktor: ~X()
  • Austauschbarkeit: swap(X&, X&)
  • Gleichheit: bool operator == (const X&, const X&)

Daher gibt es eigentlich keinen Grund, das Concept BigSix selbst zu definieren. Wer das Concept std::semiregular verwendet bekommt die Eigenschaft Austauschbarkeit umsonst. Hier ist eine C++11-Implementierung von std::swap:

template <typename T>
void swap(T& a, T& b) noexcept {
    T tmp(std::move(a));  // move constructor
    a = std::move(b);     // move assignment
    b = std::move(tmp);   // move assignment
}

Beim Aufruf von swap(a, b) wendet der Compiler Move-Semantik auf die Argumente a und b an. Folglich unterstützt ein Typ, der das Concept BigSix unterstützt, auch swappable und damit das Concept std::semiregular.

Jetzt möchte ich das trotzdem das Concept BigSix implementieren.

Dank der Funktionen der Type Traits ist die Implementierung des Concept BigSix ein Kinderspiel. Im ersten Schritt definiere ich die Type Traits isBigSix und im zweiten Schritt verwende ich sie direkt zur Definiton des Concept BigSix. Und los geht es:

// bigSixConcept.cpp

#include <algorithm>
#include <iostream>
#include <type_traits>

template<typename T>
struct isBigSix: std::integral_constant<bool,
                        std::is_default_constructible<T>::value &&
                        std::is_copy_constructible<T>::value &&
                        std::is_copy_assignable<T>::value &&
                        std::is_move_constructible<T>::value &&
                        std::is_move_assignable<T>::value &&
                        std::is_destructible<T>::value>{};


template<typename T>
concept BigSix = isBigSix<T>::value;

template <BigSix T>                                   // (1)
void swap(T& a, T& b) noexcept {
    T tmp(std::move(a));
    a = std::move(b);
    b = std::move(tmp);
}

struct MyData{                                        // (2)
  MyData() = default;
  MyData(const MyData& ) = default;
  MyData& operator=(const MyData& m) = default;

};

int main(){

    std::cout << '\n';

    MyData a, b;
    swap(a, b);                                             // (3)

    static_assert(BigSix<MyData>, "BigSix not supported");  // (4)

    std::cout << '\n';

}

Die Funktion swap setzt nun voraus, dass der Typ-Parameter T das Concept BigSix unterstützt (1). In (3) rufe ich die Funktion swap mit Argumenten vom Typ MyData auf. Außerdem prüfe ich in (4) explizit, ob MyData das Concept BigSix unterstützt. MyData (2) hat einen Default-Konstruktor und unterstützt Copy-Semantik. Das Programm lässt sich kompilieren und ausführen.

Bedeutet das, dass MyData das Concept BigSix unterstützt und deshalb in meine Funktion swap verschoben wird? Ja, MyData unterstützt das Concept BigSix, aber nein, MyData wird nicht in der Funktion swap verschoben. Die Copy-Semantik tritt als Fallback für die Move-Semantik in Aktion.

Hier ist ein leicht verändertes Programm.

// bigSixConceptComments.cpp

#include <algorithm>
#include <iostream>
#include <type_traits>

template<typename T>
struct isBigSix: std::integral_constant<bool,
                        std::is_default_constructible<T>::value &&
                        std::is_copy_constructible<T>::value &&
                        std::is_copy_assignable<T>::value &&
                        std::is_move_constructible<T>::value &&
                        std::is_move_assignable<T>::value &&
                        std::is_destructible<T>::value>{};


template<typename T>
concept BigSix = isBigSix<T>::value;

template <BigSix T> 
void swap(T& a, T& b) noexcept {
    T tmp(std::move(a));
    a = std::move(b);
    b = std::move(tmp);
}

struct MyData{ 
    MyData() = default;
    MyData(const MyData& ) {
        std::cout << "copy constructor\n";
    }
    MyData& operator=(const MyData& m) {
        std::cout << "copy assignment operator\n";
        return *this;
    }

};

int main(){

    std::cout << '\n';

    MyData a, b;
    swap(a, b);
    
    static_assert(BigSix<MyData>, "BigSix not supported");

    std::cout << '\n';

}

Ich habe Kommentare zum Copy-Konstruktor und zum Copy-Zuweisungsoperator von MyData hinzugefügt. Wer das Programm ausführt, sieht, dass beide speziellen Mitgliedsfunktionen verwendet werden:

Übrigens ist diese Beobachtung bereits in cppreference.com dokumentiert. So heißt es zum Beispiel in einem Hinweis auf die Typ-Eigenschaft std::is_move_constructible: "Types without a move constructor, but with a Copy-Konstruktor that accepts const T& arguments, satisfy std::is_move_constructible."

Okay, wir sind wieder am Anfang. Wir können entscheiden, ob ein Typ die BigSix unterstützt, aber wir können nicht entscheiden, ob ein Typ wirklich verschoben wird. Wer wissen willt, ob ein Typ die Move-Semantik unterstützt und nicht, ob die Copy-Semantik als Fallback für die Move-Semantik verwendet wird, muss die Abhängigkeitstabelle in meinem vorherigen Beitrag studieren: "Datentypen mit Concepts prüfen - The Motivation".

In meinem nächsten Beitrag möchte ich meine Geschichte mit Ranges in C++20 fortsetzen. Darüber hinaus erfahren Ranges in C++23 viele Verbesserungen. ()