C++23: Eine multidimensionale View

Der neue Standard der Programmiersprache führt mit std::mdspan eine nicht besitzende multidimensionale View einer zusammenhängenden Folge von Objekten ein.

In Pocket speichern vorlesen Druckansicht 2 Kommentare lesen

(Bild: SergioVas/Shutterstock)

Lesezeit: 4 Min.
Von
  • Rainer Grimm

Ein std::mdspan ist eine nicht-besitzende multidimensionale View einer zusammenhängenden Folge von Objekten. Dabei kann es sich um ein einfaches C-Array, einen Zeiger mit einer Größe, ein std::array, ein std::vector oder ein std::string handeln.

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

Oft wird diese multidimensionale View auch als multidimensionales Array bezeichnet.

Die Anzahl der Dimensionen und die Größe der einzelnen Dimensionen bestimmen die Form des mehrdimensionalen Arrays. Die Anzahl der Dimensionen wird Rang (rank) genannt und die Größe jeder Dimension Erweiterung (extension). std::mdspan's Größe ist das Produkt aus allen Dimensionen, die nicht 0 sind. Man kann auf die Elemente eines std::mdspan mit dem mehrdimensionalen Indexoperator [] zugreifen.

Jede Dimension eines std::mdspan kann einen static extent oder einen dynamic extent haben. static extent bedeutet, dass ihre Länge zur Compile-Zeit festgelegt wird; dynamic extent bedeutet, dass ihre Länge zur Laufzeit festgelegt wird.

Hier ist die Definition eines std::mdspan:

template<
    class T,
    class Extents,
    class LayoutPolicy = std::layout_right,
    class AccessorPolicy = std::default_accessor<T>
> class mdspan;
  • T: die zusammenhängende Folge von Objekten,
  • Extents: gibt die Anzahl der Dimensionen als deren Größe an; jede Dimension kann einen static extent oder einen dynamic extent haben.
  • LayoutPolicy: legt die Layout-Richtlinie für den Zugriff auf den zugrunde liegenden Speicher fest.
  • AccessorPolicy: legt fest, wie die zugrunde liegenden Elemente referenziert werden.

Dank Klassen-Template Argument Deduktion (CTAG) in C++17 kann der Compiler die Template-Argumente oft automatisch aus den Datentypen der Initialisierer ableiten:

// mdspan.cpp

#include <mdspan>
#include <iostream>
#include <vector>

int main() {
    
    std::vector myVec{1, 2, 3, 4, 5, 6, 7, 8};          // (1)

    std::mdspan m{myVec.data(), 2, 4};                  // (2)
    std::cout << "m.rank(): " << m.rank() << '\n';      // (4)

    for (std::size_t i = 0; i < m.extent(0); ++i) {     // (6)
        for (std::size_t j = 0; j < m.extent(1); ++j) { // (7)
            std::cout << m[i, j] << ' ';                // (8)
        }
        std::cout << '\n';
    }

    std::cout << '\n';

    std::mdspan m2{myVec.data(), 4, 2};                 // (3)
    std::cout << "m2.rank(): " << m2.rank() << '\n';    // (5)

    for (std::size_t i = 0; i < m2.extent(0); ++i) {
        for (std::size_t j = 0; j < m2.extent(1); ++j) {
        std::cout << m2[i, j] << ' ';  
    }
    std::cout << '\n';
  }

}

In diesem Beispiel wende ich Klassen-Template Argument Deduktion dreimal an. In (1) wird sie für einen std::vector verwendet und in (2) und (3) für ein std::mdspan. Das erste zweidimensionale Array m hat die Form (2, 4), das zweite m2 hat die Form (4, 2). In (4) und (5) werden die Ränge der beiden std::mdspan angezeigt. Dank der Ausdehnung jeder Dimension (6 und 7) und des Indexoperators in (8) ist es ganz einfach, durch mehrdimensionale Arrays zu iterieren.

Wenn ein mehrdimensionales Array ein static extent haben soll, muss man die Template-Argumente angeben.

// staticDynamicExtent.cpp

#include <mdspan>
#include <iostream>
#include <string>
#include <vector>
#include <tuple>

int main() {
    
    std::vector myVec{1, 2, 3, 4, 5, 6, 7, 8};

    std::mdspan<int, std::extents<std::size_t, 2, 4>>
      m{myVec.data()};                                       // (1)
    std::cout << "m.rank(): " << m.rank() << '\n';

    for (std::size_t i = 0; i < m.extent(0); ++i) {
        for (std::size_t j = 0; j < m.extent(1); ++j) {
            std::cout << m[i, j] << ' ';  
        }
        std::cout << '\n';
    }

    std::cout << '\n';

    std::mdspan<int, std::extents<std::size_t, std::dynamic_extent, 
                std::dynamic_extent>> m2{myVec.data(), 4, 2};// (2)
    std::cout << "m2.rank(): " << m2.rank() << '\n';

    for (std::size_t i = 0; i < m2.extent(0); ++i) {
        for (std::size_t j = 0; j < m2.extent(1); ++j) {
        std::cout << m2[i, j] << ' ';  
    }
    std::cout << '\n';
  }

   std::cout << '\n';

}

Das Programm staticDynamicExtent.cpp basiert auf dem vorherigen Programm mdspan.cpp und erzeugt die gleiche Ausgabe. Der Unterschied ist, dass der std::mdspan m (1) einen static extent hat. Der Vollständigkeit halber: std::mdspan m2 (2) hat einen dynamic extent. Dementsprechend wird die Form von m mit Template-Argumenten angegeben, die Form von m2 jedoch mit Funktionsargumenten.

Ein std::mdspan ermöglicht es dir, die Layout-Strategie für den Zugriff auf den zugrunde liegenden Speicher anzugeben. Standardmäßig wird std::layout_right (C-, C++- oder Python-Stil) verwendet, du kannst aber auch std::layout_left (Fortran- oder MATLAB-Stil) angeben. Die folgende Grafik veranschaulicht, in welcher Reihenfolge auf die Elemente des std::mdspan zugegriffen wird.

Das Durchlaufen von zwei std::mdspan mit den Layout-Strategien std::layout_right und std::layout_left zeigt den Unterschied.

// mdspanLayout.cpp

#include <mdspan>
#include <iostream>
#include <vector>

int main() {
    
    std::vector myVec{1, 2, 3, 4, 5, 6, 7, 8};

    std::mdspan<int, std::extents<std::size_t,      // (1)
         std::dynamic_extent, std::dynamic_extent>, 
         std::layout_right> m{myVec.data(), 4, 2};
    std::cout << "m.rank(): " << m.rank() << '\n';

    for (std::size_t i = 0; i < m.extent(0); ++i) {
        for (std::size_t j = 0; j < m.extent(1); ++j) {
            std::cout << m[i, j] << ' ';  
        }
        std::cout << '\n';
    }

    std::cout << '\n';

    std::mdspan<int, std::extents<std::size_t,     // (2)
         std::dynamic_extent, std::dynamic_extent>, 
         std::layout_left> m2{myVec.data(), 4, 2};
    std::cout << "m2.rank(): " << m2.rank() << '\n';

    for (std::size_t i = 0; i < m2.extent(0); ++i) {
        for (std::size_t j = 0; j < m2.extent(1); ++j) {
        std::cout << m2[i, j] << ' ';  
    }
    std::cout << '\n';
  }

}

Der std::mdspan m verwendet std::layout_right (1), der andere std::mdspan std::layout_left (2). Dank der Deduktion der Klassen-Template-Argumente benötigt der Konstruktoraufruf von std::mdspan (2) keine expliziten Template-Argumente und ist äquivalent zum Ausdruck std::mdspan m2{myVec.data(), 4, 2}.

Die Ausgabe des Programms zeigt die beiden unterschiedlichen Layout-Strategien:

Die folgende Tabelle gibt einen Überblick über die Schnittstelle von std::mdspan md.

C++20 bietet keine konkreten Koroutinen an, aber ein Framework für deren Implementierung von. Das ändert sich mit C++23. std::generator ist die erste konkrete Koroutine. (rme)