Softwareentwicklung: std::span in C++20: Weitere Details

In diesem Blogbeitrag geht es um das Objekt std::span, das sich auf eine zusammenhängende Folge von Objekten bezieht, und um ein persönliches Anliegen.

In Pocket speichern vorlesen Druckansicht 3 Kommentare lesen

(Bild: SergioVas/Shutterstock)

Lesezeit: 6 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Heute möchte ich über die nicht so bekannten Features eines std::span und die Gefahren schreiben. Außerdem geht es um ein persönliches Anliegen für die Forschung zu ALS.

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++.

Ein std::span, manchmal auch View genannt, ist nie ein Besitzer. Dieser zusammenhängende Speicher kann ein einfaches Array, ein Zeiger mit einer bestimmten Größe, ein std::array, ein std::vector oder ein std::string sein. Eine typische Implementierung besteht aus einem Zeiger auf sein erstes Element und einer Größe. Der Hauptgrund für eine std::span<T> ist: Ein C-Array wird auf einen Zeiger auf seines erstes Element reduziert wird, wenn es an eine Funktion übergeben wird. Dadurch geht die Größe des Arrays verloren. Dieser decay ist ein typischer Grund für Fehler in C/C++.

Im Gegensatz dazu leitet std::span<T> die Größe von zusammenhängenden Folgen von Objekten automatisch ab.

// printSpan.cpp

#include <iostream>
#include <vector>
#include <array>
#include <span>

void printMe(std::span<int> container) {
    
    std::cout << "container.size(): " << container.size() << '\n';  // (4)
    for(auto e : container) std::cout << e << ' ';
    std::cout << "\n\n";
}

int main() {
    
    std::cout << std::endl;
    
    int arr[]{1, 2, 3, 4};              // (1)
    printMe(arr);
    
    std::vector vec{1, 2, 3, 4, 5};     // (2)
    printMe(vec);

    std::array arr2{1, 2, 3, 4, 5, 6}; // (3)
    printMe(arr2);
    
}

Das C-Array (1), std::vector (2) und das std::array (3) besitzen ints. Folglich enthält std::span auch ints. In diesem einfachen Beispiel gibt es noch etwas Interessantes. Für jeden Container kann std::span seine Größe ableiten (4).

Dies war eine kurze Erinnerung an std::span. Alles Weitere steht in meinem vorherigen Artikel "C++20: Geschützer Zugriff auf Sequenzen von Objekte mit std::span".

Eine std::span kann einen static extent oder einen dynamic extent haben.

Standardmäßig hat std::span einen dynamischen Extent:

template <typename T, std::size_t Extent = std::dynamic_extent>
class span;

Wenn ein std::span einen statischen Extent besitzt, ist seine Größe zur Compile-Zeit bekannt und Teil des Datentyps: std::span. Daher braucht ihre Implementierung nur einen Zeiger auf das erste Element der zusammenhängenden Folge von Objekten.

Die Implementierung eines std::span mit einem dynamischen extent besteht aus einem Zeiger auf das erste Element und der Größe der zusammenhängenden Folge von Objekten. Die Größe ist nicht Teil des std::span-Typs.

Das nächste Beispiel verdeutlicht die Unterschiede zwischen den beiden Arten von Spans.

// staticDynamicExtentSpan.cpp

#include <iostream>
#include <span>
#include <vector>

void printMe(std::span<int> container) {        // (3)  
    
    std::cout << "container.size(): " << container.size() << '\n';
    for (auto e : container) std::cout << e << ' ';
    std::cout << "\n\n";
}

int main() {

    std::cout << '\n';

    std::vector myVec1{1, 2, 3, 4, 5};        
    std::vector myVec2{6, 7, 8, 9};

    std::span<int> dynamicSpan(myVec1);          // (1)
    std::span<int, 4> staticSpan(myVec2);        // (2)

    printMe(dynamicSpan);
    printMe(staticSpan);

    // staticSpan = dynamicSpan;    ERROR        // (4)
    dynamicSpan = staticSpan;                    // (5) 

    printMe(staticSpan);                         // (6)

    std::cout << '\n';
    
}

dynamicSpan (1) hat einen dynamischen Extent, während staticSpan (2) einen statischen hat. Beide std::spans geben ihre Größe in der Funktion printMe (3) zurück. Ein std::span mit statischen Extent kann einem std::span mit dynamischen extent zugewiesen werden, aber nicht umgekehrt. (4) würde einen Fehler verursachen, aber (5) und (6) sind gültig.

Es gibt einen besonderen Anwendungsfall von std::span. Eine std::span kann ein konstanter Bereich von veränderbaren Elementen sein.

Der Einfachheit halber nenne ich einen std::vector und einen std::span Bereich. Ein std::vector modelliert einen veränderbaren Bereich von veränderbaren Elementen: std::vector. Wenn man diesen std::vector als const deklariert, modelliert er einen konstanten Bereich konstanter Objekte: const std::vector. Man kann keinen konstanten Bereich von veränderbaren Elementen modellieren. An dieser Stelle kommt std::span ins Spiel. Ein std::span modelliert einen konstanten Bereich von veränderbaren Objekten: std::span. Die folgende Abbildung verdeutlicht die Unterschiede zwischen (konstanten/veränderbaren) Bereichen und (konstanten/veränderbaren) Elementen.

// constRangeModifiableElements.cpp

#include <iostream>
#include <span>
#include <vector>

void printMe(std::span<int> container) {
    
    std::cout << "container.size(): " << container.size() << '\n';  
    for (auto e : container) std::cout << e << ' ';
    std::cout << "\n\n";
}

int main() {

    std::cout << '\n';

    std::vector<int> origVec{1, 2, 2, 4, 5};

    // Modifiable range of modifiable elements
    std::vector<int> dynamVec = origVec;           // (1)
    dynamVec[2] = 3;
    dynamVec.push_back(6);
    printMe(dynamVec);

    // Constant range of constant elements
    const std::vector<int> constVec = origVec;     // (2)
    // constVec[2] = 3;        ERROR
    // constVec.push_back(6);  ERROR
    std::span<const int> constSpan(origVec);       // (3)
    // constSpan[2] = 3;       ERROR

    // Constant range of modifiable elements
    std::span<int> dynamSpan{origVec};             // (4)
    dynamSpan[2] = 3;
    printMe(dynamSpan);

    std::cout << '\n';

}

Der Vektor dynamVec (1) ist ein änderbarer Bereich mit änderbaren Elementen. Diese Feststellung gilt nicht für den Vektor constVec (2). constVec kann weder seine Elemente noch seine Größe ändern. constSpan (3) verhält sich entsprechend. dynamSpan (4) modelliert den einzigartigen Anwendungsfall eines konstanten Bereichs mit veränderbaren Elementen.

Zum Schluss möchte ich noch auf zwei Gefahren hinweisen, die man beim Verwenden von std::span kennen sollte.

Die typischen Probleme von std::span sind zweierlei. Erstens sollte eine std::span nicht auf einen temporären Bereich angewandt werden; zweitens sollte die Größe des zugrundeliegenden zusammenhängenden Bereichs einer std::span nicht verändert werden.

Ein std::span ist nie ein Besitzer und verlängert die Lebensdauer des zugrundeliegend Bereichs nicht. Daher sollte eine std::span nur auf lValues operieren. Die Verwendung einer std::span auf einem temporären Bereich ist undefiniertes Verhalten.

// temporarySpan.cpp

#include <iostream>
#include <span>
#include <vector>

std::vector<int> getVector() {                          // (2)
    return {1, 2, 3, 4, 5};
}

int main() {

     std::cout << '\n';
    
    std::vector<int> myVec{1, 2, 3, 4, 5};              // (1)
    std::span<int, 5> mySpan1{myVec};                  
    std::span<int, 5> mySpan2{getVector().begin(), 5};  // (3)

    for (auto v: std::span{myVec}) std::cout << v << " ";
    std::cout << '\n';
    for (auto v: std::span{getVector().begin(), 5}) std::cout << v << " ";  // (4)

     std::cout << "\n\n";
    
}

Die Verwendung eines std::span mit einem statischen Extent oder einer std::span mit einem dynamischen Extent auf dem lValue ist in Ordnung. Wenn ich vom lValue std::vector in (1) zu einem temporären std::vector wechsle, der durch die Funktion getVector (2) erzeugt wird, besitzt das Programm undefiniertes Verhalten. Sowohl (3) als auch (4) sind ungültig. Beim Ausführen wird das undefinierte Verhalten sichtbar. Die Ausgabe in (4) stimmt nicht mit dem std::vector überein, der von der Funktion getVector() erzeugt wird.

Ändern der Größe des zugrundeliegenden zusammenhängenden Bereichs

Wenn man die Größe des zugrundeliegenden zusammenhängenden Bereichs ändert, wird der zusammenhängende Bereich möglicherweise neu allokiert und der std::span bezieht sich auf veraltete Daten.

std::vector<int> myVec{1, 2, 3, 4, 5};

std::span<int> sp1{myVec};

myVec.push_back(6);  // undefined behavior

In meinem nächsten Artikel tauche ich noch einmal in die Formatierungsbibliothek von C++20 ein.

Mein 3-Bücher-Bundle Modern C++ Collection ist jetzt für die Hälfte des Preises ($70 -> $35) verfügbar. Ich werde das Geld für die ALS-Forschung spenden. Lass mich mit den Fakten beginnen.

Hier ist eine Erklärung der ALS-Ambulanz:

ALS ist eines der “Sorgenkinder” der Medizin – seltene Krankheiten stehen nicht im sozialen Fokus. Es existiert keine generelle Finanzierung der ALS-Forschung. Deshalb ist die Behandlung von Menschen mit ALS erheblich unterfinanziert. Hierbei sind Spenden und externe Unterstützung von großer Wichtigkeit.

Diese Unterfinanzierung ist durch die Ice Bucket Challenge deutlich geworden.

Die Forschung braucht mehr Geld. Hier ist meine Idee. Lasst uns eine Spendenaktion starten!

Der Coupon für das 3-Bücher-Bundle Modern C++ Collection für die Hälfte des Preises ($70 -> $35) ist bis zum 21. Januar gültig (inklusive).

Die folgenden drei Bücher sind in diesem Bundle enthalten: The C++ Standard Library, Concurrency with Modern C++ und C++20.

Ich werde das gesamte Geld an die ALS-Ambulanz spenden. Die ALS-Ambulanz ist Teil der Charité und ist führend in der Erforschung und Behandlung von ALS in Europa. (rme)