Bit-Manipulationen mit C++20

Modernes C++ Rainer Grimm  –  69 Kommentare

Nun schließe ich meine Artikelserie zu Features der C++20-Bibliothek ab. Den Abschluss bilden die Klasse std::source_location und die Funktionen zur Bit-Manipulation.

std::source_location

std::source_location bietet Informationen zum Sourcecode an. Diese umfassen den Dateinamen, die Zeilennummer und den Funktionsnamen. Diese Werte sind zum Debuggen, Loggen oder Testen sehr wertvoll. Damit ist die Klasse std::source_location die deutliche bessere Alternative zu den vordefinierten Makros __FILE__ und __LINE__ in C++11. Das heißt natürlich, dass std::source_location in C++20 zum Einsatz kommen sollte.

Die folgende Tabelle stellt das Interface von std::source_location kompakt dar.

Der Aufruf std::source_location::current() erzeugt eine neues source_location-Objekt src. Escstellt die Information des Aufrufers bereit. Zum jetzigen Zeitpunkt unterstützt noch kein C++-Compiler std::source_location. Konsequenterweise ist das folgende Beispiel sourceLocation.cpp direkt von der Online-Ressource cppreference.com/source_location:

// sourceLocation.cpp
// from cppreference.com

#include <iostream>
#include <string_view>
#include <source_location>

void log(std::string_view message,
const std::source_location& location = std::source_location::current())
{
std::cout << "info:"
<< location.file_name() << ':'
<< location.line() << ' '
<< message << '\n';
}

int main()
{
log("Hello world!"); // info:main.cpp:19 Hello world!
}

Die Ausgabe des Programms ist Bestandteil des Sourcecodes.

Durch C++20 wird es sehr einfach, auf Bits oder Bit-Sequenzen zuzugreifen oder sie zu modifizieren.

Bit-Manipulationen

Dank des neuen Datentyps std::endian ist es einfach, die Byte-Reihenfolge eines skalaren Datentyps zu ermitteln.

Byte-Reihenfolge

  • Die Byte-Reihenfolge kann big-endian order little-endian sein. Ersteres bedeutet, dass das höchstwertige Byte zuerst gespeichert wird, das zweite, dass das kleinstwertige Byte zuerst gespeichert wird.
  • Ein skalarer Datentyp ist ein arithmetischer Datentyp, eine enum, ein Zeiger, ein Zeiger auf ein Mitglied oder ein std::nullptr_t.

Die Klasse endian bietet die Byte-Reihenfolge für skalare Datentypen an:

enum class endian
{
little = /*implementation-defined*/,
big = /*implementation-defined*/,
native = /*implementation-defined*/
};
  • Wenn alle skalare Datentypen little-endian sind, dann ist der Wert von std::endian::native std::endian::little.
  • Wenn alle skalare Datentypen big-endian sind, dann ist der Wert von std::endian::native std::endian::big.

Selbst Sonderfälle werden unterstützt:

  • Wenn alle skalare Daten sizeof 1 besitzen und damit die Byte-Reihenfolge irrelevant ist, sind die Werte aller Aufzähler std::endian::little, std::endian::big und std::endian::native identisch.
  • Falls eine Plattform verschiedene Byte-Reihenfolgen verwendet, dann besitzt std::endian::native einen anderen Wert als std::endian::big oder std::endian::little.

Das Ausführen des Programms getEndianness.cpp auf einer x86-Architektur gibt mir die Antwort little-endian zurück:

// getEndianness.cpp

#include <bit>
#include <iostream>

int main() {

if constexpr (std::endian::native == std::endian::big) {
std::cout << "big-endian" << '\n';
}
else if constexpr (std::endian::native == std::endian::little) {
std::cout << "little-endian" << '\n'; // little-endian
}

}

constexpr if erlaubt es, Sourcecode bedingt zu kompilieren. Das heißt in dem konkreten Fall, dass die Kompilierung von der Byte-Reihenfolge der Architektur abhängt. Mehr Information zur Byte-Reihenfolge gibt die gleichnamige Wikipedia-Seite.

Bits oder Bit-Sequenzen manipulieren

Die folgenden Tabellen zeigen alle Funktionen im Überblick.

Die Funktionen benötigen mit Ausnahme der Funktion std::bit_cast eine vorzeichenlose Ganzzahl (unsigned char, unsigned short, unsigned int, unsigned long oder unsigned long long).

Die Funktion bit.cpp zeigt die Anwendung der Funktionen:

// bit.cpp

#include <bit>
#include <bitset>
#include <iostream>

int main() {

std::uint8_t num= 0b00110010;

std::cout << std::boolalpha;

std::cout << "std::has_single_bit(0b00110010): "
<< std::has_single_bit(num) << '\n';

std::cout << "std::bit_ceil(0b00110010): "
<< std::bitset<8>(std::bit_ceil(num)) << '\n';

std::cout << "std::bit_floor(0b00110010): "
<< std::bitset<8>(std::bit_floor(num)) << '\n';

std::cout << "std::bit_width(5u): "
<< std::bit_width(5u) << '\n';

std::cout << "std::rotl(0b00110010, 2): "
<< std::bitset<8>(std::rotl(num, 2)) << '\n';

std::cout << "std::rotr(0b00110010, 2): "
<< std::bitset<8>(std::rotr(num, 2)) << '\n';

std::cout << "std::countl_zero(0b00110010): "
<< std::countl_zero(num) << '\n';

std::cout << "std::countl_one(0b00110010): "
<< std::countl_one(num) << '\n';

std::cout << "std::countr_zero(0b00110010): "
<< std::countr_zero(num) << '\n';

std::cout << "std::countr_one(0b00110010): "
<< std::countr_one(num) << '\n';

std::cout << "std::popcount(0b00110010): "
<< std::popcount(num) << '\n';

}

Die folgende Ausgabe erzeugt das Programm:

Das nächste Programm zeigt den Einsatz und die Ausgabe der Funktionen std::bit_floor, std::bit_ceil, std::bit_width und std::bit_popcount für die Zahlen 2 bis 7:

// bitFloorCeil.cpp

#include <bit>
#include <bitset>
#include <iostream>

int main() {

std::cout << std::endl;

std::cout << std::boolalpha;

for (auto i = 2u; i < 8u; ++i) {
std::cout << "bit_floor(" << std::bitset<8>(i) << ") = "
<< std::bit_floor(i) << '\n';

std::cout << "bit_ceil(" << std::bitset<8>(i) << ") = "
<< std::bit_ceil(i) << '\n';

std::cout << "bit_width(" << std::bitset<8>(i) << ") = "
<< std::bit_width(i) << '\n';

std::cout << "bit_popcount(" << std::bitset<8>(i) << ") = "
<< std::popcount(i) << '\n';

std::cout << std::endl;
}

std::cout << std::endl;

}

Wie geht's weiter?

Neben Coroutinen hat C++20 viele weitere Concurrency-Features anzubieten. Die neuen atomaren Variablen gibt es für Gleitkommazahlen und Smart Pointer. Atomare Variablen erlauben es darüber hinaus, auf Benachrichtigungen zu warten. Zur Koordination von Threads wird C++20 um Sempaphoren, Latches und Barriers erweitert. Zusätzlich wurde std::thread mit std::jthread verbessert. Die Ausführung eines std::jthread kann unterbrochen werden. Zusätzlich ruft ein std::jthread automatisch join in seinem Destruktor auf.