Case Study: Range Based for On std::map

Gool Ol’ Days: Iteration In C++ < 11

Loud!

  • Cannot simply initialize std::map with content

    • Initialize empty map

    • Fill it in regular code flow

  • Iterators are cool only if one likes pointer arithmetic (otherwise not)

  • Manually unpacking a std::pair is not so cool

#include <gtest/gtest.h>
#include <map>
#include <string>

static void do_something_with(int, const std::string&) { /*...*/ }

TEST(range_based_for_suite, map_pre_11)
{
    using table = std::map<int, std::string>;

    // no initialization in C++<11; have to insert at runtime
    table translations;
    translations.insert(std::make_pair(0, "zero"));
    translations.insert(std::make_pair(1, "one"));
    translations.insert(std::make_pair(2, "two"));

    // iteration using iterator objects (c/v pointer arithmetic)
    for (table::const_iterator it=translations.begin(); it!=translations.end(); ++it) {
        int num = it->first;
        std::string str = it->second;

        do_something_with(num, str);
    }
}

Basic C++11 Iteration: Range Based For, auto, And Initialization

#include <gtest/gtest.h>
#include <map>
#include <string>

static void do_something_with(int, const std::string&) { /*...*/ }

TEST(range_based_for_suite, map_basic)
{
    using table = std::map<int, std::string>;

    table translations {
        {0, "zero"},
        {1, "one"},
        {2, "two"},
    };

    for (auto elem: translations) {
        auto num = elem.first;
        auto str = elem.second;

        do_something_with(num, str);
    }
}

Variation: Preventing Accidental Copy Using const auto&

Using plain auto can be expensive …

for (auto elem: translations) {
    ...
}
  • std::pair<int, std::string> is usually cheap (depends on std::string implementation though)

  • But what if iterating over std::map<int, std::vector<std::string>>?

  • ⟶ Copying a possibly huge std::vector!

#include <gtest/gtest.h>
#include <map>
#include <string>

static void do_something_with(int, const std::string&) { /*...*/ }

TEST(range_based_for_suite, map_basic_const_auto_reference)
{
    using table = std::map<int, std::string>;

    table translations {
        {0, "zero"},
        {1, "one"},
        {2, "two"},
    };

    for (const auto& elem: translations) {
        auto num = elem.first;
        const auto& str = elem.second;

        do_something_with(num, str);
    }
}

Still unwanted though: manual std::pair unpacking

Pythonicity: Tuple Unpacking, err Structured Binding

Wanted: Python’s tuple unpacking, as used in dictionary iteration

translations = { 0: 'zero', 1: 'one', 2: 'two' }
for key, value in translations.items():
    do_something_with(key, value)

Structured binding: simplest usage - by copy

#include <gtest/gtest.h>
#include <map>
#include <string>

static void do_something_with(int, const std::string&) { /*...*/ }

TEST(range_based_for_suite, map_unpack_copy)
{
    using table = std::map<int, std::string>;

    table translations {
        {0, "zero"},
        {1, "one"},
        {2, "two"},
    };

    for (auto [num, str]: translations)
        do_something_with(num, str);
}

Structured Binding - Preventing Accidental Copy

⟶ let’s use references more …

#include <gtest/gtest.h>
#include <map>
#include <string>

static void do_something_with(int, const std::string&) { /*...*/ }

TEST(range_based_for_suite, map_unpack_reference)
{
    using table = std::map<int, std::string>;

    table translations {
        {0, "zero"},
        {1, "one"},
        {2, "two"},
    };

    for (auto& [num, str]: translations)
        do_something_with(num, str);
}
#include <gtest/gtest.h>
#include <map>
#include <string>

static void do_something_with(int, const std::string&) { /*...*/ }

TEST(range_based_for_suite, map_unpack_const_reference)
{
    using table = std::map<int, std::string>;

    table translations {
        {0, "zero"},
        {1, "one"},
        {2, "two"},
    };

    for (const auto& [num, str]: translations)
        do_something_with(num, str);
}