Dear Readers!

Today I’m going to write about a new feature of C++17, a feature that probably has been around in other languages like python already, but I’m happy enough with its inclusion into C++ to make it deserve a post. It is called “structured binding”, or in compilerese “decomposition declaration”, where the former naming is unfortunately not that descriptive unless you already know what it does. Are we binding something structurally or are we binding something to a structure? If it is the latter, what are we binding to the structure? Compiler-talk on the other hand is less murky, as it immediately suggests some sort of declaration through some kind of decomposition, which is exactly what it is going on. Basically this feature enables the user to immediately access public data members of simple structures without the need of creating temporary variables or using the member access operator. Let’s see some examples:

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// ...

struct Lady
{
    std::string maidenName;
    std::string givenName;
    int age;
};

// Pre C++17 style
void listLadiesClassic(const std::vector<Lady>& l) {
    for (auto& lady : l) {
        std::cout << lady.maidenName << ", " << lady.givenName
                    << ": " << lady.age*0.9 << std::endl;
    }
}

// C++17 style
void listLadiesNew(const std::vector<Lady>& l) {
    for (auto& [mn,gn,a] : l) {
        std::cout << mn << ", " << gn
                    << ": " << a*0.9 << std::endl;
    }
}

std::vector<Lady> ladies;

// fill vector with ladies
// ...

listLadiesClassic(ladies);
listLadiesNew(ladies);

In the above example we can see both classic and new style usage of structures and the difference is very clear: with the new notation we don’t anymore need to use a temporary Lady object to access the data members, instead we can directly declare and use them using auto. Also note, that ‘gn’ and ‘a’ have different types (string and int, respectively), meaning that auto is able to handle them both simultaneously. This has two implications:

  • whenever we want to use Lady’s data members, we can type less by omitting the member access operator, just as it can be seen inside the for loop.
  • we don’t need to know the names of the data members to be able to use them.

The second implication is much more important, as it allows better code reuse by decreasing coupling. Not only we decoupled ourselves from types by using auto (which is true for the classic example as well), but since C++17, we can decouple ourselves from the names as well.

Obviously, one would immediately wonder, if this feature only works with structs that don’t contain anything else but data members. Let’s see:

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Gentleman
{
public:
    void resetName() noexcept {surName = ""; givenName = "";}
    void resetAge() noexcept {age = 0;}

    std::string surName;
    std::string givenName;
    int age;
};

// Pre C++17 style
void listGentsClassic(const std::vector<Gentleman>& g) {
    for (auto& gent : g) {
        std::cout << gent.surName << ", " << gent.givenName
                    << ": " << gent.age << std::endl;
    }
}

// C++17 style
void listGentsNew(const std::vector<Gentleman>& g) {
    for (auto& [sn,gn,a] : g) {
        std::cout << sn << ", " << gn
                    << ": " << a << std::endl;
    }
}

std::vector<Gentleman> gents;
// fill vector with gentlemen
// ...

listGentsClassic(gents);
listGentsNew(gents);

This code compiles and runs just the same as the previous one, so the presence of helper functions doesn’t interfere with decomposition declaration as long as the data members remain public. The second implication from the pervious paragraph has also been demonstrated here, as the contents of the new function could remain the same while feeding it a class with slightly differently named data members, while the old stlye function clearly had to be modified. Of course, such naming discrepancy should probably not occur in real world code (and it isn’t the function that you should be modifying if it does occur), but for demonstative purposes it works well. It should also be noted, that declaration decomposition only works on public data members, otherwise the compiler will complain:

error: cannot decompose non-public member ‘Gentleman::surName’ of ‘const Gentleman’

This feature is not restricted to be used inside loops only, it handles return values with struct types as well:

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...

auto findPerfectMatch(const std::vector<Lady>& l, const std::vector<Gentleman>& g)
{
    // logic for finding matches
    // ...
    
    auto perfectMatch = std::make_pair(l[x], g[y]);
    return perfectMatch;
}

// ...

auto [lady,gent] = findPerfectMatch(ladies, gentlemen);
// output operators are assumed
std::cout << "Congratulations to " << lady << and << gent
            << " for finding their significant other!" << std::endl;

A small plus here was the return type of the function, which was set to auto , and structured binding still worked. One could legitimally ask though, if using auto and decomposition declaration too heavily has any negative impact on debugging among other things, so probably just as with all new features, showing some restraint might be prudent.

So today’s lesson is:

Use decomposition declaration (aka. structured binding) if it is available to you and doesn’t hinder clarity of intent.

Thanks for reading.