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. statement. - Returned objects can be directly split into variables.
- Working with
std::tuple values is straightforward. 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.