trainings/AdvancedCppV2/Presentation/moder_cpp_cpp20_small_features.md

40 KiB
Raw Permalink Blame History

Small features


operator<=>

Since C++20 we don't need to define all comparison operators for custom structures. Instead of ==, !=, >, <, <=, >= we can simply write one universal comparator <=>.

struct Student {
    auto operator<=>(const Student& other) const = default;

    std::string name_;
    double average_;
    int index_;
};

int main() {
    Student student1{.name_ = "Mateusz", .average_ = 4.67, .index_ = 456123};
    Student student2{.name_ = "Michał", .average_ = 4.57, .index_ = 123456};
    Student student3{.name_ = "Marcelina", .average_ = 4.67, .index_ = 321456};
    Student student4{.name_ = "Mirosław", .average_ = 4.47, .index_ = 135246};

    std::cout << "student1 < student2 ? " << (student1 < student2) << '\n';   // True
    std::cout << "student1 < student3 ? " << (student1 < student3) << '\n';   // False
    std::cout << "student1 < student4 ? " << (student1 < student4) << '\n';   // True
    std::cout << "student2 < student3 ? " << (student2 < student3) << '\n';   // False
    std::cout << "student2 < student4 ? " << (student2 < student4) << '\n';   // True
    std::cout << "student3 < student4 ? " << (student3 < student4) << '\n';   // True
    std::cout << "student1 == student1 ? " << (student1 == student1) << '\n'; // True
}

How it works?

First, we need to understand how the compiler treats the structure Student. This structure has 3 fields: name, average, and index. During the comparison, the first field is to compare if both values are the same, if true we reach a second one, and so on. In other words, first different fields decide which structure is lower.

int main() {
    Student student1{.name_ = "Mateusz", .average_ = 4.67, .index_ = 456123};
    Student student2{.name_ = "Mateusz", .average_ = 4.67, .index_ = 123456};
    Student student3{.name_ = "Mateusz", .average_ = 4.57, .index_ = 321456};
    Student student4{.name_ = "Mateusz", .average_ = 4.57, .index_ = 135246};

    std::cout << "student1 > student2 ? " << (student1 > student2) << '\n';   // T (higher index)
    std::cout << "student1 > student3 ? " << (student1 > student3) << '\n';   // T (higher average)
    std::cout << "student1 > student4 ? " << (student1 > student4) << '\n';   // T (higher average)
    std::cout << "student2 > student3 ? " << (student2 > student3) << '\n';   // T (higher average)
    std::cout << "student2 > student4 ? " << (student2 > student4) << '\n';   // T (higher average)
    std::cout << "student3 > student4 ? " << (student3 > student4) << '\n';   // T (higher index)
    std::cout << "student1 == student1 ? " << (student1 == student1) << '\n'; // T (the same person)
}

But I don't want to compare some fields...

When you don't want to compare one or more fields, you need to write your own comparator.

struct Student {
    std::partial_ordering operator<=>(const Student& other) const {
        if (const auto res = average_ <=> other.average_; res != 0) {
            return res;
        }
        return name_ <=> other.name_;
    }
};

int main() {
    Student student1{.name_ = "Mateusz", .average_ = 4.67, .index_ = 456123};
    Student student2{.name_ = "Mateusz", .average_ = 4.67, .index_ = 123456};
    Student student3{.name_ = "Mateusz", .average_ = 4.57, .index_ = 321456};

    std::cout << "student1 > student2 ? " << (student1 > student2) << '\n';    // T (higher index)
    std::cout << "student1 < student3 ? " << (student1 < student3) << '\n';    // F (lower average)
    // This line do not compile :(
    std::cout << "student1 == student1 ? " << (student1 == student1) << '\n';  // T (the same person)
}

But why do we wrotestd::partial_ordering instead of auto? It's because we compare two floating point values. I know that earlier we wrote auto operator<=> and it's worked, but this was fully generated by the compiler. If we are writing it manually and we need to specify what result we return.


Types of ordering

  • strong_ordering: a total ordering, where equality implies substitutability (that is (a <=> b) == strong_ordering::equal implies that for reasonable functions f, f(a) == f(b). “Reasonable” is deliberately underspecified but shouldnt include functions that return the address of their arguments or do things like return the capacity() of a vector, etc. We want to only look at “salient” properties itself very underspecified, but think of it as referring to the value of a type. The value of a vector is the elements it contains, not its address, etc.). The values are strong_ordering::greater, strong_ordering::equal, and strong_ordering::less.
  • weak_ordering: a total ordering, where equality actually only defines an equivalence class. The canonical example here is case-insensitive string comparison where two objects might be weak_ordering::equivalent but not actually equal (hence the naming change to equivalent).
  • partial_ordering: a partial ordering. Here, in addition to the values greater, equivalent, and less (as with weak_ordering), we also get a new value: unordered. This gives us a way to represent partial orders in the type system: 1.f <=> NaN is partial_ordering::unordered.

Ok but what with operator==

beacuse we have custom operator<=>, which return std::partial_ordering we can't use operator==. We need to provide own operator==. And we can do this by default (compare all fileds) or manually (compare specific fileds).

struct Student {
    std::partial_ordering operator<=>(const Student& other) const {
        if (const auto res = average_ <=> other.average_; res != 0) {
            return res;
        }
        return name_ <=> other.name_;
    }

    bool operator==(const Student& other) const {
        return name_ == other.name_ && average_ == other.average_;
    }

    std::string name_;
    double average_;
    int index_;
};

int main() {
    Student student1{.name_ = "Mateusz", .average_ = 4.67, .index_ = 456123};
    Student student2{.name_ = "Mateusz", .average_ = 4.67, .index_ = 123456};

    std::cout << "student1 > student2 ? " << (student1 > student2) << '\n';    // T (higher index)
    std::cout << "student1 == student1 ? " << (student1 == student1) << '\n';  // T (the same person)
}

We can also decide to use default operator== and this also work!

struct Student {
    std::partial_ordering operator<=>(const Student& other) const {
        if (const auto res = average_ <=> other.average_; res != 0) {
            return res;
        }
        return name_ <=> other.name_;
    }

    bool operator==(const Student&) const = default;

    std::string name_;
    double average_;
    int index_;
};

int main() {
    Student student1{.name_ = "Mateusz", .average_ = 4.67, .index_ = 456123};
    Student student2{.name_ = "Mateusz", .average_ = 4.67, .index_ = 123456};

    std::cout << "student1 > student2 ? " << (student1 > student2) << '\n';    // T (higher index)
    std::cout << "student1 == student1 ? " << (student1 == student1) << '\n';  // T (the same person)
}

More about partial_ordering

To sum up, if we compare 2 floating point values, the result is std::partial_ordering. We can't use operator== in such a case. This makes sense, but when do we also need this type of order? Let's check the below example:

struct Student {
    std::partial_ordering operator<=>(const Student& other) const {
        if (!name_ || !other.name_) {
            return std::partial_ordering::unordered;
        }
        if (const auto res = name_ <=> other.name_; res != 0) {
            return res;
        }
        return index_ <=> other.index_;
    }
    bool operator==(const Student&) const = default;

    std::optional<std::string> name_;
    double average_;
    int index_;
};

int main() {
    Student student1{.name_ = "Mateusz", .average_ = 4.67, .index_ = 456123};
    Student student2{.name_ = std::nullopt, .average_ = 4.67, .index_ = 123456};

    std::cout << "student1 > student2 ? " << (student1 > student2) << '\n';    // F (second student doesn't has name!)
    std::cout << "student1 < student2 ? " << (student1 < student2) << '\n';    // F (second student doesn't has name!)
    std::cout << "student1 != student2 ? " << (student1 != student2) << '\n';  // T (here we use operator== not <=>)
    std::cout << "student1 == student2 ? " << (student1 == student2) << '\n';  // F (here we use operator== not <=>)
    std::cout << "student2 == student2 ? " << (student2 == student2) << '\n';  // T (here we use operator== not <=>)
}

Ok but how <=> exactly works?

Each type or ordering could represent one of those 3 values (partial could also has unordered):

  • std::strong_ordering::less / weak_ordering::less / std::partial_ordering::less
  • std::strong_ordering::equal / weak_ordering::equivalent / std::partial_ordering::equivalent
  • std::strong_ordering::greater / weak_ordering::greater / std::partial_ordering::greater

We can simply compare them:

int val1(1234);
int val2(12345);
auto res = val1 <=> val2;
if (res < 0) 
    std::cout << "val1 < val2" << std::endl;
else if (res == 0) 
    std::cout << "val1 == val2" << std::endl;
else if (res > 0) 
    std::cout << "val1 > val2" << std::endl;

This is implicity generated by compiler. For instance a > b is converted to (a <=> b) > 0


Finally, something about weak ordering

Weak ordering is returned when does not imply substitutability: if a is equivalent to b, f(a) may not be equivalent to f(b), where f denotes a function that reads only comparison-salient state that is accessible via the argument's public const members. In other words, equivalent values may be distinguishable.

struct Student {
    int score() const {
        return std::accumulate(cbegin(grades_), cend(grades_), 0, [](const auto& sum, const auto& pair) { return sum + pair.second; });
    }

    std::weak_ordering operator<=>(const Student& other) const {
        if (const auto res = this->score() <=> other.score(); res != 0) {
            return res;
        }
        return name_ <=> other.name_;
    }

    bool operator==(const Student&) const = default;

    std::string name_;
    std::multimap<std::string, int> grades_;
    int index_;
};

int main() {
    Student student1{.name_ = "Mateusz", .grades_ = {{"Math", 5},{"Bio", 4}}, .index_ = 456123};
    Student student2{.name_ = "Mateusz", .grades_ = {{"Math", 5},{"Eng", 4}}, .index_ = 123456};
    Student student3{.name_ = "Mateusz", .grades_ = {{"Math", 4},{"Phys", 3}}, .index_ = 123456};

    std::cout << "student1 > student2 ? " << (student1 > student2) << '\n';    // F 
    std::cout << "student1 < student2 ? " << (student1 < student2) << '\n';    // F 
    std::cout << "student1 != student2 ? " << (student1 != student2) << '\n';  // T (use operator == here) 
    std::cout << "student1 == student2 ? " << (student1 == student2) << '\n';  // F (use operator == here) 
    std::cout << "student1 > student3 ? " << (student1 > student3) << '\n';    // T (higher grades) 
    std::cout << "student1 < student3 ? " << (student1 < student3) << '\n';    // F 
}

Implicit conversion

std::strong_ordering can be implicitly converted to std::weak_ordering and std::weak_ordering can be implicitly converted to std::partial_ordering.


designated initializers

This is a small feature that you saw a few times in action during these lectures:

struct Student {
    std::string name_;
    std::multimap<std::string, int> grades_;
    int index_;
};

int main() {
    Student student1{.name_ = "Mateusz", .grades_ = {{"Math", 5},{"Bio", 4}}, .index_ = 456123};
    Student student2{.name_ = "Mateusz", .grades_ = {{"Math", 5},{"Eng", 4}}, .index_ = 123456};
    Student student3{.name_ = "Mateusz", .grades_ = {{"Math", 4},{"Phys", 3}}, .index_ = 123456};

Every structure or class with public fields could now be initialized by using the name of a field. This makes code more readable and you don't need to switch a few times between headers and source code to check which field means what.

struct StreamInfo {
    std::string sourceAddress_;
    std::string destinationAddress_;
    uint16_t sourcePort_;
    uint16_t destinationPort_;
    uint16_t vlan_;
};

struct Streamer {
    Streamer(uint16_t sourcePort): sourcePort_(sourcePort) {}

    uint16_t getSourcePort() const { return sourcePort_; }
private:
    uint16_t sourcePort_;
};

int main() {
    std::map<std::string, std::string> ipTables;
    Streamer streamer(6523);
    ipTables.emplace("192.168.0.15", "192.168.0.35");
    ipTables.emplace("192.168.0.16", "192.168.0.42");

    StreamInfo info {
        .sourceAddress_ = "192.168.0.15",
        .destinationAddress_ = ipTables["192.168.0.15"],
        .sourcePort_ = streamer.getSourcePort(),
        .destinationPort_ = 9998,
        .vlan_ = [](){ return 123; }()
    };
}

Atributes

Since C++20 we got 4 more atributes

  • nodiscard("reason") - encourages the compiler to issue a warning if the return value is discarded
  • likely and unlikely - indicates that the compiler should optimize for the case where a path of execution through a statement is more or less likely than any other path of execution
  • no_unique_address - indicates that a non-static data member need not have an address distinct from all other non-static data members of its class

likely and unlikely

Everyone knows well this if-else statements:

if (!vec.empty()) { return vec.front(); }

if (divider != 0) {
    return num / divider;
} else {
    return std::nan("nan");
}

if (ptr) {
    return ptr->doSth();
}

if (name_ == other.name_) {
    return index < other.index_;
} else {
    return name_ < other.name_
}
  • What is common for all of them?

Let's help compiler!

constexpr double pow(double x, int n) noexcept {
    if (n > 0) [[likely]] { return x * pow(x, n - 1); }
    else [[unlikely]] { return 1; }
}

constexpr double pow2(double x, int n) noexcept {
    if (n > 0) { return x * pow(x, n - 1); }
    else { return 1; }
}

int main() {
    auto benchmark = [](auto fun) {
        const auto start = std::chrono::high_resolution_clock::now();
        fun();
        const auto diff = std::chrono::high_resolution_clock::now() - start;
        std::cout << "Time: " << std::fixed << std::setprecision(6) << diff.count() << " ns\n";
    };

    std::vector<double> vec(1'000'000);
    std::iota(begin(vec), end(vec), 1);
    benchmark([&]() { for (auto el : vec) { pow(el, 13); } });
    benchmark([&]() { for (auto el : vec) { pow2(el, 13); } });
    // Time: 59839100 ns
    // Time: 79786800 ns
}

There is more!

int Nwd(int a, int b) {
    while (b != 0) [[likely]] { a = std::exchange(b, a % b); }
    return a;
}

int Nwd2(int a, int b) {
    while (b != 0) { a = std::exchange(b, a % b); }
    return a;
}

int main() {
    auto benchmark = [](auto fun) {
        const auto start = std::chrono::high_resolution_clock::now();
        fun();
        const auto diff = std::chrono::high_resolution_clock::now() - start;
        std::cout << "Time: " << std::fixed << std::setprecision(6) << diff.count() << " ns\n";
    };

    std::vector<double> vec(1'000'000);
    std::iota(begin(vec), end(vec), 1);
    benchmark([&](){ for (auto el : vec) { Nwd(1'000'000, el); } });
    benchmark([&](){ for (auto el : vec) { Nwd2(1'000'000, el); } });
    // Time: 140909100 ns
    // Time: 182572100 ns
}

Ok ok, but what happens when I already use optimization flag?

int main() {
    auto benchmark = [](auto fun) {
        const auto start = std::chrono::high_resolution_clock::now();
        fun();
        const auto diff = std::chrono::high_resolution_clock::now() - start;
        std::cout << "Time: " << std::fixed << std::setprecision(6) << diff.count() << " ns\n";
    };

    std::vector<double> vec(1'000'000);
    std::iota(begin(vec), end(vec), 1);
    int res = 0;
    benchmark([&](){ for (auto el : vec) { res += Nwd(1'000'000, el); } });  // Time: 88765100 ns
    benchmark([&](){ for (auto el : vec) { res += Nwd2(1'000'000, el); } }); // Time: 82797600 ns
    benchmark([&]() { for (auto el : vec) { res += pow(el, 13); } });        // Time: 6002300 ns
    benchmark([&]() { for (auto el : vec) { res += pow2(el, 13); } });       // Time: 8012500 ns

    return res;
}

Wait what?!. When we use -O3 optimization flag we got worse output for likely. It depends on various things, sometimes code run faster sometimes not, but generally, you can't charm the compiler with optimization, but sometimes you can help :)


Use it when it is worth it, and the compiler could have a problem with manual optimization. For instance in 99.99% during response validation, an error occurs when there is no result filed, other cases happen mainly when someone implements wrong behavior and this is eliminated during tests. Ofc you still can always write more predictable if statement at the beginning :) And this is even better option in my opinion.

bool validate(const std::string& str) {
    constexpr char kResult[] = "result:";

    if (str.empty()) [[unlikely]] {
        return false;
    } else if (str.size() > 30) [[unlikely]] {
        return false;
    } else if (str.front() != '{') [[unlikely]] {
        return false;
    } else if (str.back() != '}') [[unlikely]] {
        return false;
    } else if (std::all_of(std::cbegin(str), std::cend(str), [](const char c) { return !std::isalnum(c); })) [[unlikely]] {
        return false;
    } else if (std::search(std::cbegin(str), std::cend(str), std::cbegin(kResult), std::cend(kResult)) ==
               std::cend(str)) [[likely]] {
        return false;
    }

    return true;
}

bool validate2(const std::string& str) {
    constexpr char kResult[] = "result:";

    if (str.empty()) {
        return false;
    } else if (str.size() > 30) {
        return false;
    } else if (str.front() != '{') {
        return false;
    } else if (str.back() != '}') {
        return false;
    } else if (std::all_of(std::cbegin(str), std::cend(str), [](const char c) { return !std::isalnum(c); })) {
        return false;
    } else if (std::search(std::cbegin(str), std::cend(str), std::cbegin(kResult), std::cend(kResult)) ==
               std::cend(str)) {
        return false;
    }

    return true;
}

int main() {
    auto benchmark = [](auto fun) {
        const auto start = std::chrono::high_resolution_clock::now();
        fun();
        const auto diff = std::chrono::high_resolution_clock::now() - start;
        std::cout << "Time: " << std::fixed << std::setprecision(6) << diff.count() << " ns\n";
    };

    int wrong = 0;
    std::vector<std::string> vec(1'000'000, "{result:12345}");
    benchmark([&]() {
        wrong += std::count_if(std::cbegin(vec), std::cend(vec), [](const auto& str) { return validate(str); });
    });
    benchmark([&]() {
        wrong += std::count_if(std::cbegin(vec), std::cend(vec), [](const auto& str) { return validate2(str); });
    });
    // Time: 293222100 ns
    // Time: 376011000 ns
    // 
    // With 03
    // Time: 14437300 ns
    // Time: 21943400 ns

    return wrong;
}

no_unique_address

Allows this data member to be overlapped with other non-static data members or base class subobjects of its class. Generally, we can use it when we have an empty filed in the structure, and we don't want to lost 1 byte to store it.

struct Empty {};

struct Filed {
    int val_;
    double val2_;
    Empty empty_;
};

struct Filed2 {
    int val_;
    double val2_;
    [[no_unique_address]] Empty empty_;
};

int main() {
    static_assert(sizeof(Empty) >= 1);
    std::cout << "sizeof(Filed): " << sizeof(Filed) << '\n';  // 24
    std::cout << "sizeof(Filed): " << sizeof(Filed2) << '\n'; // 16
}

pack-expansion in lmabdas

Since C++20 we can easily move all arguments to lambda. Before C++20 we can do this only for one object, but now it works for packages also.

auto postponeTask(auto&& fun, auto&&... args) {
    return [f = std::move(fun), ... pack = std::move(args)]() {
        return f(pack...);
    };
}

int main() {
    auto task = postponeTask([](const std::string& str, int num) {
        std::cout << str << " | " << num << '\n';
    }, std::string("Ala ma kota"), 42);

    task();
}

If you don't have C++20 (but you have C++17) you can still use the trick with the std::tuple with std::apply method.

template <typename Fun, typename... Args>
auto postponeTask(Fun fun, Args... args) {
    return [fun = std::move(fun), tup = std::make_tuple(std::move(args)...)]() -> decltype(auto) {
        return std::apply([fun = std::move(fun)](auto const&... args) -> decltype(auto) {
            return fun(args...);
        }, tup);
    };
}
int main() {
    auto task = postponeTask([](const std::string& str, int num) {
        std::cout << str << " | " << num << '\n';
    }, std::string("Ala ma kota"), 42);

    task();
}

template syntax for lambdas

Ok, but what if I want to perfect forward arguments, instead of moving them? Since C++20 you can use template syntax!

int main() {
    auto benchmark = []<typename... Args>(auto&& fun, Args... args) {
        const auto now = std::chrono::system_clock::now();
        const auto res = std::move(fun)(std::forward<Args>(args)...);
        const auto then = std::chrono::system_clock::now();
        std::cout << "res: " << res << " | time: " << (then - now).count() << " ns\n";
    };

    benchmark([](int count, int init) -> long long {
        std::vector<int> vec(count);
        std::iota(begin(vec), end(vec), init);
        return std::accumulate(begin(vec), end(vec), 0);
    }, 1'000'000, 50);
}

uniform erasure

Ok let's do sth easier! How many times have you written sth like this :)?

std::vector<int> vec {1,2,3,4,5,6};
vec.erase(std::remove_if(begin(vec), end(vec), [](auto num){ return num & 1; }), vec.end());

std::remove only prepares a vector to actually remove, but in the end, we need to erasure these values. Yes, this is why a lot of ppl hate C++, always complications. For instance we can write sth like this:

std::map<std::string, int> map {{"One", 1}, {"Two", 2}, {"Three", 3}};
map.erase("One");

std::list<std::string> list {"Ala", "ma", "kota"};
list.remove("Ala");

In the case of a list, there is a member function remove, which actually removes an element! But for the map, we have only erase, which also takes a key as a value and removes an element. 3 containers and 3 different behavior. Definitely, newcomers won't like this. The funniest part is that list has remove_if method, but the map doesn't have erase_if ;)


Since C++20 erasing elements is finally unified! We have two options, erase or erase_if. Unfortunately, in C++ there is always an exception to the rule. std::map and std::set (and their hash versions) don't have std::erase overload, because they already have specialized members functions.

    std::vector<int> vec {1,2,3,4,5,6};
    std::erase(vec, 4);
    std::erase_if(vec, [](auto num){ return num & 1; });
    
    std::map<std::string, int> map {{"One", 1}, {"Two", 2}, {"Three", 3}};
    map.erase("One"); // There is no std::erase() for map :/
    std::erase_if(map, [](const auto& pair){ return pair.second == 2; }); // But there is std::erase_if
    
    std::list<std::string> list {"Ala", "ma", "kota"};
    std::erase(list, "ma");
    std::erase_if(list, [](const auto& str){ return str.length() == 3; });

    std::unordered_set<int> set{1,2,3,4,5,6};
    set.erase(4);
    std::erase_if(set, [](auto num){ return num & 1; });

source_loaction

Since C++20 we got a nice functionality to perform easy and efficient logging.

void log(const std::string_view message,
         const std::source_location location = 
               std::source_location::current())
{
    std::cout << "file: "
              << location.file_name() << "("
              << location.line() << ":"
              << location.column() << ") `"
              << location.function_name() << "`: "
              << message << '\n';
}
 
template <typename T> void fun(T x)
{
    log(x);
}
 
int main(int, char*[])
{
    log("Hello world!");
    fun("Hello C++20!");
}
file: prog.cc(24:8) `int main(int, char**)`: Hello world!
file: prog.cc(19:8) `void fun(T) [with T = const char*]`: Hello C++20!

It's easy to rebuild a little this solution and create own logger:

enum LogType {INF, WRN, ERR};

struct Logger {
    Logger(LogType logType, std::source_location location = std::source_location::current()): 
        logType_{logType},
        location_{location} {}

    Logger& operator<<(std::string_view message) {
        const auto time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
        std::ostringstream os;
        os << "[" << toString(logType_) << "]"
           << "(" << std::put_time(std::localtime(&time), "%Y-%m-%d %X") << "): "
           << location_.file_name() << "("
           << location_.line() << ":"
           << location_.column() << ") `"
           << location_.function_name() << "`: "
           << message << '\n';
        file_ << os.str() << std::flush;

        return *this;
    }

private:
    std::string_view toString(LogType logType) {
        switch (logType) {
        case INF: return "INFO";
        case WRN: return "WARNING";
        case ERR: return "ERROR";
        default: return "UNKNOWN!";
        }
    }

    LogType logType_;
    std::source_location location_;
    static std::ofstream file_;
};

std::ofstream Logger::file_("log.txt");

Ok now is time to log sth

int main() {
    Logger(INF) << "Hello!" << "And Hi" << "And Dzien dobry!";
    Logger(WRN) << "Ojej!";
    Logger(ERR) << "Critical!!!" << "UPS!" << "Bad bad!";
}

And in file log.txt we got:

[INFO](2022-05-22 14:24:47): prog.cc(55:15) `int main()`: Hello!
[INFO](2022-05-22 14:24:47): prog.cc(55:15) `int main()`: And Hi
[INFO](2022-05-22 14:24:47): prog.cc(55:15) `int main()`: And Dzien dobry!
[WARNING](2022-05-22 14:24:47): prog.cc(56:15) `int main()`: Ojej!
[ERROR](2022-05-22 14:24:47): prog.cc(57:15) `int main()`: Critical!!!
[ERROR](2022-05-22 14:24:47): prog.cc(57:15) `int main()`: UPS!
[ERROR](2022-05-22 14:24:47): prog.cc(57:15) `int main()`: Bad bad!

bit operations

There is a new header <bit> which allows performing many valuable operations on bits. All functions are declared as constexpr, so there is a big chance that everything will be evaluated at compile time! Finally, you don't need to write an ugly macro. Let's start with the most important one.

std::endian indicates the endianness of all scalar types:

  • If all scalar types are little-endian, std::endian::native equals std::endian::little
  • If all scalar types are big-endian, std::endian::native equals std::endian::big

Corner case platforms are also supported:

  • If all scalar types have sizeof equal to 1, endianness does not matter and all three values, std::endian::little, std::endian::big, and std::endian::native are the same.
  • If the platform uses mixed endian, std::endian::native equals neither std::endian::big nor std::endian::little.
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";
    else std::cout << "mixed-endian\n";
    // Output: little-endian
}

For me very usefull is also popcount, and all countX_Y function, whereX could be l - left, r - right, and Y could be zero or one:

int main() {
    // counts the number of 1 bits in an unsigned integer
    std::cout << std::popcount(0b1010101010101u) << '\n'; // 7

    // counts the number of consecutive 1 bits, starting from the most significant bit
    std::cout << std::countl_one(std::numeric_limits<uint8_t>::max()) << '\n'; // 8 

    // counts the number of consecutive 0 bits, starting from the most significant bit
    std::cout << std::countl_zero(0b111000011111u) << '\n'; // 20 because this is uint32_t

    // counts the number of consecutive 1 bits, starting from the least significant bit
    std::cout << std::countr_one(0b111000011111u) << '\n'; // 5

    // counts the number of consecutive 0 bits, starting from the least significant bit
    std::cout << std::countr_zero(0b111000011100u) << '\n'; // 2
}

Sometimes in the algorithm, we need to know the closest power of 2 for a given number.

int main() {
    for (uint8_t i = 0 ; i < 16 ; ++i) {
        std::cout << "num: " << std::bitset<sizeof(i) << 3>(i)
                  << " | bit_width: " << +std::bit_width(i) 
                  << " | power of 2: " << std::has_single_bit(i)
                  << " | floor: " << +std::bit_floor(i)
                  << " | ceil: " << +std::bit_ceil(i) << '\n';
    }
}
num: 00000000 | bit_width: 0 | power of 2: 0 | floor: 0 | ceil: 1
num: 00000001 | bit_width: 1 | power of 2: 1 | floor: 1 | ceil: 1
num: 00000010 | bit_width: 2 | power of 2: 1 | floor: 2 | ceil: 2
num: 00000011 | bit_width: 2 | power of 2: 0 | floor: 2 | ceil: 4
num: 00000100 | bit_width: 3 | power of 2: 1 | floor: 4 | ceil: 4
num: 00000101 | bit_width: 3 | power of 2: 0 | floor: 4 | ceil: 8
num: 00000110 | bit_width: 3 | power of 2: 0 | floor: 4 | ceil: 8
num: 00000111 | bit_width: 3 | power of 2: 0 | floor: 4 | ceil: 8
num: 00001000 | bit_width: 4 | power of 2: 1 | floor: 8 | ceil: 8
num: 00001001 | bit_width: 4 | power of 2: 0 | floor: 8 | ceil: 16
num: 00001010 | bit_width: 4 | power of 2: 0 | floor: 8 | ceil: 16
num: 00001011 | bit_width: 4 | power of 2: 0 | floor: 8 | ceil: 16
num: 00001100 | bit_width: 4 | power of 2: 0 | floor: 8 | ceil: 16
num: 00001101 | bit_width: 4 | power of 2: 0 | floor: 8 | ceil: 16
num: 00001110 | bit_width: 4 | power of 2: 0 | floor: 8 | ceil: 16
num: 00001111 | bit_width: 4 | power of 2: 0 | floor: 8 | ceil: 16

There is also a rotation left / right. Standard c ++ 23 introduced also byteswap to convert variables from little endian to big endian and vice versa.

uint16_t num = 0b1111001100111001;
std::cout << "num: " << std::bitset<16>(num) << "\n\n";
for (uint8_t i = 0 ; i < 16 ; ++i) {
    std::cout << "rotr: " << std::bitset<16>(std::rotr(num, i))
                << " | rotl: " << std::bitset<16>(std::rotl(num, i))
                << '\n';
}
num: 1111001100111001

rotr: 1111001100111001 | rotl: 1111001100111001
rotr: 1111100110011100 | rotl: 1110011001110011
rotr: 0111110011001110 | rotl: 1100110011100111
rotr: 0011111001100111 | rotl: 1001100111001111
rotr: 1001111100110011 | rotl: 0011001110011111
rotr: 1100111110011001 | rotl: 0110011100111110
rotr: 1110011111001100 | rotl: 1100111001111100
rotr: 0111001111100110 | rotl: 1001110011111001
rotr: 0011100111110011 | rotl: 0011100111110011
rotr: 1001110011111001 | rotl: 0111001111100110
rotr: 1100111001111100 | rotl: 1110011111001100
rotr: 0110011100111110 | rotl: 1100111110011001
rotr: 0011001110011111 | rotl: 1001111100110011
rotr: 1001100111001111 | rotl: 0011111001100111
rotr: 1100110011100111 | rotl: 0111110011001110
rotr: 1110011001110011 | rotl: 1111100110011100

format your string like in printf

The main problem with std::cout is a problem with easy formatting (I know streams are also inefficient, but this is not a point right now). For instance:

int main() {
    for (uint8_t i = 0 ; i < 16 ; ++i) {
        std::cout << "num: " << std::bitset<sizeof(i) << 3>(i)
                  << " | bit_width: " << +std::bit_width(i) 
                  << " | power of 2: " << std::has_single_bit(i)
                  << " | floor: " << +std::bit_floor(i)
                  << " | ceil: " << +std::bit_ceil(i) << '\n';
    }
}

With <format> library now we can write:

for (uint8_t i = 0; i < 16; ++i) {
    std::cout << std::format("num: {} | bit_width: {} | power of 2: {} | floor: {} | ceil {}\n",
                            std::bitset<sizeof(i) << 3>(i).to_string(),
                            std::bit_width(i),
                            std::has_single_bit(i),
                            std::bit_floor(i),
                            std::bit_ceil(i));
}

On day 25.05.2022 the newest gcc 13 didn't support the library <format>. Fortunately clang 15 already supports it! <Format> library is very powerful, we can easily create any format we want and pass any values we need:

void raw_write_to_log(std::string_view users_fmt, std::format_args&& args) {
    constinit static int line{};
    std::clog << std::format("{:04} : ", line++) << std::vformat(users_fmt, args) << '\n';
}
 
template <typename... Args>
constexpr void log(Args&&... args) {
    // Generate formatting string "{} "...
    std::array<char, sizeof...(Args) * 3 + 1> braces{};
    constexpr const char c[] = "{} ";
    for (auto i{0u}; i != braces.size() - 1; ++i) {
        braces[i] = c[i % 3];
    }
    braces.back() = '\0';
 
    raw_write_to_log(braces.data(), std::make_format_args(std::forward<Args>(args)...));
}

int main()
{
    std::string str{"Printable"};
    log("You", "Can", "Pass", "Any", "Number", "Of", "Arguments");
    log("Everything", "Which", "Can", "Be", str);
    log(1, 4.5, 1234);
}
0000 : You Can Pass Any Number Of Arguments 
0001 : Everything Which Can Be Printable 
0002 : 1 4.5 1234 

Limitations

We need to wait to specify how we should specify custom object formatting. Currently on cppreference we can see an example, but looking very ugly and don't compile on clang 15.

#include <format>
#include <iostream>
 
// A wrapper for type T
template<class T>
struct Box {
    T value;
};
 
// The wrapper Box<T> can be formatted using the format specification of the wrapped value
template<class T, class CharT>
struct std::formatter<Box<T>, CharT> : std::formatter<T, CharT> {
    // parse() is inherited from the base class
 
    // Define format() by calling the base class implementation with the wrapped value
    template<class FormatContext>
    auto format(Box<T> t, FormatContext& fc) const {
        return std::formatter<T, CharT>::format(t.value, fc);
    }
};
 
int main() {
    Box<int> v = { 42 };
    std::cout << std::format("{:#x}", v);
}

But to make you more interested in this library check out more possibilities:

int main()
{
    std::string buffer;
    
    std::format_to(
        std::back_inserter(buffer),
        "Hello, C++{}!\n", // formater
        "20",   // args
        "More args, make no error :)");

    std::cout << buffer << '\n';
}
Hello, C++20!