zurück zum Artikel

Die Ranges-Bibliothek in C++20: Weitere Designentscheidungen​

Rainer Grimm
Neural,Network,3d,Illustration.,Big,Data,And,Cybersecurity.,Data,Stream.

(Bild: Yurchanka Siarhei / Shutterstock.com)

Die Ranges-Bibliothek in C++20 hat aus Performancegründen ein paar besondere Designentscheidungen zum Caching und zur Konstanz getroffen.

Im Interesse der Performance gibt es einige Besonderheiten der Ranges-Bibliothek in C++20. Diese Designentscheidungen besitzen Konsequenzen: Probleme mit dem Cache und der Konstanz.

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

Hier ist eine kurze Erinnerung. In meinem letzten Artikel, "Die Ranges-Bibliothek in C++20: Designentscheidungen [1]", habe ich diese mögliche Implementierung von std::ranges::filter_view vorgestellt:

if constexpr (!ranges::forward_range<V>)
    return /* iterator */{*this, ranges::find_if(base_, std::ref(*pred_))};
else
{
    if (!begin_.has_value())
        begin_ = ranges::find_if(base_, std::ref(*pred_)); // caching
    return /* iterator */{*this, begin_.value())};
}

Die wichtigste Beobachtung ist, dass der begin-Iterator für nachfolgende Aufrufe zwischengespeichert wird. Diese Zwischenspeicherung hat zwei interessante Konsequenzen:

Oder positiv formuliert: Man muss Views direkt verwenden, nachdem man sie definiert hat.

Es gibt noch mehr wichtige Designentscheidungen.

Die Mitgliedsfunktion einer View kann die Position zwischenspeichern. Das impliziert die folgenden Punkte:

Zuerst möchte ich mich dem ersten Punkt widmen.

Die Funktion printElements nimmt ihren View per Universal Referenz an.

void printElements(std::ranges::input_range auto&& rang) {
    for (int i: rang) {
        std::cout << i << " ";  
    }
    std::cout << '\n';
}

printElements nimmt ihr Argument per Universal Referenz an. Sie per lvalue-Referenz oder als Wert anzunehmen, besitzt negative Konsequenzen.

Das Argument per konstanter lvalue-Referenz anzunehmen schlägt fehl, weil der implizite begin-Aufruf der View das Argument verändern kann. Im Gegenteil dazu kann eine nichtkonstante lvalue-Referenz keinen rvalue verarbeiten.

Das Argument als Wert anzunehmen, kann hingegen den Zwischenspeicher ungültig machen.

Das folgende Programm veranschaulicht das Problem der Concurrency bei Views:

// dataRaceRanges.cpp

#include <numeric>
#include <iostream>
#include <ranges>
#include <thread>
#include <vector>
 
int main() {

    std::vector<int> vec(1'000);
    std::iota(vec.begin(), vec.end(), 0);

    auto first5Vector = vec | std::views::filter([](auto v) { return v > 0; }) 
                            | std::views::take(5);

    std::jthread thr1([&first5Vector]{
        for (int i: first5Vector) {
            std::cout << i << " ";  
        }
    });


    for (int i: first5Vector) {
        std::cout << i << " ";  
    }

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

Ich iteriere im Programm dataRaceRanges.cpp zweimal gleichzeitig durch eine View, ohne ihn zu verändern. Zuerst iteriere ich im std::jthread thr1 und dann in der main-Funktion. Das ist ein Data Race, weil beide Iterationen implizit die Mitgliedsfunktion begin verwenden, die die Position zwischenspeichern kann. ThreadSanitizer [2]macht dieses Data Race sichtbar und beschwert sich über ein vorheriges Schreiben in Zeile 24: std::cout << i << " ";

Im Gegensatz dazu ist die Iteration durch einen klassischen Container wie std::vector thread-safe. Es gibt einen weiteren Unterschied zwischen klassischen Containern und Views.

Klassische Container modellieren tiefe Konstanz. Sie geben sie an ihre Elemente weiter. Das bedeutet, dass es unmöglich ist, Elemente eines konstanten Containers zu ändern.

// constPropagationContainer.cpp

#include <iostream>
#include <vector>

template <typename T>
void modifyConstRange(const T& cont) {
    cont[0] = 5;
}
 
int main() {

    std::vector myVec{1, 2, 3, 4, 5};
    modifyConstRange(myVec);         // ERROR

} 

Der Aufruf modifyConstRange(myVec)verursacht einen Fehler zur Compile-Zeit.

Im Gegensatz dazu modellieren Views eine flache Konstanz. Sie geben sie nicht an ihre Elemente weiter. Die Elemente können daher trotzdem geändert werden.

// constPropagationViews.cpp

#include <iostream>
#include <ranges>
#include <vector>

template <typename T>
void modifyConstRange(const T& cont) {
    cont[0] = 5;
}
 
int main() {

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

    modifyConstRange(std::views::all(myVec));   // OK

}  

Der Aufruf modifyConstRange(std::views::all(myVec)) ist in Ordnung.

Coroutinen sind wahrscheinlich die anspruchsvollste Komponente von C++20. Mein nächster Artikel ist ein Gastbeitrag von Dian-Lun Lin. Er wird eine kurze Einführung in Coroutinen geben und seine Idee anhand eines einfachen Schedulers, der Tasks verwaltet, veranschaulichen. (rme [3])


URL dieses Artikels:
https://www.heise.de/-9351546

Links in diesem Artikel:
[1] https://www.heise.de/blog/Die-Ranges-Bibliothek-in-C-20-Designentscheidungen-9346271.html
[2] https://clang.llvm.org/docs/ThreadSanitizer.html
[3] mailto:rme@ix.de