Structured Bindings in C++

Metin Cakircali

3 min read

Introduced in C++17, structured bindings (aka tuple unpacking) are a convenient way to use tuple-like data structures. The idea is to avoid writing multiple assignment statements, and instead, to access individual values in a single statement.

ℹ️
Structured bindings are usable with types that provide iterators or references, such as std::map or std::vector.

So, why care about structured bindings? Here are some reasons:

  • Simplify Code: you can reduce the amount of boilerplate code and focus on the logic that matters.
  • Improve Readability: by assigning values in a concise manner, code becomes easier to read and understand.
  • Reduce Error Prone Code: minimize errors by avoiding multiple assignment statements.

Syntax

The syntax for structured bindings is as follows:

auto&& [var1, var2, ..., varN] = expression;

where,

  • auto is used to deduce the types of the variables,
  • [var1, var2, ..., varN] is a list of variable names,
  • expression is the tuple-like structure from which values are extracted.
ℹ️
Be aware that all of the variables must be specified.

Behind the scenes, the compiler may generate something like the following pseudo code:

auto&& tmp = expression; // compiler generated
decltype(auto) var1 = tmp.get<0>();
decltype(auto) var2 = tmp.get<1>();
decltype(auto) varN = tmp.get<N>();

The compiler generates a unique name for the tmp object. The decltype(auto) preserves the return type of get<>() method (see below). On the other hand, the auto would decay a reference to a copy.

Example: std::map

The scores object is a list of key-value (name-score) pairs. We iterate over the list using two different range-based for loop; with C++14 (item : scores) and with C++17 structured bindings ([name, score] : scores). Obviously, using the structured bindings is more expressive. Using the C++14 ranged-loop, the map iterator returns the <first, second> pair that is less expressive and more error prone.

std::map<std::string, int> scores = {{"Ali", 80}, {"Veli", 90}, {"Deli", 70}};

// Iterate using ranged-loop C++14
for (const auto& item : scores) {
    std::cout << "Name: " << item.first << ", Score: " << item.second << '\n';
}

// Iterate using structured bindings C++17
for (auto&& [name, score] : scores) {
    std::cout << "Name: " << name << ", Score: " << score << '\n';
}

Avoiding copy

Consider the std::map example where the C++14 loop uses reference qualifier on auto but item.first results in a copy. On the other hand, using auto&& results in const std::string& name, which avoids unnecessary copy.

Custom class

#include "Part.h"

#include <iostream>
#include <vector>

int main() {
    std::vector<Part> parts;

    // input some parts
    parts.emplace_back("Bumper", "Body");
    parts.emplace_back("Crankshaft", "Engine");
    parts.emplace_back("Battery", "Electrical");
    parts.emplace_back("Distribution", "Electrical");

    // fix the last part
    auto& [name, type, id] = parts.back();

    name = "Spark Plug";
    type = "Ignition";

    // print the parts
    for (auto&& part : parts) { std::cout << part << std::endl; }

    return 0;
}

PartStore.cc

#pragma once

#include <iostream>
#include <string>
#include <utility>

/**
 * @class Part
 * @brief Represents a part with a name, type, and unique ID.
 */
class Part {
public:
    Part(std::string name, std::string type):
        name_(std::move(name)), type_(std::move(type)), id_(counter++) { }

    auto id() const -> uint64_t { return id_; }
    auto name() const -> const std::string& { return name_; }
    auto type() const -> const std::string& { return type_; }

    template<std::size_t Index>
    auto& get() {
        static_assert(Index < 3, "Index out of bounds");
        if constexpr (Index == 0) { return name_; }
        if constexpr (Index == 1) { return type_; }
        if constexpr (Index == 2) { return id_; }
    }

    template<std::size_t Index>
    auto& get() const {
        static_assert(Index < 3, "Index out of bounds");
        if constexpr (Index == 0) { return name_; }
        if constexpr (Index == 1) { return type_; }
        if constexpr (Index == 2) { return id_; }
    }

private:
    // Overloads the << operator to allow printing a Part object.
    friend std::ostream& operator<<(std::ostream& out, const Part& part) {
        out << '#' << part.id_ << ": " << part.type_ << " - " << part.name_;
        return out;
    }

    std::string name_;  // The name of the part.
    std::string type_;  // The type of the part.
    uint64_t    id_;    // The unique ID of the part.

    // The counter for generating unique IDs.
    static inline uint64_t counter = 0;
};

template<>
struct std::tuple_size<Part> {
    // The number of elements in Part.
    static constexpr const std::size_t value = 3;
};

template<std::size_t Index>
struct std::tuple_element<Index, Part> {
    using type = std::conditional_t<Index == 2, uint64_t, std::string>;
};

Part.h