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

На первом заседании комитет сосредоточился на усовершенствовании функций C++23, в том числе:

  • static operator[]
  • static constexpr в constexpr функциях
  • Безопасный диапазон для
  • Взаимодействие std::print с другими выводами консоли
  • Монадический интерфейс для std::expected
  • static_assert(false) и другие функции

На втором заседании комитет работал над разработкой новых функций для C++26, в том числе:

  • std::get и std::tuple_size для агрегатов
  • #embed
  • Получение std::stacktrace из исключений
  • Стековые сопрограммы

C++23

статический оператор[]

Прошлым летом комитет добавил static operator() в C++23 и сделал возможным определение 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);

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

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

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

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

статический constexpr в функциях constexpr

C++23 расширил свою функциональность добавлением constexpr to_chars/from_chars. Однако некоторые разработчики столкнулись с проблемой. Несколько стандартных библиотек содержали массивы констант для быстрого преобразования string<>number, которые были объявлены как статические переменные внутри функций. К сожалению, это помешало их использованию в constexpr функциях. Эту проблему можно обойти, но обходные пути выглядели очень неуклюжими.

В конце концов комитет решил проблему, разрешив использование static constexpr переменных в constexpr функциях, как указано в P2647R1. Небольшое, но долгожданное улучшение.

Безопасный диапазон для

Это, пожалуй, самая захватывающая новость за последние две встречи!

Кстати говоря, давайте начнем с забавной загадки: можете ли вы определить ошибку в коде?

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 << ',';
  }
}

Вот ответ.

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

На RG21 мы впервые попытались исправить эту ситуацию четыре года назад с D0890R0. К сожалению, процесс застопорился на стадии обсуждения.

К счастью, инициативу подхватил Николай Йосуттис, и в C++23 аналогичный код больше не будет создавать висячую ссылку. Все объекты, созданные справа от : в цикле for на основе диапазона, теперь уничтожаются только при выходе из цикла.

Дополнительные технические подробности см. в документе P2718R0.

станд:: печать

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

printf("first");
std::print("second");

Монадный интерфейс для 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);

// Somewhere in the application code...
from_iso_str(input_data)
    .and_then(&to_bson)
    .and_then(&insert_into_db)
    // Throws “Exception” if any of the previous steps resulted in an error
    .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");
    }
}

В дополнение к упомянутым ранее изменениям в C++23 было внесено бесчисленное количество улучшений диапазонов. Наиболее существенным из них является включение 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++, которую мы уже активно используем в Яндекс.Го и фреймворке userver, и она доступна всем желающим благодаря Boost.PFR.

Если вы пишете общую библиотеку шаблонов, скорее всего, вам придется использовать std::tuple и std::pair. Тем не менее, есть некоторые проблемы с этими типами. Во-первых, они затрудняют чтение и понимание кода, поскольку поля не имеют четких имен, и может быть сложно понять значение чего-то вроде std::get<0>(tuple). Более того, пользователи вашей библиотеки могут не захотеть работать с этими типами напрямую и будут создавать объекты этих типов прямо перед вызовом ваших методов, что может быть неэффективно из-за копирования данных. Во-вторых, std::tuple и std::pair не «распространяют» тривиальность типов, которые они хранят. Следовательно, при передаче и возврате std::tuple и std::pair из функций компилятор может генерировать менее эффективный код.

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

Идея P2141R0 состоит в том, чтобы разрешить использование агрегатов в универсальном коде, заставив std::get и std::tuple_size работать с ними. Это позволит пользователям передавать свои структуры непосредственно в вашу общую библиотеку без ненужного копирования.

Идея была хорошо воспринята комитетом, и мы будем работать над тестированием и устранением любых потенциальных шероховатостей в будущем.

#встроить

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

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

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

Необходимо проработать некоторые мелкие детали. Полное описание идеи доступно в P1967.

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

Идея P2370 WG21 столкнулась с неожиданной неудачей.

Возможность получения трассировки стека из исключения присутствует в большинстве языков программирования. Эта функция невероятно полезна и позволяет проводить более информативную и понятную диагностику, а не неинформативные сообщения об ошибках, такие как 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) эта функция может быть невероятно полезной. Это позволяет быстро выявлять проблемы в тесте и избегать проблем с локальным воспроизведением проблемы, что не всегда возможно.

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

Стековые сопрограммы

После многих лет работы стандарт C++, наконец, близок к добавлению базовой поддержки сопрограмм с поддержкой стека в C++26 (см. P0876). Стоит углубиться в сопрограммы со стеком и без него.

Бесстековые сопрограммы требуют поддержки компилятора и не могут быть реализованы сами по себе в виде библиотеки. Stackful сопрограммы, с другой стороны, могут быть реализованы сами по себе — например, с помощью Boost.Context.

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

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

Без стеков:

auto data = co_await socket.receive();
process(data);
co_await socket.send(data);
co_return; // requires function to return a special data type

Групповые:

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

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

thread_local int i = 0;
// ...
++i;
foo();  // Stackful coroutines can switch execution threads
assert(i > 0);  // The compiler saved the address in a register; we’re working with the TLS of another thread

Краткое содержание

Так и сделано! C++23 официально отправлен в вышестоящие инстанции ISO и вскоре будет опубликован как полноценный стандарт.

Между тем, разработка C++26 идет полным ходом, и есть захватывающие перспективы для исполнителей, сетей, сопоставления с образцом и статического отражения. Если у вас есть инновационные идеи по улучшению C++, поделитесь ими. Или, что еще лучше, подумайте о том, чтобы подать предложение. Будем рады помочь!