Сегодня мы собираемся добавить в наш набор инструментов индексированные циклы for.
В частности, нас интересует эквивалент функции enumerate() в Python, которую можно использовать следующим образом:

colors = ['yellow', 'blue', 'green', 'magenta']
for i, color in enumerate(colors):
    print(i, color)

# Output:
# 0 yellow
# 1 blue
# 2 green
# 3 magenta

С парой функций, представленных в C++17 и C++20, мы можем добиться синтаксиса, довольно близкого к приведенному выше в C++:

const std::vector colors = {"yellow", "blue", "green", "magenta"};
for(const auto& [i, color] : enumerate(colors))
  std::cout << i << ": " << color<< '\n';

Конечно, здесь больше информации о шрифте и пара дополнительных символов для украшения, но это не так уж и далеко и довольно выразительно. Так как же части работают вместе?

На самом деле требуется всего две вещи, первая из которых выделяется прямо в самом цикле: const auto& [i, color]. Это структурированное связывание, позволяющее брать кортежеподобные конструкции и деструктурировать их на отдельные части.

Зная это, можете ли вы догадаться, что делает перечисление? Он принимает вектор строк (технически строковых литералов) и должен возвращать кортежеподобный тип, состоящий из двух частей: индекса и значения итерируемого элемента (в данном случае std::pair).

Таким образом, мы могли бы, конечно, создать вектор пар или, например. карта из входных данных, но С++ 20 предлагает более элегантный подход с использованием новых адаптеров диапазона, который выглядит следующим образом:

auto enumerate(const auto& data) {
    return data | std::views::transform([i = 0](const auto& value) mutable {
        return std::make_pair(i++, value);
    });
}

Вот и все! Очень аккуратно и лаконично. Тем не менее, это довольно плотно, так что давайте пройдемся по нему. enumerate(...) — это шаблонная функция с выведенным типом возвращаемого значения, которая принимает один параметр, который напрямую подключается к представлению преобразования. Преобразование состоит из лямбды, превращающей любое значение (здесь наши строковые литералы) в std::pair этого значения и его индекса. Индекс инициализируется 0 в лямбда-захвате и увеличивается для каждого значения. Чтобы это работало, нам нужно добавить ключевое слово mutable в лямбду, так как в противном случае его operator() было бы константой.

Важно понимать, что на самом деле это не будет повторяться по нашему списку дважды. Адаптер диапазона лениво оценивается только непосредственно перед тем, как каждый элемент передается нам во время нашего цикла for-each.

Как вы думаете, нравится ли вам такой подход или у вас есть другие предложения, которые вы предпочитаете? Вы бы рассмотрели возможность добавления этой утилиты в свою кодовую базу или просто добавили бы дополнительный счетчик в свой цикл?

Здесь вы можете найти весь фрагмент в Compiler Explorer, чтобы поэкспериментировать и сравнить его с другими решениями. Как вариант, скопируйте его снизу.

#include <iostream>
#include <ranges>
#include <vector>

auto enumerate(const auto& data) {
    return data | std::views::transform([i = 0](const auto& value) mutable {
        return std::make_pair(i++, value);
    });
}

int main() {
    const auto colors = {"yellow", "blue", "green", "magenta"};
    for (const auto& [i, color] : enumerate(colors)) {
        std::cout << i << ": " << color << '\n';
    }
}

Бонусное упражнение (для начинающих)

Перечисление Python фактически принимает дополнительный необязательный параметр, который позволяет нам установить начальный индекс счетчика. Попробуйте адаптировать фрагмент для поддержки этого.

Бонусное упражнение (средний уровень)

Какие дополнительные улучшения вы можете предложить? Учитывайте удобочитаемость, эффективность и выразительность. Попробуйте подключить различные типы данных к enumerate. Все они работают? Каковы требования к типу для его работы? Получаете ли вы осмысленные сообщения об ошибках? Можете ли вы улучшить их, используя концептуальные ограничения?

Бонусное упражнение (продвинутое)

Предлагаемая функция перечисления не работает с временными значениями. Можете ли вы придумать решение, которое делает?

Заключительные замечания

Я хотел бы начать небольшую серию о крошечных утилитах, которые часто включаются в другие языки «из коробки», но недоступны в C++. Один из примеров — индексированные циклы for, другой — утилита разделения строк. Я не только думаю, что они полезны сами по себе, но, кроме того, многому можно научиться, внедряя и обсуждая их. У меня есть еще пара примеров в бэклоге, но был бы очень заинтересован в вашем личном опыте. Многие разработчики не начинали свою карьеру с C++, а перешли с другого языка. Были ли вы удивлены особыми тонкостями, которые вы обычно использовали, но которых не было в C++? Или, может быть, вы пошли решать проблему на C++, которую, как вы ожидали, будет простым вызовом библиотеки, и в итоге реализовали крошечную утилиту для себя, поскольку узнали, что она не включена. Я хотел бы услышать о них и, возможно, рассказать о некоторых интересных в будущих постах. Если у вас есть пример, не стесняйтесь обращаться ко мне по адресу [email protected].

Кроме того, я постараюсь уточнить уровень пояснений фрагментов кода в зависимости от аудитории. Должен быть баланс между объяснением основ C++ и описанием передовых методов. Вы можете помочь мне в этом, прокомментировав части, которые я пропустил слишком быстро или которые вы не совсем поняли. Для более сложных концепций, которые я не считаю базовыми знаниями, но которые также не умещаются в рамках одного поста, я постараюсь оставить ссылки на Cppreference или другие объяснения, которые я нашел полезными в прошлом.

Спасибо за чтение!

Батарейки может и не быть в комплекте, но кто мешает создать свою?