
Привет! На связи Антон Полухин из Техплатформы Городских сервисов Яндекса, и сейчас я расскажу о софийской встрече Международного комитета по стандартизации языка программирования C++, в которой принимал активное участие. Это была последняя встреча, на которой новые фичи языка, с предодобренным на прошлых встречах дизайном, ещё могли попасть в C++26.
И результат превзошёл все ожидания:
- compile-time-рефлексия
- рефлексия параметров функций
- аннотации
std::optional<T&>
- параллельные алгоритмы
Compile-time рефлексия
Рефлексия будет в C++26! Это просто великолепные новости, очень многие ожидали эту фичу — у многих разработчиков уже чешутся руки написать что-то интересное с её помощью.
Рефлексия в C++ отличается от рефлексии в большинстве других языков программирования, ведь она:
- Compile-time — происходит в момент компиляции единицы трансляции.
- Type-erased — результат рефлексирования любой сущности (типа данных, объекта, параметра, namespace) всегда представляет собой один и тот же тип:
std::meta::info
. - Императивная — работа с рефлексией идёт в привычном императивном стиле программирования (в отличие от старого метапрограммирования через специализации шаблонов).
- Работает на уровне сущностей языка, а не на уровне токенов.
- Опционально учитывает права доступа (public, private) текущей области видимости.
- Обрабатывает ошибки через сompile-time-исключения.
Инструмент получился крайне мощный — он позволяет убрать множество boilerplate code при решении типовых (и не очень) задач.
Например, мы постоянно сталкиваемся с необходимостью задавать маппинг значения перечисления (enum) на его текстовое представление. В нашей кодовой базе для этого заводится специфичный bimap:
enum class Colors { kRed, kOrange, kYellow, kGreen, kBlue, kViolet };
constexpr userver::utils::TrivialBiMap kColorSwitch = [](auto selector) {
return selector()
.Case("Red", Colors::kRed)
.Case("Orange", Colors::kOrange)
.Case("Yellow", Colors::kYellow)
.Case("Green", Colors::kGreen)
.Case("Blue", Colors::kBlue)
.Case("Violet", Colors::kViolet);
};
TEST(TrivialBiMap, EnumToString) {
EXPECT_EQ(kColorSwitch.TryFind(Colors::kGreen), "Green");
EXPECT_EQ(kColorSwitch.TryFind("Orange"), Colors::kOrange);
}
Как видите, писать такие маппинги — весьма рутинная и скучная задача. С помощью рефлексии её можно проделать единожды:
namespace impl {
template <typename E>
consteval auto MakeEnumLambda() {
auto lambda = [](auto selector) {
auto s = selector();
template for (std::meta::info e : std::meta::enumerators_of(^^E)) {
s.Case(
std::meta::extract<E>(e),
std::meta::identifier_of(e).remove_prefix(1) // удаляем `k`
);
});
return s;
};
return lambda;
}
} // namespace impl
template <typename E>
requires std::is_enum_v<E>
inline constexpr userver::utils::TrivialBiMap kEnum = impl::MakeEnumLambda<E>();
И после этого переиспользовать решение:
enum class Colors { kRed, kOrange, kYellow, kGreen, kBlue, kViolet };
TEST(TrivialBiMap, EnumToString) {
EXPECT_EQ(kEnum<Colors>.TryFind(Colors::kGreen), "Green");
EXPECT_EQ(kEnum<Colors>.TryFind("Orange"), Colors::kOrange);
}
Предложение по рефлексии и больше примеров можно увидеть в P2996. С предложением на
template for
(expansion statement, compile-time развёрнутый цикл) можно ознакомиться в P1306.От меня, как от пользователя языка C++, огромное спасибо всем людям, которые сделали рефлексию возможной! Это был долгий путь, который начался в 2007 году с первого предложения на добавление
constepxr
. С тех пор Комитет расширял возможности compile-time-вычислений: добавил constepxr
-алгоритмы, разметил классы как constepxr
, ввёл consteval
, реализовал constepxr
-аллокации и использование исключений в constepxr
… — и наконец пришёл к P2996!Приятно осознавать, что Рабочая Группа 21 тоже приложила руку к этому процессу: P0031, P0426, P0639, P0202, P0858, P0879, P1032, P2291, P2417… Хотя наш вклад несравним с работой, проделанной Daveed Vandevoorde, Hana Dusíková, Faisal Vali, Andrew Sutton, Barry Revzin, Dan Katz, Peter Dimov, Wyatt Childers и многими другими людьми, годами работавшими над рефлексией и
constepxr
-вычислениями.Рефлексия аннотаций и параметров функций
Праздник на предложении P2996 не закончился. Весьма неожиданно успели принять в стандарт P3096 и P3394.
Первое предложение позволяет получить типы и имена параметров функций, и в том числе — работать с конструкторами и операторами. Это мощный, но в то же время хрупкий инструмент. Например, реализации стандартных библиотек C++ вольно обходятся с именами параметров функций, и они не всегда совпадают с именами, описанными в стандарте. Так что привязываться к именам параметров без большой необходимости не рекомендуется.
Второе предложение позволяет делать собственные аннотации (не путать с атрибутами!), которые доступны для рефлексии и дают возможность дополнительно настраивать кодогенерацию.
У аннотаций есть синтаксис
[[=constant-expression]]
, где constant-expression
может быть любым выражением, вычислимым на этапе компиляции.Например, без аннотаций можно сделать вот такую функцию, которая будет выводить имена полей структуры вместе со значениями полей:
namespace my_reflection {
template <typename T>
void PrintKeyValue(const T& value) {
template for (constexpr auto field : nonstatic_data_members_of(^^T)) {
std::println("{}: {}", identifier_of(field), value.[: field :]);
}
}
} // namespace my_reflection
// Пример использования:
struct Pair {
int first;
int y;
};
my_reflection::PrintKeyValue(Pair{1, 2});
// Вывeдет в консоль:
// first: 1
// y: 2
А с помощью аннотаций можно переопределять имена полей:
namespace my_reflection {
struct Name{ std::string_view name; };
template <typename T>
void PrintKeyValue(const T& value) {
template for (constexpr auto field : nonstatic_data_members_of(^^T)) {
constexpr auto annotation_vec = annotations_of(field);
constexpr std::string_view name = (
annotation_vec.size() == 1
&& type_of(annotation_vec[0]) == ^^my_reflection::Name
? std::meta::extract<my_reflection::Name>(annotation_vec[0]).name
: identifier_of(field)
);
std::println("{}: {}", name, value.[: field :]);
}
}
} // namespace my_reflection
// Пример использования:
struct Pair {
int first;
[[=my_reflection::Name{"second"}]]
int y;
};
my_reflection::PrintKeyValue(Pair{1, 2});
// Вывeдет в консоль:
// first: 1
// second: 2
Ещё больше примеров доступно в самом предложении P3394.
std::optional<T&>
Библиотека Boost.Optional долгое время позволяла создавать объекты
boost::optional<T&>
. В P2988 эта функциональность доехала и до C++26.Но если можно использовать просто
T*
, зачем же нужен std::optional<T&>
? У последнего есть свои плюсы:- запрещает арифметику указателей, избавляя от части возможных ошибок;
- не вызывает недоумения (а надо ли освобождать ресурсы по этому указателю?);
- имеет удобные для использования монадические интерфейсы и удобные value_or()-функции;
- может передаваться как диапазон в ranges.
Параллельные алгоритмы
Радостная новость для тех, кто пользуется параллельными алгоритмами. С принятием в C++26 предложения P3179 можно использовать политики выполнения (например,
std::execution::par_unseq
) с алгоритмами в std::ranges
.Основной автор предложения, Ruslan Arutyunyan, подсвечивает интересную фишку из данного документа: начиная с P3179
ranges
начинают использоваться как выходной параметр. Вместо std::ranges::copy(std::execution::par, in, out.begin());
мы получаем более безопасный и короткий интерфейс вида std::ranges::copy(std::execution::par, in, out);
. Если выходной диапазон меньше, чем входной, не произойдёт проезда по памяти — скопируется лишь то количество элементов, которое можно скопировать в выходной диапазон. Более того, если пользователь передал выходной диапазон меньшего размера по ошибке, у него всегда есть возможность это определить: все алгоритмы возвращают точку, до которой они смогли дойти во входном диапазоне (входных диапазонах). Особенными в этом отношении являются ranges::reverse_copy и ranges::rotate_copy. Кому интересно, могут почитать о последних двух алгоритмах в P3179 и в P3709
Грустная новость: пока не получится использовать параллельные алгоритмы с schedulers и senders из принятого в C++26 P2300. Работа в этом направлении продолжится уже в C++29 (в P2500).
В P3111 для C++26 расширили возможности атомарных переменных. Им добавили методы
void store_
, которые, в отличие от методов fetch_
, не возвращают значение, и, соответственно, у компилятора больше возможностей для их оптимизаций.Казалось бы, что делает эта новость в разделе про параллельные алгоритмы? А вот что: по стандарту, нельзя использовать операции
atomic::fetch_
в параллельных алгоритмах с std::execution::*unseq
. Операции atomic::store_
как раз позволяют обойти эту проблему — их можно использовать вместе с std::execution::*unseq
.Ещё немного о ranges
Давайте поиграем в угадайку! Как вы думаете, почему следующий код не скомпилируется?
for (auto x : std::ranges::iota(0, some_vector.size())) {
std::cout << some_vector[x] << std::endl;
}
no matching function for call to 'iota_view(int, long unsigned int)'
, так как iota требует одинаковые типы входных параметров.Как раз чтобы не сталкиваться с такой проблемой и не писать лишнего, в C++26 был добавлен
std::ranges::indices
в P3060:for (auto x : std::ranges::indices(some_vector.size())) {
std::cout << some_vector[x] << std::endl;
}
Продолжим с нашей угадайкой. Теперь загадка от Nicolai Josuttis. Что произойдёт в следующем примере?
std::vector<std::string> coll1{"Amsterdam", "Berlin", "Cologne", "LA"};
// Перемещаем длинные строки в обратном порядке в другой контейнер
auto large = [](const auto& s) { return s.size() > 5; };
auto sub = coll1 | std::views::filter(large)
| std::views::reverse
| std::views::as_rvalue
| std::ranges::to<std::vector>();
std::println
в фильтр может помочь.Проблема кроется прямо в дизайне
std::views::filter
. Увы, фильтр позволяет проходить по диапазону несколько раз, при этом он не накладывает константность на данные. Как результат — данные можно «вытащить» или изменить, и при последующих прохождениях фильтр будет сходить с ума. Nicolai Josuttis приводит ещё пример, который является неопределённым поведением (undefined behavior, UB) с точки зрения стандарта:// Возвращаем умерших монстров к жизни
auto dead = [] (const auto& m) { return m.isDead(); };
for (auto& m : monsters | std::views::filter(dead)) {
m.bringBackToLive(); // undefined behavior
}
Если бы после фильтра был ещё, например,
std::views::reverse
, код мог бы сломаться.Чтобы обойти все эти ужасы c
std::views::filter
, в P3725 (документ может быть пока недоступен) предлагается добавитьstd::views::input_filter
, фактически убирая возможность несколько раз фильтровать один и тот же элемент, эквивалентен filter_view(to_input_view(E), P)
. Возможно, эту новинку удастся внести в стандарт как багфикс и увидеть решение уже в C++26.Прочие новинки
-
std::string
обзавёлся методомsubview
, который работает по аналогии сsubstr
, но, в отличие от последнего, возвращаетstd::string_view
(P3044). -
std::simd
оброс новыми методами и функциональностью в P2876, P3480, P2664, P3691. - Из
std::exception_ptr
теперь можно достать исключение, не выкидывая его, а используяstd::exception_ptr_cast<Exception>(exception_ptr)
(P2927). И можно это делать даже в compile-time (P3748, может быть доступен позже). - В последний момент проскочило предложениеP3560, которое меняет способ сообщения об ошибке для рефлексии. То, что раньше было ошибкой компиляции, теперь стало исключением, выкинутым на этапе компиляции, — его можно ловить и обрабатывать.
- Из приятных мелочей — в C++26 добавили класс
std::constant_wrapper
и переменную constexprstd::cw
. Это более краткая замена дляstd::integral_constant
. При этом они обладают всеми операторами нижележащего типа, что позволяет использовать их как обычные числа, но передавать в функцию как compile-time-константы:
void sum_is_42(auto x, auto y) {
static_assert(x + y == 42);
}
sum_is_42(std::cw<40>, std::cw<2>);
-
std::cw
из предложения P2781 собенно удобен при работе сstd::mdspan
. Например,std::mdspan(data, std::integral_constant<std::size_t, 10>{}, std::integral_constant<std::size_t, 20>{}, std::integral_constant<std::size_t, 30>{});
, превращается просто вstd::mdspan(data, std::cw<10>, std::cw<20>, std::cw<30>);
- В executors добавили
std::execution::task
в P3552 иstd::execution::write_env
+std::execution::unstoppable
sender-адаптеры в P3284. Теперь можно совмещать executors и корутины,чтобы ещё сильнее смущать коллег на код-ревью. - Наконец в P3697 продолжили завинчивание гаек с безопасностью, и ещё больше функций стандартной библиотеки обросли hardening-проверками.
Итоги
C++26 теперь feature complete! И рефлексия в нём будет!
Следующий этап стандартизации С++: представители стран посылают свои замечания к C++26, подсвечивая важные баги и проблемы. Тут и вы можете внести свою лепту! Если у вас есть замечания к C++26 или любимый многострадальный баг, а может, вы знаете о какой-то проблеме — пишите нашей рабочей группе в раздел раздел «Предложения»: и мы отправим ваши (исправимые на данном этапе) замечания в ISO. Разборам и исправлениям багов будут посвящены как минимум две ближайшие встречи Международного комитета.
На этом у меня всё. Приходите пообщаться на C++ Zero Cost Conf 2 августа, послушать интересные и практичные доклады и пообщаться с командой userver на стенде городских сервисов Яндекса.
Пишите в комментариях о самой ожидаемой или любимой фиче в предстоящем C++26. С радостью отвечу на ваши вопросы :)
Комментарии (35)
Kelbon
01.07.2025 07:08template for конечно ошибка, но при условии что компиляторщики смогут это реализовать без слома всего, рефлексия в целом перевесит этот недостаток
У меня такой вопрос: можно ли пройти по всем методам типа, посмотреть их типы (т.е. например int(float, double) ) ? И что насчёт шаблонных методов?
Jijiki
01.07.2025 07:08// Возвращаем умерших монстров к жизни auto dead = [] (const auto& m) { return m.isDead(); }; for (auto& a : monsters | std::views::filter(dead)) { a.bringBackToLive(); // а так? }
Kelbon
01.07.2025 07:08а что изменилось
P.S. если вы про имя параметра, то в С++ несколько другие правила видимости нежели в JS
Jijiki
01.07.2025 07:08Скрытый текст
понял, тут правда переставил сборку по фильтру как я понял-это сборка по фильтру и перенес её действие до цикла
а вообще если там уб то логично же, надо собрать же, сделать проверку с последовательностью по фильтру, а в фильтре лямбда, это даёт последовательность и уже пройтись по последовательности так звучит ожидаемо и очевидно
KanuTaH
01.07.2025 07:08и перенес её действие до цикла
Вообще-то нет.
это даёт последовательность и уже пройтись по последовательности так звучит ожидаемо и очевидно
У вас не делается ничего подобного. Вставьте отладочную печать скажем в лямбду и непосредственно перед строкой с
for
и убедитесь.Jijiki
01.07.2025 07:08вы правы,
Скрытый текст
#include <cstdint> #include <cstring> #include <iostream> #include <vector> #include <ranges> #include <string> class test{ public: std::string name; int p=1; int te(){return p;} void pName(){std::cout<<name<<std::endl;} void bringBackToLive(){std::cout<<p<<std::endl;} }; int main (){ std::vector<test> r(10); std::string tttt[]={"Name0","Name1","Name2","Name3","Name4","Name5","Name6","Name7","Name8","Name9"}; int o=0; int c=0; for(auto& k: r){ if(o%2){k.p=o;k.name=tttt[c];}o++;c++; } //std::vector<test> b(10); auto b= r|std::views::filter([](const auto& m) {std::cout<<"inlambda"<<std::endl; return m.p>1; }); std::cout<<"NOTLAMBDA"<<std::endl; for (auto& v :b) { v.pName(); v.bringBackToLive(); } return 0; } NOTLAMBDA inlambda inlambda inlambda inlambda Name3 3 inlambda inlambda Name5 5 inlambda inlambda Name7 7 inlambda inlambda Name9 9 тогда можно еще просто так auto b= [](auto m) { for(auto x: m){ if(x.p>1){ x.pName(); x.bringBackToLive(); } } }; b(r);
забыл что это лямбда и расслабился
ааааааа, это как функция на момент вызова в цикле for чтоли?
поидее обратились к б в цикле пошла лямбда поидее так же, сначала я растерялся, но поидее всё равно логично, логично даже если лямбду записать вместо б, тогда будет нагляднее просто
JordanCpp
01.07.2025 07:08Почему с рефлексией так тянули, аж с 2007? Малый интерес или неготовность ядра С++ на то время?
antoshkka Автор
01.07.2025 07:08Интерес огромный, а вот с возможностями compile time вычислений в 2007 было очень тяжко. В C++11 constexpr функция должна была состоять только из одного return, в C++14 уже можно было делать циклы... А потом уже появились динамические аллокации в constepr, исключения, стандартная библиотека подтянулась
JordanCpp
01.07.2025 07:08Как говорится дождались, осталось дождаться когда реализуют в компиляторах.
antoshkka Автор
01.07.2025 07:08Прототипы рефлексии есть для clang, и кажется что для GCC тоже. Так что надеюсь, что реализуют сравнительно быстро, тем более что изменения затрагивают только frontend компилятора (в отличие от модулей, которые затрагивают вообще всё, включая системы сборки)
Lainhard
01.07.2025 07:08Кастомные [[my_attr]] завезли?
Упс. Недочитал статью и сразу полез в комментарии. Не смейте меня прощать. Вопрос снимается.
NeoCode
01.07.2025 07:08Не знаю что и сказать. Рад за С++, что он развивается, рефлексия это реально важная вещь для языков программирования; но примеров кода не понял вообще (последнее время пишу в основном или на чистом Си или на Qt5)
А рефлексию (для енумов и не только) я делаю вот так:
#define LIST \ ITEM(ONE, "hello", x) \ ITEM(TWO, "world", y) \ ITEM(THREE, "!", z) // перечисление #define ITEM(a, b, c) a, enum Foo { LIST }; // имена элементов перечисления #define ITEM(a, b, c) #a, const char *names[] = { LIST }; // ассоциированные строки #define ITEM(a, b, c) b, const char *strings[] = { LIST }; // и т.д. - декларация структуры, имена полей структуры, аргументы функции, что угодно
Самое прикольное что эта фича работала еще до появления самого С++, более того она, вероятно, была еще когда меня на свете не было:) Но в книгах этого приема обычно нет, и люди мучаются, выдумывая всякие костыли или просто тупо вручную пишут код и рискуют однажды забыть дописать очередной элемент.
tenzink
01.07.2025 07:08Это же стандартный https://en.wikipedia.org/wiki/X_macro. Не сильно раскрученная техника, но, мне казалось, довольно известная
X-Ray_3D
01.07.2025 07:08namespace Impl { namespace ranges = std::ranges; using std::array; using std::string_view; template <class Ty> inline constexpr bool hasStrings = false; template <class Ty> inline constexpr Ty Tokens = Ty{}; template <auto EnumVal> static consteval auto enumName() { #ifdef _MSC_VER // MSVC: auto __cdecl Impl::enumName<align::CM>(void) constexpr string_view sv{__FUNCSIG__}; constexpr size_t last = sv.find_last_of(">"); #else // clang: auto Impl::name() [E = align::CM] // gcc: consteval auto Impl::name() [with auto E = align::CM] constexpr string_view sv{__PRETTY_FUNCTION__}; constexpr auto last = sv.find_last_of("]"); #endif constexpr size_t first = sv.find_last_of(":", last) + 1; array<char, last - first + 1> buf{}; // +1 '\0' tetminated c_str ranges::copy(string_view{sv.data() + first, last - first}, buf.begin()); return buf; } template <auto EnumVal> inline constexpr auto ENameArr{enumName<EnumVal>()}; template <auto EnumVal> inline constexpr string_view EnumName{ENameArr<EnumVal>.data(), ENameArr<EnumVal>.size() - 1}; template <typename Enum, auto... Enums> class Tokenizer { struct Data { string_view name; Enum value; }; static constexpr array tokens{ Data{EnumName<Enums>, Enums} ... }; using Ty = std::underlying_type_t<Enum>; static constexpr Enum errorValue{static_cast<Enum>(std::numeric_limits<Ty>::max())}; public: static constexpr auto toString(Enum e) noexcept { auto it = ranges::find(tokens, e, &Data::value); return it == tokens.end() ? string_view{""} : it->name; } static constexpr Enum toEnum(string_view str) noexcept { auto it = ranges::find(tokens, str, &Data::name); return it == tokens.end() ? errorValue : it->value; } }; } // namespace Impl #define XML_ENUM(Enum, ...) \ enum class Enum : int { \ __VA_ARGS__ \ }; \ inline auto operator+(Enum e) noexcept { return std::underlying_type_t<Enum>(e); } \ namespace Impl { \ template <> \ inline constexpr auto hasStrings<Enum> = true; \ template <> \ inline constexpr auto Tokens<Enum> = [] { \ using enum Enum; /* using enum ↓ P1099R5 */ \ return Tokenizer<Enum, __VA_ARGS__>{}; \ }(); \ } template <typename E, typename Enum = std::remove_cvref_t<E>> requires Impl::hasStrings<Enum> inline constexpr auto enumToString(E e) { return Impl::Tokens<Enum>.toString(e); } template <typename E, typename Enum = std::remove_cvref_t<E>> requires Impl::hasStrings<Enum> inline constexpr E stringToEnum(std::string_view str) { return Impl::Tokens<Enum>.toEnum(str); } // Параметр надписей (ярлыков): способ выравнивания текста. Значение по умолчанию – CM. XML_ENUM(align, CM, // по центру LT, // по левому верхнему углу CT, // по верхнему краю RT, // по правому верхнему углу LM, // по левому краю RM, // по правому краю LB, // по левому нижнему углу CB, // по нижнему краю RB // по правому нижнему углу )
Наконец-то без макродристни это по-человечески просто писать можно будет.
enum class align{ CM, // по центру LT, // по левому верхнему углу CT, // по верхнему краю RT, // по правому верхнему углу LM, // по левому краю RM, // по правому краю LB, // по левому нижнему углу CB, // по нижнему краю RB // по правому нижнему углу };
jaobabus
01.07.2025 07:08Почему именно template for, а не for constexpr по аналогии с if constexpr?
KanuTaH
01.07.2025 07:08Ну, насколько я понимаю, разница в именовании проистекает из принципа работы.
template for
- это именно шаблон, по которому компилятор перегенерирует "тело цикла" для каждого элемента из указанного списка. Аif constexpr
сам по себе никакой генерацией кода не занимается, компилятор просто выбирает один из вариантов в соответствии со значениемconstexpr
условия. По-моему достаточно логично.
domix32
01.07.2025 07:08Увы, фильтр позволяет проходить по диапазону несколько раз, при этом он не накладывает константность на данные.
чёт я не понял, а сфига ли он проходит несколько раз? Казалось бы, рэнж должен был быть в размер контейнера/ Где что-то пошло не так и он начинает бродить по "кругу"?
причем если делать
reverse|filter
ему нормально, и ожидаемо перемещённые значения рендерятся как пустые, аfilter|reverse
это почему-то ересь.
Jijiki
01.07.2025 07:08кстати std::views::filter используется неоправдно в цикле кстати, тоесть проблема в том что это погоня за другим языком, а в тех примерах совсем другие подходы и работа с памятью, если это тащить в С++ будут проблемы ) мне кажется) тоесть зачем нам проходить 2 раза по данным? надо идти 1 раз по данным производя очевидные проверки, а так понаставишь таких фильтров я чувствую и что-то не то, тоесть сначало собрали, и что удалили когда вошли в цикл?
чото я с фильтром запутался не делает ли он копий в памяти я этот вопрос еще давно задавал в другом языке частенько
mamaximov
01.07.2025 07:08Да вообще непонятно зачем это сделано. Иногда есть ощущение что создали стандарта сознательно закладывают мины в язык, которые потом чинят усложняя язык и стандартную билиотеку. Лично я никак это иначе не могу объяснить. Причем если рассматривать range-ы. Они же не первопроходцы. Уже были аналоги в других языках. Нигде никто ничего не кеширует в filter-е (если брать mainstream). Более того в например в Java-е, повторная итерация по stream-у это вообще прохая практика, сразу получите exception. Да и просто если посмотреть на api range-ов (взгляд из другой экосистемы) он кажется неудачный. Да очень элегантно сделали, но есть фатальный недостаток не работает "." (не будет нормальной поддержки со стороны IDE), нужно помнить какие есть views-ы или постоянно набирать std::ranges::views или alias какой-то использовать. Может не очень актуально во времена llm, но тем не менее
antoshkka Автор
01.07.2025 07:08Это обычная ситуация для всех языков программирования, которые активно развиваются. Вон даже в простом C сравнительно недавно напортачили с extern inline.
Просто зачастую у нас есть язык, за которым внимательно следим, и не очень замечаем что в других проблемы не меньше.
AlexeyK77
очень давно не программирую. Но посмотрев листинги с этими всеми жуткими шаблонами на меташаблонах, понимаю, что С++ превратился во что-то другое, не приспособленное для человечьих мозгов.
Верните обратно С++93 ;)
Хотя лично я болею за Rust и надеюсь, что при смене поколений этот "монстрик" уйдет туда-же куда ушел кобол с программистами мэйнфреймов.
denis_iii
Да, основная проблема С++ в том, что с каждым новым релизом он становится все более синтаксически нечитаемым. Но, главное, что бы AI-агенты справлялись.
А для души и быстрой компиляции всегда есть 14 и 17.
Jijiki
шаблоны становятся доступнее, маленькие тесты делал, вроде нормально, вот по старинке писать это тогда только С как раз(тоесть всё расписывать и тд)
с++ удобно есть operator - это удобно
добавили std::println("{}",1); вид вывода - тоже удобно
простенькие лямбды без std тоже удобно
сама стд стала всё равно удобнее
на расте же тоже шаблонный синтаксис как я понимаю <> с такими кавычками
domix32
Там есть несколько механизмов, но да с угловыми скобками. Местные концепты там тоже можно писать через where clause, а не фигачить все в параметры шаблонов. В новейших стандартах вроде как завезли require clauses, но там как обычно.
dv0ich
У Раста-то синтаксис охренеть какой читаемый, лол. Даже в сравнении с плюсами.