Softwareentwicklung: Spezielle Allokatoren mit C++17​

Nach der Theorie der polymorphen Allokatoren in C++17 geht es diesmal um deren praktische Anwendung.

In Pocket speichern vorlesen Druckansicht 12 Kommentare lesen
Lesezeit: 3 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

In meinem letzten Artikel "Softwareentwicklung: Polymorphe Allokatoren mit C++17" habe ich die Theorie der polymorphen Allokatoren in C++17 vorgestellt. Heute werde ich die Theorie anwenden.

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

Bevor ich fortfahre, hier noch einmal die wichtigsten Teile meines letzten Artikels "Softwareentwicklung: Polymorphe Allokatoren mit C++17".

Das folgende Programm verwendet polymorphe Allokatoren:

// polymorphicAllocator.cpp

#include <array>
#include <cstddef>
#include <memory_resource>
#include <vector>

int main() {

    std::array<std::byte, 200> buf1;                         // (1)
    std::pmr::monotonic_buffer_resource pool1{buf1.data(), 
      buf1.size()};
    std::pmr::vector<int> myVec1{&pool1};                    // (3)
    for (int i = 0; i < 5; ++i) {
        myVec1.push_back(i);
    }

    char buf2[200] = {};                                     // (2)
    std::pmr::monotonic_buffer_resource pool2{std::data(buf2), 
      std::size(buf2)};
    std::pmr::vector<int> myVec2{&pool2};
    for (int i = 0; i < 200; ++i) {
        myVec2.push_back(i);
    }

}

Jetzt möchte ich mich auf myVec2 konzentrieren. 200 ints werden auf den std::pmr::vector<int> geschoben. Diese 200 ints passen nicht in ein char buf[200] und deshalb springt std::pmr::new_delete_resource() als sogenannter Upstream-Allokator ein und ruft das globale new für die restlichen Elemente auf. Den Upstream-Allokator werde ich nun instrumentalisieren.

Das folgende Programm basiert auf dem vorherigen, verwendet einen Tracking-Allokator und macht die dynamische Speicherzuweisung und -freigabe sichtbar.

// trackAllocator.cpp

#include <array>
#include <cstdlib>
#include <format>
#include <iostream>
#include <memory_resource>
#include <vector>

class TrackAllocator : public std::pmr::memory_resource {
    void* do_allocate(std::size_t bytes, 
                      std::size_t alignment) override {
        void* p = std::pmr::new_delete_resource()->
          allocate(bytes, alignment);
        std::cout << 
          std::format("  do_allocate: {:6} bytes at {}\n",
                      bytes, p);
        return p;
    }
 
    void do_deallocate(void* p, 
                       std::size_t bytes, 
                       std::size_t alignment) override {
        std::cout << 
          std::format("  do_deallocate: {:4} bytes at {}\n", 
                      bytes, p);
        return 
          std::pmr::new_delete_resource()->
            deallocate(p, bytes, alignment);
    }
 
    bool do_is_equal(const std::pmr::memory_resource& other) 
      const noexcept override {
        return std::pmr::new_delete_resource()->is_equal(other);
    }
};


int main() {

    std::cout << '\n';

    TrackAllocator trackAllocator;                         // (1)
    std::pmr::set_default_resource(&trackAllocator);       // (2)

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

    std::array<std::byte, 200> buf1;
    std::pmr::monotonic_buffer_resource pool1{buf1.data(), 
                                              buf1.size()};
    std::pmr::vector<int> myVec1{&pool1};                  // (3)
    for (int i = 0; i < 5; ++i) {
        myVec1.push_back(i);
    }

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

    char buf2[200] = {}; 
    std::pmr::monotonic_buffer_resource pool2{std::data(buf2),
                                             std::size(buf2)};
    std::pmr::vector<int> myVec2{&pool2};                  // (4)
    for (int i = 0; i < 200; ++i) {
        myVec2.push_back(i);
    }

    std::cout << '\n';

}

TrackAllocator ist ein Allokator zum Nachverfolgen der Speicherallokation und deren Freigabe. Er leitet sich von der Schnittstellenklasse std::pmr::memory_resource ab, von der sich alle Speicherressourcen ableiten. TrackAllocator definiert die drei erforderlichen Mitgliedsfunktionen do_allocate, do_deallocate und do_is_equal. Jeder Aufruf leitet diesen an den Aufruf von std::pmr::new_delete_resource weiter. std:pmr::new_delete_resource ist die Standard-Speicherressource und ruft das globale new und delete auf. Die Aufgabe der Mitgliedsfunktion do_is_equal ist es, zu prüfen, ob die beiden Speicherressourcen gleich sind. Der interessante Punkt an dem Beispiel ist, dass ich die Zuweisung und Freigabe des Speichers in den Mitgliedsfunktionen do_allocate und do_deallocate visualisiere.

iX-Workshop mit Rainer Grimm: C++20 Deep Dive

Die mit C++20 eingeführten Concepts haben zusammen mit der Ranges-Bibliothek, Modulen und Coroutinen das Erstellen von modernen C++- Anwendungen neu definiert. Vom 7. bis zum 9. November 2023 bringt Sie Rainer Grimm in seinem Intensiv-Workshop C++20: die neuen Konzepte umfassend erklärt auf Stand und geht auf die vielen nützlichen Funktionen ein, die C++20 mitbringt.

Ich instanziiere den TrackAllocator (Zeile 1) und mache ihn zur Standardressource (2). myVec1 (3) und myVec2 (4) verwenden ihn als Upstream-Allokator. Dieser Allokator springt ein, wenn der primäre Allokator konsumiert ist. Dieser Fallback ist für myVec1 nicht notwendig, für myVec2 aber schon.

Diese Ausgabe zeigt die dynamische Zuweisung und Freigabe von myVec2, bis std::pmr::vector<int> alle ints aufnehmen kann.

Ein Upstream-Allokator lässt sich auch an einen bestimmten Container binden.

std::pmr::null_resource_allocator ist ein besonderer Allokator. Sie für die Allokation zu verwenden, führt zu einer std::bad_alloc-Ausnahme. Diese Speicherressource stellt sicher, dass man nicht willkürlich Speicher auf dem Heap alloziert:

// nullMemoryResource.cpp

#include <array>
#include <cstddef>
#include <iostream> 
#include <memory_resource>
#include <string>
#include <vector>

int main() {

    std::cout << '\n';
 
    std::array<std::byte, 2000> buf;
    std::pmr::monotonic_buffer_resource pool{buf.data(),
                            buf.size(),                   // (1)
                            std::pmr::null_memory_resource()};
    std::pmr::vector<std::pmr::string> myVec{&pool};      // (2)
    try {
        for (int i = 0; i < 100; ++i) {                   // (3)
            std::cerr << i << " ";
            myVec.emplace_back("A short string");
        }
    }
    catch (const std::bad_alloc& e) {                     // (4)
        std::cerr << '\n' << e.what() << '\n';
    }
    
    std::cout << '\n';
    
}

Zuerst weise ich Speicher auf dem Stack zu und initialisiere std::pmr::monotonic_buffer_resource damit. Dann verwende ich diese Speicherressource und std::pmr::null_memory_resource als Upstream-Allokator (1). In (2) erstelle ich einen std::pmr::vector<std::pmr::string>. Da ich einen std::pmr::string verwende, nutzt der String auch die Speicherressource und ihren Upstream-Allokator. Wenn ich std::pmr::vector<std::string> verwende, nutzt std::string die globalen Allokatoren new und delete. Schließlich erstelle ich in (3) 100 Strings und fange in (4) eine std::bad_alloc-Ausnahme ab. Ich habe bekommen, was ich verdient habe: 100 Strings passen nicht in einen std::array<std::byte, 2000>-Puffer. Nach 17 Strings ist der Puffer aufgebraucht.

Der std::pmr::monotonic_buffer besitzt hervorragende Eigenschaften. Er ist ziemlich schnell und gibt keinen Speicher frei. Die Zahlen dazu liefert mein nächster Artikel. (rme)