Lucky Resistor
Menu
  • Home
  • Learn
    • Learn C++
    • Product Photography for Electronics
      • Required Equipment and Software
    • Soldering for Show
  • Projects
  • Libraries
  • Applications
  • Shop
  • About
    • About Me
    • Contact
    • Stay Informed
  •  
Menu

Auto and Structured Binding

Posted on 2019-07-162022-09-04 by Lucky Resistor

This article is just a short follow-up article for “Write Less Code using the ‘auto’ Keyword”. Structured binding is something handy, introduced in C++17. Therefore, only the latest compiler will support it.

If you are mainly write embedded code, you may skip this article, because it will take some years until C++17 support is available for your platform. If you also write desktop code, read on, you may like this feature.

Find a detailed reference about structural binding here. The following sections will explain the feature using several use cases.

Multiple Return Values using Tuples

Let us write a function to convert a string into an integer. For error handling, we like to use parameters or return values, but no exceptions. You may consider these variants:

uint32_t getInt(const std::string &text, bool *ok);
uint32_t getInt(const std::string &text, bool &ok);
bool getInt(const std::string &text, uint32_t &result);

The problem is, if you like to use the full range of the uint32_t, you need a second value to report of the conversion was successful or not. It may be a bool, or even better an enum to report this result.

#include <iostream>
#include <cstdint>
#include <string>
#include <limits>

uint32_t safeMultiplication(uint32_t a, uint32_t b, bool *ok = nullptr) {
    uint32_t result;
    if (__builtin_mul_overflow(a, b, &result)) {
        if (ok != nullptr) {
            *ok = false;
        }
        return 0;
    }
    if (ok != nullptr) {
        *ok = true;
    }
    return result;
}

uint32_t safeAdd(uint32_t a, uint32_t b, bool *ok = nullptr) {
    uint32_t result;
    if (__builtin_add_overflow(a, b, &result)) {
        if (ok != nullptr) {
            *ok = false;
        }
        return 0;
    }
    if (ok != nullptr) {
        *ok = true;
    }
    return result;
}

uint32_t getInt(const std::string &text, bool *ok = nullptr) {
    uint32_t result = 0;
    uint32_t factor = 1;
    bool factorValid = true;
    for (auto it = text.rbegin(); it != text.rend(); ++it) {
        if (*it < '0' || *it > '9') {
            if (ok != nullptr) {
                *ok = false;
            }
            return 0;
        }
        auto value = static_cast<uint32_t>(*it - '0');
        if (value > 0) {
            if (!factorValid) {
                if (ok != nullptr) {
                    *ok = false;
                }
                return 0;
            }
            bool multiplicationOk;
            value = safeMultiplication(factor, value, &multiplicationOk);
            if (!multiplicationOk) {
                if (ok != nullptr) {
                    *ok = false;
                }
                return 0;
            }
            bool additionOk;
            result = safeAdd(result, value, &additionOk);
            if (!additionOk) {
                if (ok != nullptr) {
                    *ok = false;
                }
                return 0;
            }
        }
        if (factorValid) {
            bool multiplicationOk;
            factor = safeMultiplication(factor, 10u, &multiplicationOk);
            if (!multiplicationOk) {
                factorValid = false;
            }
        }
    }
    if (ok != nullptr) {
        *ok = true;
    }
    return result;
}

void testText(const std::string &text) {
    bool ok;
    const auto number = getInt(text, &ok);
    std::cout << text << " -> " << number << " ok: " << (ok ? "yes" : "no") << std::endl;
}

int main(int argc, const char * argv[]) {
    testText("text");
    testText("4294967295");
    testText("4294967296");
    testText("00004294967295");
    testText("x123");
    return 0;
}

The example code works well. For simplicity, it uses two built-in functions __builtin_add_overflow and __builtin_mul_overflow from the GCC and clang compiler.

Using two return values would simplify the interface. It can be done using std::tuple and structured binding.

#include <iostream>
#include <cstdint>
#include <string>
#include <limits>
#include <tuple>

using Result = std::tuple<uint32_t, bool>;

Result safeMultiplication(uint32_t a, uint32_t b) {
    uint32_t result;
    if (__builtin_mul_overflow(a, b, &result)) {
        return std::make_tuple(0, false);
    }
    return std::make_tuple(result, true);
}

Result safeAdd(uint32_t a, uint32_t b) {
    uint32_t result;
    if (__builtin_add_overflow(a, b, &result)) {
        return std::make_tuple(0, false);
    }
    return std::make_tuple(result, true);
}

Result getInt(const std::string &text) {
    uint32_t result = 0;
    uint32_t factor = 1;
    bool factorValid = true;
    for (auto it = text.rbegin(); it != text.rend(); ++it) {
        if (*it < '0' || *it > '9') {
            return std::make_tuple(0, false);
        }
        const auto digitValue = static_cast<uint32_t>(*it - '0');
        if (digitValue > 0) {
            if (!factorValid) {
                return std::make_tuple(0, false);
            }
            const auto[value, multiplicationOk] = safeMultiplication(factor, digitValue);
            if (!multiplicationOk) {
                return std::make_tuple(0, false);
            }
            bool additionOk;
            std::tie(result, additionOk) = safeAdd(result, value);
            if (!additionOk) {
                return std::make_tuple(0, false);
            }
        }
        if (factorValid) {
            bool multiplicationOk;
            std::tie(factor, multiplicationOk) = safeMultiplication(factor, 10u);
            if (!multiplicationOk) {
                factorValid = false;
            }
        }
    }
    return std::make_tuple(result, true);
}

void testText(const std::string &text) {
    const auto[number, ok] = getInt(text);
    std::cout << text << " -> " << number << " ok: " << (ok ? "yes" : "no") << std::endl;
}

int main(int argc, const char * argv[]) {
    testText("text");
    testText("4294967295");
    testText("4294967296");
    testText("00004294967295");
    testText("x123");
    return 0;
}

It simplified the code with just a single downside. While the initial implementation made the ok optional, the new version will get it for each result.

Bind Elements of a Struct

You can bind to members of any struct using the same syntax:

struct Result {
    uint32_t value;
    bool ok;
}

Result result = {10, true}

int main() {
    auto[value, ok] = result;
}

If we use a struct in our example, it simplifies several lines, but requires some additions elsewhere.

#include <iostream>
#include <cstdint>
#include <string>
#include <limits>
#include <tuple>

struct Result {
    uint32_t value;
    bool ok;
    constexpr static Result error() { return {0, false}; }
    constexpr static Result success(uint32_t value) { return {value, true}; }
};

Result safeMultiplication(uint32_t a, uint32_t b) {
    uint32_t result;
    if (__builtin_mul_overflow(a, b, &result)) {
        return Result::error();
    }
    return Result::success(result);
}

Result safeAdd(uint32_t a, uint32_t b) {
    uint32_t result;
    if (__builtin_add_overflow(a, b, &result)) {
        return Result::error();
    }
    return Result::success(result);
}

Result getInt(const std::string &text) {
    uint32_t result = 0;
    uint32_t factor = 1;
    bool factorValid = true;
    for (auto it = text.rbegin(); it != text.rend(); ++it) {
        if (*it < '0' || *it > '9') {
            return Result::error();
        }
        const auto digitValue = static_cast<uint32_t>(*it - '0');
        if (digitValue > 0) {
            if (!factorValid) {
                return Result::error();
            }
            const auto[value, multiplicationOk] = safeMultiplication(factor, digitValue);
            if (!multiplicationOk) {
                return Result::error();
            }
            const auto[immediateResult, additionOk] = safeAdd(result, value);
            if (!additionOk) {
                return Result::error();
            }
            result = immediateResult;
        }
        if (factorValid) {
            const auto[newFactor, multiplicationOk] = safeMultiplication(factor, 10u);
            if (!multiplicationOk) {
                factorValid = false;
            }
            factor = newFactor;
        }
    }
    return Result::success(result);
}

void testText(const std::string &text) {
    const auto[number, ok] = getInt(text);
    std::cout << text << " -> " << number << " ok: " << (ok ? "yes" : "no") << std::endl;
}

int main(int argc, const char * argv[]) {
    testText("text");
    testText("4294967295");
    testText("4294967296");
    testText("00004294967295");
    testText("x123");
    return 0;
}

The struct made the status optional, because we can write a line like this:

const auto value = getInt(text).value;

Or only check if the value is valid:

if (getInt(text).ok) {
    // ...
}

Binding to an Array

Binding values from an array is another interesting use:

#include <iostream>

int table[4][4] = {
    { 1,  2,  3,  4},
    { 5,  6,  7,  8},
    { 9, 10, 11, 12},
    {13, 14, 15, 16},
};

int main(int argc, const char * argv[]) {
    for (const auto &row : table) {
        const auto [a, b, c, d] = row;
        std::cout << a << ", " << b << ", " << c << ", " << d << std::endl;
    }
    return 0;
}

References

I found structural binding also useful creating references to values for actual modification, or const references to avoid copy operations.

#include <iostream>

int table[4][4] = {
    { 1,  2,  3,  4},
    { 5,  6,  7,  8},
    { 9, 10, 11, 12},
    {13, 14, 15, 16},
};

int main(int argc, const char * argv[]) {
    for (auto &row : table) {
        auto& [a, b, c, d] = row;
        std::swap(a, d);
        std::swap(b, c);
    }
    for (const auto &row : table) {
        const auto& [a, b, c, d] = row;
        std::cout << a << ", " << b << ", " << c << ", " << d << std::endl;
    }
    return 0;
}

Conclusion

If you already use a C++17 capable compiler, like the GCC, clang or Visual Studio, try to use structural binding in your project. You can find a table with the current compiler support in this document.

  • It can create cleaner interfaces, where all values are returned with the return statement.
  • Returned objects can be directly split into variables.
  • Working with std::tuple values is straightforward.
  • Array tables can be accessed in a column like way.

If you have questions, miss some information or have any feedback, feel free to add a comment below.

Learn More

Extreme Integers – Doom from Below

Extreme Integers – Doom from Below

As a beginner or immediate C++ programmer, you heard never mixing unsigned and signed integer types or avoiding unsigned integers at all. There was also this talk about undefined behaviour. Yet, in embedded software development, ...
Read More
Guide to Modular Firmware

Guide to Modular Firmware

This article is for embedded software developers with a solid working knowledge of C or C++, but who struggle with large and complex projects. If you learn to develop embedded code, e.g. using the Arduino ...
Read More
Consistent Error Handling

Consistent Error Handling

Error handling in firmware is a difficult task. If you decide to ignore errors, the best you can expect is a deadlock, but you could also damage the hardware. When reviewing existing code, I often ...
Read More
Use Enum with More Class!

Use Enum with More Class!

You may be familiar with enum values, but do you know about enum classes? This great feature was introduced with C++11 to solve several problems with the regular enum declaration. Now I will explain the ...
Read More
C++ Templates for Embedded Code (Part 2)

C++ Templates for Embedded Code (Part 2)

Templates are a powerful feature of the C++ language, but their syntax can be complex. Here I will continue with the second part of the article. Although the examples I provide are for the Arduino ...
Read More
It's Time to Use #pragma once

It’s Time to Use #pragma once

In my opinion, preprocessor macros and the outdated #include mechanism are one of the worst parts of the C++ language. It is not just these things are causing a lot of problems, even more, it ...
Read More

Leave a ReplyCancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Stay Updated / Donate

Join me on Mastodon!

☕️ Support my Work

Top Posts & Pages

  • Real Time Counter and Integer Overflow
  • Storage Boxes System for 3D Print
  • Simple Box with Lid for 3D-Print
  • Logic Gates Puzzles
  • Welcome to My Shop
  • Circle Pattern Generator
  • Use Enum with More Class!
  • Circle Pattern Generator
  • Units of Measurements for Safe C++ Variables
  • How and Why to Avoid Preprocessor Macros

Latest Posts

  • New Version 2 of the Pattern Generator2023-07-25
  • The Importance of Wall Profiles in 3D Printing2023-02-12
  • The Hinges and its Secrets for Perfect PETG Print2023-02-07
  • Better Bridging with Slicer Guides2023-02-04
  • Stronger 3D Printed Parts with Vertical Perimeter Linking2023-02-02
  • Logic Gates Puzzle 1012023-02-02
  • Candlelight Emulation – Complexity with Layering2023-02-01
  • Three Ways to Integrate LED Light Into the Modular Lantern2023-01-29

Categories

  • 3D Printing
  • Build
  • Common
  • Fail
  • Fun
  • Learn
  • Projects
  • Puzzle
  • Recommendations
  • Request for Comments
  • Review
  • Software
Copyright (c)2022 by Lucky Resistor. All rights reserved.