С момента моей прошлой публикации состоялось уже две встречи международного комитета по стандартизации C++.

Комитет занимался полировкой C++23:
  • static operator[];
  • static constexpr в constexpr-функциях;
  • безопасный range-based for;
  • взаимодействие std::print с другими консольными выводами;
  • монадический интерфейс для std::expected;
  • static_assert(false) и прочее.

И прорабатывал новые фичи C++26:
  • std::get и std::tuple_size для агрегатов;
  • #embed;
  • получение std::stacktrace из исключений;
  • stackful-корутины.


C++23


static operator[]


Прошлым летом в C++23 добавили static operator() и внедрили возможность определять operator[] для нескольких аргументов. Следующий шаг напрашивался сам собой: сделать равные возможности этим операторам, а именно — добавить возможность писать static operator[].

enum class Color { red, green, blue };

struct kEnumToStringViewBimap {
  static constexpr std::string_view operator[](Color color) noexcept {
    switch(color) {
    case Color::red: return "red";
    case Color::green: return "green";
    case Color::blue: return "blue";
    }
  }

  static constexpr Color operator[](std::string_view color) noexcept {
    if (color == "red") {
      return Color::red;
    } else if (color == "green") {
      return Color::green;
    } else if (color == "blue") {
      return Color::blue;
    }
  }
};

// ...
assert(kEnumToStringViewBimap{}["red"] == Color::red);

А это точно эффективный код для преобразования строки в enum?
Может оказаться неожиданным, но этот код и правда очень эффективный. Подобным подходом пользуются разработчики компиляторов. Ну и мы во фреймворке userver подобный подход свели к отдельному классу utils::TrivialBiMap с более удобным описанием:

constexpr utils::TrivialBiMap kEnumToStringViewBimap = [](auto selector) {
  return selector()
      .Case("red", Color::red)
      .Case("green", Color::green)
      .Case("blue", Color::blue);
};

Большая эффективность достигается благодаря особенностям работы современных оптимизирующих компиляторов, однако надо быть крайне внимательным при написании обобщённого решения. Мы готовим отдельный рассказ про этот подход — приходите на C++Russia.

Все немногочисленные детали описаны в предложении P2589R1.

static constexpr в constexpr-функциях


C++23 обзавёлся constexpr to_chars/from_chars. Однако при реализации этой новинки столкнулись с проблемой: различные массивы констант для быстрых преобразований строка<>число в некоторых стандартных библиотеках были объявлены как статические переменные внутри функций, а их нельзя использовать в constexpr-функциях. Разумеется, проблему можно обойти, но обходные пути выглядели криво.

В итоге комитет разрешил использовать static constexpr-переменные внутри constexpr-функций в P2647R1. Мелочь, а приятно.

Безопасный range-based for


Это, пожалуй, самая большая новость и радость последних двух встреч!

Но начнём с загадки. Какой баг спрятался в коде:

class SomeData {
 public:
  // ...
  const std::vector<int>& Get() const { return data_; }
 private:
  std::vector<int> data_;
};

SomeData Foo();

int main() {
  for (int v: Foo().Get()) {
    std::cout << v << ',';
  }
}

Отгадка
Функция Foo() возвращает временный объект, вызов метода Get() возвращает ссылку на данные внутри этого временного объекта, а весь range based for преобразовывается в конструкцию вида:
    auto && __range = Foo().Get() ;
    for (auto __begin = __range.begin(), __end = __range.end(); __begin != __end; ++__begin)
    {
        int v = *__begin;
        std::cout << v << ',';
    }

Здесь auto && __range = Foo().Get() ; эквивалентен const std::vector<int>& __range = Foo().Get() ;. В итоге получаем висящую ссылку.

Под капотом у range based for происходит достаточно много всего, поэтому подобные баги неочевидны. Конечно, тесты с санитайзерами отлавливают такое достаточно эффективно — благо во всех современных проектах они включены и используются (мы в Яндексе не исключение). Но хотелось бы, чтобы подобные баги вообще не возникали.

Первую попытку поправить положение дел мы предприняли аж четыре года назад в РГ21 (подробности в D0890R0), но процесс заглох на этапе обсуждения. К счастью, инициативу подхватил Nicolai Josuttis и теперь в C++23 подобный код не порождает висящую ссылку: все объекты, которые создаются справа от : в range based for теперь уничтожаются только по выходу из цикла.

Технические детали можно найти в документе P2718R0.

std::print


Совсем маленькая новость: в C++23 потюнили std::print, чтобы его вывод синхронизировался с другими выводами данных. На практике для современных операционных систем ничего не изменится, но теперь в стандарте есть гарантия, что на консоль будут выводиться сообщения именно в том порядке, который задан в исходном коде:

printf("first");
std::print("второе");


Монадический интерфейс для std::expected


В последний момент в C++23 пролезла достаточно большая правка: для std::expected добавили монадический интерфейс по аналогии с монадическим интерфейсом для std::optional.

using std::chrono::system_clock;
std::expected<system_clock, std::string> from_iso_str(std::string_view time);
std::expected<formats::bson::Timestamp, std::string> to_bson(system_clock time);
std::expected<int, std::string> insert_into_db(formats::bson::Timestamp time);

// Где-то в коде приложения...
from_iso_str(input_data)
    .and_then(&to_bson)
    .and_then(&insert_into_db)
    // Выкинет исключение Exception, если один из прошлых шагов завершился ошибкой
    .transform_error([](std::string_view error) -> std::string_view {
        throw Exception(error);
    })
;

Полное описание всех монадических интерфейсов std::expected можно найти в документе P2505R5.

static_assert(false) и прочее


Помимо вышеперечисленных ощутимых изменений, как всегда было внесено огромное количество правок, призванных убрать небольшие шероховатости и улучшить повседневную разработку.

Так были добавлены форматеры для std::thread::id и std::stacktrace (P2693), чтобы с ними можно было работать через std::print и std::format.

std::start_lifetime_as обзавёлся дополнительными проверками времени компиляции в p2679.

static_assert(false) в шаблонных функциях перестал срабатывать без инстанцирования функции. Теперь подобный код…

template <class T>
int foo() {
    if constexpr (std::is_same_v<T, int>) {
      return 42;
    } else if constexpr (std::is_same_v<T, float>) {
      return 24;
    } else {
      static_assert(false, "T should be an int or a float");
    }
}

… компилируется и выдаёт диагностику только при условии, если передали неправильный тип данных.

Также приняли бесчисленное количество улучшений для ranges, самое крупное из которых — добавление std::views::enumerate в P2164:

#include <ranges>

constexpr std::string_view days[] = {
    "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
};

for(const auto & [index, value]: std::views::enumerate(days)) {
    print("{} {} \n", index, value);
}


C++26


std::get и std::tuple_size для агрегатов


Есть одна идея по улучшению C++, которой мы уже активно пользуемся в Yandex.Go и фреймворке userver. Она доступна всем желающим благодаря Boost.PFR.

Если вы пишете обобщённую шаблонную библиотеку, то вам, скорее всего, пригодятся std::tuple и std::pair. Вот только с ними есть проблемы.

Во-первых, код с ними получается плохо читаемым: у полей нет понятных имён, поэтому сложновато догадаться, что такое std::get<0>(tuple). Возможно, пользователи вашей библиотеки не захотят работать с ними напрямую в своём коде, поэтому будут создавать объекты этих типов прямо перед вызовом ваших методов. А это может быть не эффективно из-за копирования данных.

Во-вторых, std::tuple и std::pair не пробрасывают тривиальность хранимых в них типов. Соответсвенно, при передаче и возврате std::tuple и std::pair из функций компилятор может генерировать менее эффективный код.

Описанных выше недостатков лишены агрегаты — структуры с публичными полями и без специальных функций.

Идея из P2141R0 от РГ21 как раз в том, чтобы позволить использовать агрегаты в обобщённом коде. Для этого нужно лишь сделать так, чтобы std::get и std::tuple_size работали с ними. Тогда пользователи смогут сразу передавать свои структуры в вашу обобщённую библиотеку без лишних копирований.

Идея была хорошо встречена в комитете — будем прорабатывать тесты и устранять шероховатости.

#embed


Сейчас активно идёт работа над новым стандартом языка C (без ++, тот что без классов). В новый стандарт добавляют множество полезных вещей, которые уже давно были в C++, например, nullptr, auto, constexpr, static_assert, thread_local, [[noreturn]]), так и совершенно новые для C++ фичи. Так вот: некоторые новые для C++ фичи планируется портировать из C в C++26.

Одна из таких новинок — #embed. Это препроцессорная директива для подстановки содержимого файла в качестве массива на этапе компиляции.

const std::byte icon_display_data[] = {
    #embed "art.png"
};

Осталось утрясти небольшие детали. Полное описание идеи доступно в P1967.

Получение std::stacktrace из исключений


С идеей P2370 от РГ21 нас ждал неожиданный провал.

Возможность получать стектрейс из исключения есть в большинстве языков программирования. Этот механизм весьма удобен и позволяет вместо малоинформативных ошибок Caught exception: map::at получать красивую и понятную диагностику:

Caught exception: map::at, trace:
0# get_data_from_config(std::string_view) at /home/axolm/basic.cpp:600
1# bar(std::string_view) at /home/axolm/basic.cpp:6
2# main at /home/axolm/basic.cpp:17

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

Вот только международному комитету идея не зашла. Будем разбираться, что именно смутило людей и улучшать наше предложение.

Stackful-корутины


Подходит к завершению многолетний труд по добавлению базовой поддержки stackful-корутин в стандарт C++ P0876.

Мы затрагивали тему stackful- и stackless-корутин в статье «Анатомия асинхронных фреймворков в С++ и других языках», но кажется, что надо подробнее расписать плюсы и минусы.

Stackless-корутины требуют поддержки от компилятора, их невозможно реализовать своими силами в виде библиотеки. Stackful-корутины реализуются самостоятельно, например, Boost.Context.

Первые позволяют более экономно аллоцировать память. Потенциально они лучше оптимизируются компилятором, у них есть возможность быстро уничтожаться и они доступны в C++20.

Вторые намного проще внедрять в имеющиеся проекты, потому что они не требуют переписывания всего проекта на новую идиому в отличие от stackless-корутин. Фактически они полностью скрывают детали реализации от пользователя, позволяя писать простой линейный код, который под капотом будет асинхронным.

stackless stackful
auto data = co_await socket.receive();
process(data);
co_await socket.send(data);
co_return; // Требует, чтобы функция
    // возвращала особый тип данных

auto data = socket.receive();
process(data);
socket.send(data);

P0876 уже побывал в подгруппе ядра. По итогам обсуждения было решено запретить миграции таких корутин между потоками выполнения. Основная причина такого запрета — компиляторы. Они оптимизируют доступ к TLS и кэшируют значение TLS-переменных:

thread_local int i = 0;
// ...
++i;
foo();  // Со stackful-корутинами может переключить поток выполнения
assert(i > 0);  // Компилятор сохранил адрес в регистре, мы работаем с TLS другого потока


Итоги


Итак, свершилось! C++23 отправлен в вышестоящие инстанции ISO, где в течение полугода будет утверждён и опубликован в виде полноценного стандарта.

А работа над C++26 идёт полным ходом! Есть неплохие шансы увидеть Executors, Networking, Pattern Matching и статическую рефлексию. Если у вас есть хорошие идеи, как сделать C++ ещё лучше, пожалуйста, делитесь ими. А ещё лучше — попробуйте написать proposal со своей идеей. Мы с радостью вам поможем!

Следить за новостями C++ и обсуждать вопросы также можно в telegram-каналах Pro.Cxx и C++ Zero Cost Conf. Ну и на сам C++ Zero Cost Conf мы уже начали отбирать доклады, приходите и расскажите как вы используете C++.

Комментарии (34)


  1. abby
    00.00.0000 00:00
    +1

    const std::vector<int>& __range = Foo().Get() ;. В итоге получаем висящую ссылку.

    Вроде, тут нет проблем, объект будет удалён, когда закончится scope для __range.

    Невнимательно посмотрел, действительно баг.

    Просто поделюсь хорошей новостью по поводу #embed, наконец-то в Visual C++ убрали ограничение на длину строки, правда пока все обновятся... https://learn.microsoft.com/en-us/cpp/cpp/string-and-character-literals-cpp?view=msvc-170#size-of-string-literals

    In versions of Visual Studio before Visual Studio 2022 version 17.0, the maximum length of a string literal is 65,535 bytes. This limit applies to both narrow string literals and wide string literals. In Visual Studio 2022 version 17.0 and later, this restriction is lifted and string length is limited by available resources.

    И на всякий случай https://learn.microsoft.com/en-us/cpp/c-language/maximum-string-length?view=msvc-170


  1. Autochthon
    00.00.0000 00:00
    +11

    А это точно эффективный код для преобразования строки в enum?

    Встроенный в язык способ преобразования строк в enum появится когда-нибудь? switch по строкам, опережающе описание для встроенных классов?

    Чего еще давно не хватает - типизированного указателя аналога void* для полиморфных объектов. какой-нибудь std::object* чтобы к нему можно было привести указатель на любой полиморфный объект и в дальнейшем безопасно сделать dynamic_cast


    1. antoshkka Автор
      00.00.0000 00:00
      +2

      Встроенный в язык способ преобразования строк в enum ожидается от статической рефлексии. Предложения по ней рассматриваются с повышенным приоритетом, все надёются увидеть рефлексию в C++26 (но лично я сомневаюсь, что успеем).

      А std::any вам не подойдёт вместо std::object* ? Работает даже и не с полиморфными объектами


      1. Autochthon
        00.00.0000 00:00
        +1

        any_cast приводит только к тому типу который хранится в any, dynamic_cast к любому в дереве наследования. Обычный POD тип и полиморфный в С++ - фундаментально разные вещи. Почему до сих нет указателя на полиморфный тип непонятно. Получается как в том анекдоте про вовочку ж..а есть а слова нет.


        1. antoshkka Автор
          00.00.0000 00:00

          А для каких именно случаев вам нужен такой указатель? Приведите пожалуйста примеры


          1. Autochthon
            00.00.0000 00:00
            +3

            В программах с GUI происходит регулярное стирание и востановление типа. В обработчиках событий и функциях типа GetSelectedObject. std::any в этом случае не пройдет т.к. в дальнейшем "обезличенные" типы опрашиваются на наследование от определенных типов/интерфейсов.


      1. Kelbon
        00.00.0000 00:00
        +2

        Типичный пример такого стирания типов:
        function_ref<R(Args...)>;
        и логика близкая к string_view вместо string - не нужно создавать временный объект, без каких либо аллокаций происходит эффективное стирание типа и допустим вы имеете в хедере сигнатуру
        void foo(function_ref<int(float)>);
        Вы не создаёте кучу инстанцирований функции foo, можете вынести реализацию в .cpp, к тому же это хорошо инлайнится(так как нет аллокаций)
        Другой пример - std::make_format_args, которое стирает типы. Это можно представить как
        print(fmt_str, any_printable::const_ref...);
        да и вообще любые использования Interface* / & это как раз то самое стирание.

        Опять же, допустим есть значение типа X и его нужно передать в функцию foo(Interface&);, даже в случае с виртуальными функциями можно передать обычную ссылку(если X поддерживает Interface), не создавая ничего лишнего. И хочется иметь такую возможность для типов не поддерживающих явно какие-то виртуальные интерфейсы


        Ну и библиотеки делающие это есть... https://github.com/kelbon/AnyAny


        1. antoshkka Автор
          00.00.0000 00:00
          +2

          Звучит интересно! Готовы помочь с написанием предложения, но готовьтесь к тому что будет тяжко принять такое в стандарт...


        1. antoshkka Автор
          00.00.0000 00:00
          +2

          В виде библиотеки принять в стандарт будет попроще :)


          1. Kelbon
            00.00.0000 00:00

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

            Пока я пытаюсь добавить поддержку в кланг(минимальную хотя бы), просто в качестве демонстрации


    1. Kelbon
      00.00.0000 00:00
      +1

      https://github.com/kelbon/AnyAny
      вот что вы ищете


    1. cdriper
      00.00.0000 00:00
      +1

      про enum вы, наверное, в курсе

      https://github.com/Neargye/magic_enum


      1. neit_kas
        00.00.0000 00:00

        С ним ней не всё так хорошо. У нас например в проекте есть здоровый enum, который представляет ID на какую-либо строку. Этот enum генерируется другим инструментом вместе с локализациями. И тут две проблемы:

        1. ID там достигают значений порядка 10000 (количество реальных значений в enum меньше, остальные берутся другими способами). И вот, перелопатить это невнятное количество в compile time (а именно это и делает эта библиотека) - серьёзное увеличение времени компиляции, а ещё скорее всего по количеству итерации свалится.

        2. Генератор этого enum по ряду причин не может нам дать максимальный ID. Поэтому не понятно, как задавать пределы (библиотека их требует).


  1. buldo
    00.00.0000 00:00

    А есть какие-то планы на управление зависимостями? Или считается, что это не часть языка, поэтому такое не рассматривают?


    1. antoshkka Автор
      00.00.0000 00:00
      +1

      Работа идёт, и она в приоритете для C++26.

      Сейчас комитет отошёл от идеи стандартизировать какой-то имеющийся пакетный менеджер или систему сборки. Вместо этого сосредоточились на создании стандартного языка описания зависимостей и сборки. Чтобы с таким файлом описания вы могли собрать проект и с помощью cmake, и с помощью basel, и с помощью conan...


  1. eptr
    00.00.0000 00:00
    +4

    Безопасный range-based for

    В качестве компромисса, для того, чтобы обезопасить range-based for, можно использовать редко применяемую возможность указать вид ссылочности this:

    class SomeData {
     public:
      // ...
      const std::vector<int>& Get() const & { return data_; }
      const std::vector<int> Get() const && { return data_; }
      std::vector<int> Get() && { return std::move(data_); }
     private:
      std::vector<int> data_;
    };

    Компромисс, потому что теперь "не висит", но — за счёт накладных расходов на copy&move.

    Что интересно: если бы поле data_ не было бы private, то в следующем коде висящей ссылки не возникло бы:

      for (int v: Foo().data_) {
        std::cout << v << ',';
      }

    Жаль, что метод не позволяет "пробросить" свойство поля, если им инициализирована ссылка, продлевать жизнь всего объекта на время жизни ссылки.


  1. firehacker
    00.00.0000 00:00

    #embed это для тех, кто не научился в линковку?


    1. antoshkka Автор
      00.00.0000 00:00
      +3

      А ещё для тех, у кого платформа не предоставляет удобных механизмов для прилинковвывания произвольных файлов.

      А ещё результат #embed доступен в constexpr, так что можно включать разлтчные таблицы констант и делать compile time вычисления над ними.


      1. firehacker
        00.00.0000 00:00

        у кого платформа не предоставляет

        В каком смысле «платформа»? Как такового механизма прилинковывания произвольных файлов и не должно существовать. Линкер должен давать возможность скормить ему на вход произвольное число произвольных объектных файлов — не важно какими путями на свет порожденных. И линкеры базово эту возможность имеют. Остается только найти или написать тривиальный инструмент, который генерирует объектный файл на базе произвольного файла.

        Ну, с compile-time вычислениями хоть какой-то смысл прослеживается. Но опять же, правильный подход — это написать утилиту, которая получив на вход бинарный файл с данными на выходе выплюнет текстовое представление этих данных, которое потом будет просто #includeиться. И никакое #embed не нужно будет, потому что только подобная кастомная утилита знает, как интерпретировать входной бинарник. Может там массив байтов? А может массив слов? А может массив массив 64-битных целых чисел? А может массив 64-битных чисел с плавающей точкой? А какой endianness? А может там массив структур?


        1. fshp
          00.00.0000 00:00
          +4

          Из объектного файла пропадет информация о размере массива.

          Утилиты такие есть. Например ресурсы в Qt.

          Однако идея описывать ресурсы на голом языке мне нравится больше.

          потому что только подобная кастомная утилита знает, как интерпретировать входной бинарник. Может там массив байтов? А может массив слов? А может массив массив 64-битных целых чисел? А может массив 64-битных чисел с плавающей точкой?

          Все перечисленное это все равно массив байт. И нет, утилита ничего не должна знать что там хранится и в каком порядке байт. Ей это не нужно. Это нужно функциям, работающим с этими данными.


          1. firehacker
            00.00.0000 00:00
            +3

            Из объектного файла пропадет информация о размере массива.

            Какой смысл вы вкладываете в словосочетание «из объектного файла пропадает»? Лежит на диске объектный файл, там эта информация есть, и вдруг она внезапно пропадает оттуда — объектный файл по прежнему лежит там, где лежал, но информации о размере там уже нет. Вряд ли вы что-то подобное имеете в виду.

            Тогда давайте разберёмся, что?

            Она вообще не попадает в объектный файл?
            Или в объектных файлах вообще нет места подобной информации, потому что формат объектных файлов не предусматривает хранение такой информации?
            Или она там есть, но её никто не принимает во внимание?

            Вообще, какие отношения вас связывают с форматами объектных файлах ELF и COFF? Знаете эти форматы «на зубок», или просто имеете примерное представление, что объектные файлы хранят и зачем это нужно? Надеюсь, что вы всё-таки достаточно хорошо представляете, что там.

            Так вот, если говорить об объектных файлах формата ELF, сам по себе формат ELF предусматривает хранение размера ELF-символа. В структуре Elf32_Sym/Elf64_Sym есть поле st_size именно для этого.

            typedef struct {
                    Elf32_Word      st_name;
                    Elf32_Addr      st_value;
                    Elf32_Word      st_size;  // <------------
                    unsigned char   st_info;
                    unsigned char   st_other;
                    Elf32_Half      st_shndx;
            } Elf32_Sym;
            
            typedef struct {
                    Elf64_Word      st_name;
                    unsigned char   st_info;
                    unsigned char   st_other;
                    Elf64_Half      st_shndx;
                    Elf64_Addr      st_value;
                    Elf64_Xword     st_size;      // <--------------
            } Elf64_Sym;
            

            То есть как минимум, теоретически возможность сохранять информацию о размере в ELF-формате объектных файлов предусмотрена. И она не только существует, а ещё и используется на практике: компиляторы в это поле записывают актуальный размер сущности, к которой относится символ в таблице символов.

            Например GCC:

            // demo.c
            const char uni[1] = {42};
            const char bi[2] = {'A', 'z'};
            const char tri[3] = {6,6,6};
            const char quadro[4] = "foo";
            const char penta[5] = {9,8};
            
            gcc -c demo.c
            objdump -t demo.o
            
            demo.o:     file format elf64-x86-64
            
            SYMBOL TABLE:
            0000000000000000 l    df *ABS*  0000000000000000 demo.c
            0000000000000000 l    d  .text  0000000000000000 .text
            0000000000000000 l    d  .data  0000000000000000 .data
            0000000000000000 l    d  .bss   0000000000000000 .bss
            0000000000000000 l    d  .rodata        0000000000000000 .rodata
            0000000000000000 l    d  .note.GNU-stack        0000000000000000 .note.GNU-stack
            0000000000000000 l    d  .comment       0000000000000000 .comment
            0000000000000000 g     O .rodata        0000000000000001 uni
            0000000000000001 g     O .rodata        0000000000000002 bi
            0000000000000003 g     O .rodata        0000000000000003 tri
            0000000000000006 g     O .rodata        0000000000000004 quadro
            000000000000000a g     O .rodata        0000000000000005 penta
            

            Предпоследняя колонка — это именно оно. Размер.

            Так что, как видите, GCC размер массива в объектный файл кладёт.
            Остаётся только взять его оттуда при линковке, но для это требуется, чтобы на уровне формата ELF поддерживался тип релоков, который бы позволил при резолвинге подставлять не адрес сущности, а размер сущности. Предусмотрен ли в формате ELF такой тип релоков? Хороший вопрос. Типы релоков специфичны для каждой аппаратной платформы. Так вот, для x86, x86_64 aka amd64 и других популярных аппаратных архитектур такой тип релоков есть.

            Например, для x86 это R_386_SIZE32.

            Так что на уровне линковки линкер вполне способен разрулить ситуацию, когда в первом объектном файле есть ссылка на размер массива, который живёт в другом объектном файле, а во-втором объектном файле есть этот самый массив. Линкер возмёт размер сущности из таблицы символов второго файла и подставить это значение (точнее скорректирует на величину этого значения) в заданное место в куске машинного кода или данных из первого объектного файла.

            Остаётся только чтобы компилятор мог породить на выходе объектный файл, ссылающийся на размеру «внешних» сущностей.

            И вот только в этом месте начинаются проблемы.
            Это не объектные файлы сами по себе не способны хранить размер массивов.
            Это не линкер сам по себе не способен подставлять размер массива там, где попросят (а не адрес массива, как просят обычно)
            Это лишь на уровне компилятора C/C++ сделано так, что компилятор by-design отказывается компилировать sizeof, применённый extern int foo[], выбрасывая ошибку, вместо того, чтобы сгенерировать объектный файл с соответствующей ссылкой на размер сущности, которую подставил бы линкер, ориентируясь на информацию из другого объектного файла.

            Но это ELF. А что с COFF?

            В COFF в таблице символов у символа нет поля, аналогичного st_size из ELF. Но есть кое-что другое.

            Во-первых, у COFF-символов есть тип символа и класс символа, а значит и два соответствующих поля, а кроме того, у символов в таблице символов есть обязательная часть постоянного формата и размера и опциональная auxilliary-часть, формат и размер которой зависит от типа/класса COFF-символа. То есть записи символов с COFF не имеют постоянной длины (но размер записей так или иначе кратен некоей гранулярности, равной размеру обязательной части символа — это к слову). Так вот, форматом COFF предусмотрен такой тип как DT_ARY, который предполагает наличие вслед за обязательной частью aux-записи, в которой хранится размер массива.

            Вот первый механизм, посредством которого в объектных файлах COFF можно было бы хранить информацию о размере массива. Но этот существующие компиляторы, например компилятор Microsoft, этот путь не использует. Типы символов как таковые не используются. Потому что нет таковой надобности. Используя sizeof на сущности из других объектных файлов мы ссылаться не можем, а раз так, то зачем сохранять в COFF-OBJ-файлы размеры массивов? И вот они и не сохраняются. К тому же нет соответствующих типов релоков, которые позволили бы на этот размер массива сослаться из другого места — а это уже серьёзно.

            Но во-вторых, есть принципиально иной механизм. Причём концепция, позволяющая этому механизму быть, присутствует как в ELF, так и в COFF.

            По сути, и в ELF, и в COFF объектный файл представляет собой три кита: заголовок файла, таблицу секций (с атрибутами каждой секции и указанием, где в объектном фале лежат данные для этой секции) и таблицу символов (с атрибутами символов и ссылкой на секцию, которой принадлежит этот символ).

            Выделенное жирным — ключевое. В ELF-символах это поле st_shndx, а в COFF-символах это поле SectionNumber (по номенклатуре Microsoft) или **n_scnum`` (в рамках оригинальной номенклатуры, берущей начало, видимо, из System V).

            // Microsoft:
            typedef struct _IMAGE_SYMBOL {
                union {
                    BYTE    ShortName[8];
                    struct {
                        DWORD   Short;     // if 0, use LongName
                        DWORD   Long;      // offset into string table
                    } Name;
                    PBYTE   LongName[2];
                } N;
                DWORD   Value;
                SHORT   SectionNumber;
                WORD    Type;
                BYTE    StorageClass;
                BYTE    NumberOfAuxSymbols;
            } IMAGE_SYMBOL;
            typedef IMAGE_SYMBOL UNALIGNED *PIMAGE_SYMBOL;
            
            // System V:
            {
            	char		n_name[8];	/* Symbol Name */
            	long		n_value;	/* Value of Symbol */
            	short		n_scnum;	/* Section Number */
            	unsigned short	n_type;		/* Symbol Type */
            	char		n_sclass;	/* Storage Class */
            	char		n_numaux;	/* Auxiliary Count */
            }
            

            В норме это поле хранит номер секции (из данного объектного файла), в которой обитает сущность, описываемая данным символом (разумеется, если символ описывает сущность из данного объектного файла, а не из какого-то внешнего, на которую в данном фале всего лишь делается ссылка).

            И в COFF, и в ELF предусмотрено несколько специальных значений этого поля, и одно из них: это ABS. Если в норме поле «значение» хранит смещение сущности относительно начала секции, индекс которой приведён в поле «индекс секции», то ABS это означает, что символ не «живёт» в какой-то секции, а поле «значение» означает не смещение в секции, а абсолютное значение, то есть, грубо говоря, числовую константу. В COFF за это отвечает специальная константа IMAGE_SYM_ABSOLUTE, в ELF — константа N_ABS.(и там, и там она равна минус единице).

            Это именно то, что позволяет сохранить в COFF размеры сущностей, в частности, массивов. И вот тут как раз соответствующий тип релоков, чтобы просто взять и подставить в нужное место абсолютное значение абсолютного COFF-символа в COFF предусмотрен для всех аппаратных архитектур. Например, для x86 это IMAGE_REL_I386_ABSOLUTE.

            И тут остаётся только один момент: как положить в объектный файл COFF одновременно информацию и о смещении массива в рамках секции и о размере массива, если оба таких символа будут иметь одинаковое имя, а имя — это единственный способ ссылаться на сущность из других объектных файлов?

            А очень просто: как компилятор С++ помещает в объектный файл (будь то COFF или ELF) информацию символы для vftable, vbtable класса, или же символы, соответствующие разным вариантам перегруженной функции или перегруженного оператора? Он декорирует имена. Причём каждый компилятор использует свою схему декорирования. Даже компилятор C (не C++) от MS использует нечто вроде декларирования имён, когда раздаёт имена COFF-символам. Так вот, для хранения информации о массива foo, если бы стандарт предусматривал применение sizeof к extern-массивам, размер которых не очевиден из объявления, то достаточно было бы придумать схему декорирования для присвоения имени COFF-символу, несущему абсолютное значение размера.

            Так что после всего это хочу сказать:
            Объектные файлы прекрасно могут хранить размеры массивов — хранение такой информации прямо заложено в ELF и прекрасно доступно в COFF. Способ ссылаться из одного объектного файла на размер сущности из другого тоже предусмотрен напрямую в ELF, и как частный случай ссылок на абсолютные константные значения в COFF. Касательно COFF я также дополнительно проверил: «а поддерживает ли линкер этот механизм ссылки на ABS-символы, который на практике вроде бы как не используется?» — прекрасно поддерживате, как оказалось. Не верите: сформируйте два COFF .obj-файла вручную (в хекс-редакторе, например) и попробуйте слинковать их MS-овским линкером. Все прекрасно линкуется.

            Информация из объектных файлах не пропадает: она там есть, если её туда положил компилятор, и она оттуда берётся и используется (линкером), если линкер попросят её взять и использовать (а линкер об этом просит компилятор).

            Так что дело исключительно в компиляторе.

            И вот, вместо того, чтобы закрыть пробел вокруг уже давно существующей части языка — вокруг применение sizeof к extern-массивам неуказанного в декларации размера (при том, что нижележащий инструментарий в виде объектных файлах и линкера со своей стороны поддерживает такую возможность), что делает комитет и горе-предлагальщики?

            Вместо логичного решения чуть-чуть довести до ума существующую конструкцию языка они привносят в язык ИДИОТСКИЙ КОСТЫЛЬ в виде новой директивы препроцессора #embed.

            А главное (ВЫ ТОЛЬКО ВДУМАЙТЕСЬ!), что #embed ВООБЩЕ НИКАК не решает обозначенную вами проблему. Вот вы говорите, что убрать большой прединициализированный массив в отдельный объектный файл это так себе решение, потому что хоть обращаться к нему можно из каждого файла-исходника, размер (с помощью sizeof) получить не удастся ни откуда.

            А как чёртова #embed решает эту проблему? Абсолютно никак. Если вы объявите огромный массив табличных значений в foo.cpp, загнав в него полгига табличных значений с помощью #embed, то из bar.cpp, baaz.cpp, test.cpp, core.cpp, main.cpp вы по прежнему не можете получить её размер (с помощью sizeof, без промежуточной функции из того же объектного файла, что и сам массив)!

            Хотя примерно даже понятно, как из этого будут выкручиваться CPP-программисты. Поскольку с введением COMDAT в объектные файлы C++-программистам разрешили объявлять одно и то же в разных компилируемых файлах (лишь бы на этапе линковки оно оказалось одинаковым во всех файлах), C++-программисты будут помещать объявление полгигабайтного массива не в .h-файл, который будут подключать везде, где только можно.

            В итоге, при компиляции полгигабайтный массив будет попадать в каждый объектный файл, а потом уже линкеру придётся отбросить дубликаты. Гениальное стратегическое решение, ничего не скажешь...

            И нет, утилита ничего не должна знать что там хранится и в каком порядке байт. Ей это не нужно.

            Она ОБЯЗАНА знать, для того, чтобы в массиве оказались правильные значения, если только вы не согласны с мыслью, что костыль #embed настолько костыль, что годится только для инициализации байтовых массивов, но не массивов int16_t, int32_t, int64_t.

            Комментатор выше написал, что основная ценность этого подхода с #embed по сравнению с прилинковыванием заранее заготовленной и упакованной в .obj/.o-файл таблицей это возможность применить оптимизацию на основе compile-time расчётов.

            Допустим, у нас есть какой-то алгоритм, который полагается на таблицу простых чисел, и чтобы не вычислять последовательность простых чисел, оно вшита в программу:

            uint16_t PrimeNumbers[] = {
            2,	3,	5,	7,	11,	13,	17,	19,	23,	29,	31,	37,
            41,	43,	47,	53,	59,	61,	67,	71,	73,	79,	83,	89,
            97,	101,	103,	107,	109,	113,	127,	131,	137,	139,	149,	151,
            157,	163,	167,	173,	179,	181,	191,	193,	197,	199,	211,	223,
            227,	229,	233,	239,	241,	251,	257,	263,	269,	271,	277,	281,
            283,	293,	307,	311,	313,	317,	331,	337,	347,	349,	353,	359,
            367,	373,	379,	383,	389,	397,	401,	409,	419,	421,	431,	433,
            439,	443,	449,	457,	461,	463,	467,	479,	487,	491,	499,	503,
            509,	521,	523,	541,	547,	557,	563,	569,	571,	577,	587,	593,
            599,	601,	607,	613,	617,	619,	631,	641,	643,	647,	653,	659,
            661,	673,	677,	683,	691,	701,	709,	719,	727,	733,	739,	743,
            751,	757,	761,	769,	773,	787,	797,	809,	811,	821,	823,	827,
            829,	839,	853,	857,	859,	863,	877,	881,	883,	887,	907,	911,
            919,	929,	937,	941,	947,	953,	967,	971,	977,	983,	991,	997,
            /*   ... ... ... */
            9739, 9743, 9749, 9767, 9769, 9781, 9787, 9791, 9803, 9811, 9817, 9829,
            9833, 9839, 9851, 9857, 9859, 9871, 9883, 9887, 9901, 9907, 9923, 9929,
            9931, 9941, 9949, 9967, 9973
            };
            

            В таком виде у вас в коде есть одно место, которое является авторитетным источником сведений о простых числах.

            Программисту не нужно заботиться о том, как число 9973 представлено в памяти. Компилятор берёт эту заботу на себя.

            Если этот код компилируют под little-endian-архитектуру, оно будет представлено как F5 B6. Если под big-endian-архитектуру — то как B6 F5.

            И вот программист проникся идеей использовать #embed, ему осточертело держать таблицу простых чисел в коде, потому что ему нужны простые числа не до 10 000, а до 10 миллонов (или миллиардов). И он, потирая радостно ладошки, пишет утилиту, которая посчитает и запишет миллионы простых чисел в бинарный файл, а потом делает #embed этого бинарного файла в свой исходный код.

            Вопрос номер один: он сходу идёт нафиг, потому что #embed предназначена только для инициализации байтовых массивов?

            Вопрос номер два: если не только байтовых, то ему нужно один бинарный файл на случай компиляции под LE-архитектуры, и другой на случай компиляции под BE-архитектуры, переключаться между которыми нужно при помощи #if...#else...#endif?

            Вопрос номер три: если эта идиотская директива #embed будет иметь дополнительные спецификаторы, которые уточнят препроцессору, что нужно поменять порядок байтов, то в каком именно порядке байтов таблица волшебных значений (простых чисел — в нашем случае) должна храниться на диске, в репозитори? Почему это должен быть именно little-endian или big-endian?

            Вопрос номер три (б): а если речь идёт о хранении в таблице отрицательных значений, принимая во внимание, что в разных аппаратных архитектурах помимо разного порядка байтов (big endian vs. little endian) может практиковаться разная схема представления отрицательных чисел (дополнительный код vs. прямой код), то открывается ещё одна неопределённость? Теперь если я хочу задать большой массив 16-битных чисел с помощью бинарного файла, у меня есть целых 4 гипотетических способа того, как в этом бинарном файле может представлено число –2: как FE FF, как FF FE, как 80 02, как 02 80.

            Вопрос номер четыре: а как быть, если мы пишем переносимый код, который могут компилировать под разные архитектуры, причём такие, что в одной целевой архитектуре тип int будет иметь размер 4 байта, а в другой — 2 байта (допустим, это какая-то 16-битная аппаратная архитектура). Как должен выглядеть бинарный файл, которым с помощью элемента #embed инициализируется массив int-ов?

            Вопрос номер 5:
            А что, если я хочу с помощью бинарного файла инициализировать массив структур, например массив 3D-векторов struct{float x; float y; float z;}? Допустим, моей программе нужно иметь огромный массив, содержащий координаты всех известных науке звёзд, и эта информация у меня есть в виде бинарного файла? А если поля в структуре гетерогенны? А если они содержат вложенные структуры? А если разные архитектуры предполагают разную ширину полей и разное выравнивание? Какую ширину должны иметь поля в бинарном файле и какие должны выдерживаться выравнивани, чтобы в массиве оказались верные значение, а не какой-то съехавший мусор? А если в массиве будет указатель?

            Это нужно функциям, работающим с этими данными.

            Ну да, да, верю. Я уже выше привёл доводы, но ещё раз, с другой стороны: я пишу прошивку для стиральной машинки, и объявляю массив sndDone, в котором содержатся PCM-кодированный семпл с фразой «Стирка закончена». Каждый элемент массива — это отсчёт, который будет выставлен на ЦАП.

            Раньше у меня этот звук был закодирован текстовым инициализатором ( `= {-3000, -122, 0, 7956, 29744, ... }), а теперь я решил делать #embed на базе WAV-файла, от которого отказывается WAV-заголовок. Почему-то теперь, чтобы код не превратился в тыкву, функция, проигрывающая PCM-кодированный семпл, которая раньше была написана просто с расчётом на то, что она просто принимает массив 16-битных чисел, и никаких нюансов с порядком байтов и быть не может, теперь должна заботиться о том, а совпадает ли используемый ею (и архитектурой в целом) порядок байтов с порядком байтов, который использовался в бинарном файле, который использовался при компиляции.

            Короче, это такой жуткий костыль, что мне кажется тут обсужать нечего.

            То что ваш комментарий обильно пролайкали, да и в целом, то что большинство народа поддерживает то, что на протяжении последних лет делают с С++, вызывает у меня только ужас и желание охарактеризовать всё это одной фразой:

            Вот такой фразой


            1. DesertDragon
              00.00.0000 00:00
              +1

              сгенерировать объектный файл с соответствующей ссылкой на размер сущности, которую подставил бы линкер

              А шаблоны по sizeof тоже линкер инстанциировать будет?


              Причем то что предлагаете вы, элементарно релизуется дополнительным extern const size_t рядом с extern int foo[].


            1. perfect_genius
              00.00.0000 00:00
              +1

              Не хотите это оформить в отдельную статью?


              1. firehacker
                00.00.0000 00:00

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

                Глядя на баланс лайков между моими комментариями, и комментариями оппонентов, можно представить себе мировоззрение апологетов modern-C++ — усомниться в правильности пути, выбранного комитетом, значит устроить себе кармическое самоубийство в рамках Хабра.


                1. perfect_genius
                  00.00.0000 00:00

                  Как статья-мнение, что в C++ хотят затащить дебильную фичу? Как статья, разбирающая внутренности объектных файлов и внутреннюю кухню процесса линковки?

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


        1. ElleSolomina
          00.00.0000 00:00
          +1

          Это же не сериализатор, а просто удобное создание массива из файла.


          1. firehacker
            00.00.0000 00:00

            Прочитайте вторую часть вот этого комментария:
            https://habr.com/ru/company/yandex/blog/715358/comments/#comment_25266596


        1. alexeyrom
          00.00.0000 00:00
          +1

          выплюнет текстовое представление этих данных, которое потом будет просто #includeиться

          Не работает, потому что

          Finally, Microsoft has an ABI problem with its maximum string literal size that cannot be solved using string literals or anything treated like string literals, as the LLVM thread and the thread from Claire Xen make clear. It has also frustrated both C an C++ programmers alike, despite their best efforts. It was so frustrating that even extended-C-and-C++-compilers, like Circle, solve this problem with custom directives.


          1. firehacker
            00.00.0000 00:00

            Каким вообще боком тут ABI? И вообще,я не понимаю каким образом эта цитата относится к описанному. Причём здесь строковые литералы? Когда я писал «выплюнет текстовое представление», я не имел в виду строковый литерал, я имел в виду то, что называется aggregate initializer list.

            Я только что скомпилировал исходник, где объявлен и инициализирован элементами глобальный массив размером 20 Мб! 20 мегабайт, Карл! Это очень много для какой-то таблицы значений, которая должна быть известна на этапе компиляции чтобы что-то там значительно оптимизировать.

            Больше не получается, потому что я использую компилятор MSVC 1998-го года выпуска, и он генерирует ошибку, что внутренняя куча, которая используется для внутренних нужд компилятора, достигла своего лимита. Потому что создателям компилятора в 1998-ом году не пришло бы и в голову, что каким-то идиотам в 2023 понадобится инициализировать массив размером в несколько десятков Мб на уровне исходного кода сишной программы.

            Но давайте разберёмся: если бы #embed был 25 лет назад, #embed позволил бы внедрить в компилируемый модуль массив размером в полгига, если оставить в силе то же ограничение на размер внутренней служебной кучи компилятора? Значит проблема-то не в отсутствии директивы #embed, а в лимите размера кучи?

            Значит можно решить проблему. просто соответствующим образом увеличив лимит на размер кучи без добавления #embed в язык?

            Причём тут тогда ABI? По сути эти люди тащат в язык новую директиву препроцессора, потому что понимают, что Microsoft бросится имплементировать новый стандарт и им придётся так или иначе убрать этот лимит на размер кучи или применить принципиально иную архитектуру для хранения этих данных? А может просто попросить их подвинуть лимит кучи?


            1. antoshkka Автор
              00.00.0000 00:00
              +1

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

              А можно стандартизировать #embed и всем будет удобно из коробки.

              Весь процесс стандартизации направлен на удобство использлвания - можно обходиться без std::string, constexpr, auto, for, switch, std::map, std::vector... Можно писать эти классы самому, обходиться без автоматизации работы со стороны компилятора. Но будет не удобно


            1. alexeyrom
              00.00.0000 00:00

              А про aggregate initializer list на 2 абзаца выше:

              Creating a brace-delimited list of numbers in C comes with baggage in the form of how numbers and lists are formatted. C’s preprocessor and the forcing of tokenization also forces an unavoidable cost to lexer and parser handling of values.

              Therefore, using arrays with specific initialized values of any significant size becomes borderline impossible. One would think this old problem would be work-around-able in a succinct manner. Given how old this desire is (that comp.std.c thread is not even the oldest recorded feature request), proper solutions would have arisen. Unfortunately, that could not be farther from the truth. Even the compilers themselves suffer build time and memory usage degradation, as contributors to the LLVM compiler ran the gamut of the biggest problems that motivate this proposal in a matter of a week or two earlier this very year. Luke is not alone in his frustrations: developers all over suffer from the inability to include binary in their program quickly and perform exceptional gymnastics to get around the compiler’s inability to handle these cases.

              Со ссылками: https://groups.google.com/g/comp.std.c/c/zWFEXDvyTwM; https://lists.llvm.org/pipermail/llvm-dev/2020-January/138225.html; https://twitter.com/oe1cxw/status/1008361214018244608.

              И ещё:

              The numbers here are not reassuring that compiler developers can reduce the memory and compilation time burdens with regard to large initializer lists. Furthermore, privately owned compilers and other static analysis tools perform almost exponentially worse here, taking vastly more memory and thrashing CPUs to 100% for several minutes (to sometimes several hours if e.g. the Swap is engaged due to lack of main memory). Every compiler must always consume a certain amount of memory in a relationship directly linear to the number of tokens produced.

              То есть "подвинуть лимит" позволит скомпилировать, но очень медленно и с гигабайтами памяти на каждый мегабайт исходных данных (у Clang лучше: всего сотни мегабайт). Это уже после десятилетий попыток улучшить ситуацию. Если вы знаете, как сильно оптимизировать обработку больших инициализаторов хотя бы в GCC, Clang и MSVC, вашему решению очень многие обрадуются.

              А вот представление тех же данных строковыми литералами отлично и быстро работает в GCC и Clang, но в MSVC невозможно из-за ABI.


          1. crackedmind
            00.00.0000 00:00
            +1

            ну это уже давно исправлено

            versions of Visual Studio before Visual Studio 2022 version 17.0, the maximum length of a string literal is 65,535 bytes. This limit applies to both narrow string literals and wide string literals. In Visual Studio 2022 version 17.0 and later, this restriction is lifted and string length is limited by available resources.


  1. izirayd
    00.00.0000 00:00

    //  Before  
    std::vector days{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
    int idx = 0; for(const auto & d : days) {  
     print("{} {} \n", idx, d);   
     idx++; 
    }
    // After 
    #include <ranges>
    std::vector days{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; 
    for(const auto & [index, value] :   
      std::views::enumerate(days)) {    
      print("{} {} \n", index, value); 
    }   


    Проблема idx всегда раздражала, что нужно описывать, так как for range не предоставляет из коробки уже объявленный idx, тратится две линии на декларирование idx вне блока и его инкрементирование и это каждый раз надо писать. Вместо этого мы получаем std::views::enumerate, мало того что это имеет длинное название, так еще и описывать раскрытие в [index, value]. Я недоволен, требуется еще больше писать кода, это так же содержит больше слов, больше чтения кода. Значит добавить новую семантику в виде for range это ок, но добавить новую семантику idx - нет? Я понимаю что новый вид конструкции будет дёргать глаз, но если это при написании потребует 1 слов, 2 символа, разве это было бы комфортно?
    Что-то похожее на это:

    for (const auto &d : days => idx) { ... }


    1. Aldrog
      00.00.0000 00:00
      +1

      получаем std::views::enumerate, мало того что это имеет длинное название

      Почему бы не объявить using-альяс на весь проект, если эта конструкция в нём часто встречается?


      Я недоволен, требуется еще больше писать кода, это так же содержит больше слов, больше чтения кода.

      Больше символов, но меньше элементов. Вместо объявления переменной, её инициализации и инкремента только объявление structured binding и вызов enumerate. Опять же, с коротким альясом на enumerate символов тоже будет меньше (да даже с простым using std::views::enumerate;).


      Значит добавить новую семантику в виде for range это ок, но добавить новую семантику idx — нет?

      Дело в том, что ranged for — универсальный механизм, а предлагаемый вами дополнительный синтаксис поверх него решает только одну частную проблему (пускай и сравнительно частую). Добавление enumerate решает его не хуже, при том, что это всего лишь одна из множества библиотечных функций, а не специальный синтаксис.


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


      Ну и для демонстрации проблемы немного разовью вашу идею:


      std::vector days{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; 
      std::vector days_ru{"Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"}; 
      // Текущий стандарт. Слишком многословно
      using namespace std::views;
      for (const auto &[idx, day, day_ru] : zip(days, days_ru) | reverse | enumerate) { ... }
      // Вот так гораздо лучше:
      for (const auto &day, &day_ru ^: days, days_ru => idx) { ... }
      // Объявление двух переменных в левой части и перечисление через запятую в правой имеет особое значение итерации по двум коллекциям сразу.
      // ^ перед двоеточием означает проход в обратном направлении.
      // => в соответствии с вашим предложением.
      // P.S. Осталось придумать что делать, если мы хотим, чтобы индексы тоже в обратном направлении шли. Предлагаю внести в стандарт ^=>