
В языке C++ перегрузки функций и шаблонов исторически были и остаются мощным инструментом для выражения различных реализаций одного и того же интерфейса. Многим перегрузки видятся как удобный способ дать одно имя разным функциям, но на практике понимание того, как компилятор выбирает нужную перегрузку, может стать источником ошибок и недоразумений. Компилятор же руководствуется сложным набором правил, которые мы ему предоставили, учитывает не только типы аргументов, но и порядок специализаций, преобразования типов, const-квалификаторы, шаблонные параметры и многое другое. А ошибки, возникающие при перегрузках, часто трудно диагностировать, поскольку сообщение компилятора может ссылаться на глубоко вложенные детали реализации вместо очевидного исходного кода. Об этом была предыдущая статья...
С введением концептов и ограничений (requires) язык получил возможность управлять этой сложностью на уровне интерфейса. Вместо того чтобы надеяться на магию перегрузки и изощрённые трюки вроде SFINAE, мы теперь можем прямо выражать намерения: какие свойства должен иметь тип, чтобы функция или шаблон были корректны, что позволило перейти от «магии разрешения перегрузок» к декларативному описанию требований к типам.
Давайте теперь поговорим о том, что именно делают ограничения (requires) в современном C++ и почему появление этого механизма стало таким важным шагом в развитии шаблонов. Тут надо сделать немного шаг в сторону и вспомнить, что исторически шаблоны в C++ были мощным, но довольно опасным инструментом, еще одним языком в языке, на котором можно было сделать почти всё, было бы желание.
В итоге компилятор позволял подставить или подхачить любой тип, а проверка того, «подходит ли он на самом деле», откладывалась до момента инстанцирования, что нередко приводило к ошибке далеко от места вызова, а сообщение о непосредственном месте ошибки превращалось в многостраничный отчёт о внутренней кухне компилятора и о том, как он работает с шаблонами. Теперь requires поменяли эту модель, позволяя описывать ожидания от типа явно и прямо в объявлении функции или класса.
Нескучное программирование: Обобщения (WIP)
Нескучное программирование: Концепты и ограничения <= Вы тут
Нескучное программирование: Иерархия концептов
Нескучное программирование: Запреты и концепты
. . .
По сути requires — это способ наложить ограничения на параметры шаблона средствами языка, формулируя некоторый контракт: «возьми такой T, который умеет вот это и вот это». Если тип не удовлетворяет этим условиям, шаблон даже не будет участвовать в разрешении перегрузок, когда мы исключаем выбранную ветку инстанцирования не через ошибку подстановки (SFINAE), а через исключение неподходящего варианта ещё на этапе выбора перегрузки. Помогает это не только компилятору ограничить множество возможных вариантов, но и нам самим, потому что теперь компилятор может выдать короткое и осмысленное сообщение, а не поток вторичных ошибок.
Кот != кот
Давайте рассмотрим простой пример, у нас есть функция, которая проверяет равенство двух объектов одного типа. Логично ожидать, что такой тип должен поддерживать оператор ==. Если до этого ошибку "неподдержки" операции сравнения мы получим, когда компилятор перебрал все возможные варианты, пару разу свалился на SFINAE (мы этого не видим, это реальное время сборки бандла или бинарника) и только потом вывел бы простыню логов, то с помощью requires мы убираем эти накладные расходы и переносим все эти проверки "до", а не "во время":
template<typename T>
bool check_equality(const T& a, const T& b)
requires std::equality_comparable<T>
{
return a == b;
}
Здесь мы явно говорим, — эта функция существует только для тех типов T, которые удовлетворяют концепту std::equality_comparable, и если попытаться вызвать её с типом, у которого нет оператора ==, компилятор не будет пытаться «протащить» нас внутрь шаблона, а сразу сообщит: ограничение не выполнено — данный тип не является сравнимым на равенство. Это принципиально другой уровень обратной связи по сравнению с классическим C++, где аналогичная ошибка приводила бы к цепочке сообщений о том, что где-то глубоко не найден подходящий оператор.
Очень важным это становится, когда условий становится больше одного. В реальном коде функции редко предъявляют к типу только одно требование, и мы можем ожидать, что тип ведёт себя как итератор и при этом поддерживает полный порядок сравнения. Теперь такие условия можно комбинировать напрямую в объявлении функции:
template<typename T>
void resolve(const T& v)
requires std::forward_iterator<T> && std::totally_ordered<T>;
В этом случае компилятор проверяет каждое условие по отдельности, и если тип не удовлетворяет хотя бы одному из них, в диагностике будет чётко указано, какое именно требование нарушено. Это резко контрастирует со старыми SFINAE-приёмами на базе enable_if, частичных специализаций и странных выражений с хаками через decltype, где ошибка часто выглядела как «нет подходящей перегрузки», без какого-либо объяснения, почему именно она не подходит.
Упрощение "перегруженных"
Ещё один важный аспект — это разделение перегрузок по категориям типов. Например, одна версия функции работает только с целыми числами, а другая — только с числами с плавающей точкой. Используя enable_if можем написать их так:
template<typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
resolve(T x) {
// Реализация для целых чисел
}
template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, void>::type
resolve(T x) {
// Реализация для вещественных чисел
}
При таком подходе мы получаем замусоренный техническими деталями возвращаемый тип. Не имея практики работы с таким кодом, может быть непонятно без комментариев, что делает enable_if. Нам все равно приходится полагаться на шаблонную магию ::value и ::type, и в любом случае логика «что требуется» спрятана в коде функции.
Либо через SFINAE + trailing return, те же решения — вид сбоку:
template<typename T>
auto resolve(T x) -> std::enable_if_t<std::is_integral_v<T>> {
// Реализация для целых чисел
}
template<typename T>
auto resolve(T x) -> std::enable_if_t<std::is_floating_point_v<T>> {
// Реализация для вещественных чисел
}
Применив requires мы можем написать несколько функций с одинаковым именем, но с разными ограничениями, и тем самым выразить разные намерения для разных семейств типов:
template<typename T>
void resolve(T x) requires std::integral<T>;
template<typename T>
void resolve(T x) requires std::floating_point<T>;
Здесь нет трюков c enable_if или скрытых условий и читая такой код, даже без глубокого знания шаблонных механизмов, вы легко поймете замысел автора о том, что поведение функции зависит от того, к какой математической категории относится тип. Для разработчика это особенно ценно, потому что requires по сути учит четко формулировать требования к абстракциям, создавая в коде явный контракт или, если хотите ТЗ, а не прятать контракт в деталях реализации “бизнес-логики”.
Еще немного о сигнатурах
C requires если попытаться вызвать resolve() с типом, который не относится ни к целочисленным, ни к вещественным, компилятор окажется в честной и понятной ситуации: подходящей перегрузки просто не существует. Теперь компилятор не будет гадать, не станет пытаться «подогнать» тип под одну из версий и не уйдёт в дебри шаблонных подстановок, а прямо скажет, что ни одно из ограничений requires std::integral<T> и requires std::floating_point<T> не выполнено. Для разработчика это выглядит как нормальная, логичная ошибка интерфейса.
Почему же всё это вообще работает? Дело в том, что с точки зрения компилятора функции с одинаковым именем и формально одинаковыми параметрами, но с разными условиями requires, считаются разными функциями. Это может показаться неожиданным, если мы привыкли думать о перегрузке только через список параметров, но в стандарте C++ явно зафиксировано правило: две функции считаются одной и той же сущностью только в том случае, если у них эквивалентные параметры и эквивалентные условия requires.
Иначе говоря, ограничения теперь — это часть сигнатуры функции на уровне языка. Но при этом важно понимать тонкий, но принципиальный момент: requires является частью интерфейса функции, но "не участвует в манглинге/не влияет на манглинг". Это означает, что с точки зрения линковки никакого конфликта имён не возникает, а с точки зрения компилятора на этапе выбора перегрузки возникают разные кандидаты, каждый со своими условиями применимости, поэтому такой код абсолютно легален (логические имена функций внутри компилятора будут разные):
template<typename T>
void resolve(T t) requires (sizeof(T) > 4)
-> resolve_t_sizeof_gr_4
template<typename T>
void resolve(T t) requires (sizeof(T) <= 4)
-> resolve_t_sizeof_ls_4
Когда мы вызываем resolve с конкретным типом, компилятор сначала подставляет этот тип, затем проверяет ограничения и просто выбирает ту версию, для которой логическое выражение в requires истинно и здесь нет никакой магии, как это было с шаблонами, теперь это обычный отбор перегрузок, но с дополнительным фильтром в виде условий.
На этом месте важно не забыть о типичной ловушке ограничений — если условия requires не являются взаимоисключающими, мы можем легко получить неоднозначность. Можем представить себе такой пример:
template<typename T>
void resolve(T t) requires (sizeof(T) >= 3);
template<typename T>
void resolve(T t) requires (sizeof(T) <= 4);
В этом случае для типа int оба условия будут истинны одновременно, и с точки зрения компилятора обе перегрузки одинаково хороши. Ни одна из перегрузок не является более специализированной, что в результате приводит нас к ошибке неоднозначного вызова. Неоднозначный вызов — это не баг и не странность реализации, а следствие того, что мы сами описали пересекающиеся контракты, поэтому при проектировании интерфейсов с requires нужно составлять их максимально четкими: либо условия должны быть строго разделены, либо одна версия должна быть более строгой и явно доминировать над другой.
Обсудим еще одну важную сторону requires, который умеет быть не только простым логическим фильтром, как в примерах выше, но и инструментом проверки корректности выражений. Особенно, когда нас интересует не числовые условия вроде sizeof(T) > 4, а сам факт существования операции или выражения для заданного типа. Для этого в языке предусмотрены так называемые requires-выражения, которые чаще всего используются при определении концептов: мы можем описать концепт сравнимости на равенство следующим образом:
template<typename T, typename U>
concept entity_comparable = requires(T a, U b) {
{ a == b } -> std::convertible_to<bool>;
};
Больше похоже на псевдокод, чем на обычные плюсы. Но тут просто условие компилятору, что для типов T и U должны существовать объекты a и b такие, что выражение a == b компилируется, и его результат можно привести к bool, и если хотя бы одно из этих требований не выполняется (оператор == не определён или возвращает странный тип) контракт считается невыполненным.
Как и в случаях выше ошибка будет локальной и точечной, и компилятор укажет именно на то выражение, которое не удалось проверить, что позволяет формулировать требования к типам в терминах языка, а не в терминах побочных эффектов компиляции, вроде подстановки void или SFINAE. Мы описываем не то, «что сломается, если тип неподходящий», а то, «каким должен быть подходящий тип», и именно поэтому requires и концепты так хорошо подходят для замены шаблонов на уже работающих проектах, уточняя область работы и делая её строже, вместо старого подхода «попробуем и посмотрим, что скажет компилятор».
Простые и сложные requires
Если вы только начинаете работать с requires, то в простейшем варианте это будет выглядеть как обычное логическое выражение, вычисляемое во время компиляции, которое выглядит и ведёт себя так же, как constexpr bool. Мы проверяем некоторое свойство типа, получаем истину или ложь, и в зависимости от этого функция либо участвует в перегрузке, либо нет. Большинство замещений условий шаблонов сводится к таким случаям: проверки размеров, выравнивания, принадлежность к категории типов, различные std::is_* и концепты стандартной библиотеки, которые сами по себе сводятся к логическим условиям.
Но, поработав какое-то время, вы заметите, что есть и более мощная, «сложная», форма requires, когда нас интересует не просто значение некоторого предиката, а корректность самой операции. Тогда мы спрашиваем компилятор: «а можно ли вообще написать вот такое выражение для этого типа?»; «если можно, то что оно возвращает?»; «обладает ли оно дополнительными свойствами, например, гарантией отсутствия исключений?». Это уже не абстрактная логика, а прямая проверка синтаксиса и семантики кода на этапе компиляции.
Конструкция requires { ++a; } означает: для данного типа должна существовать операция префиксного инкремента, и выражение ++a должно быть корректным. Мы ничего не говорим о том, что оно возвращает, и ничего не говорим о побочных свойствах в простом случае. Нас просто интересует сам факт применимости операции ++, но если же мы пишем requires { ++a } noexcept; то добавляем ещё одно, более тонкое требование: операция не только должна существовать, но и быть помеченной как не выбрасывающая исключений, приводя нас таким образом к тому факту, что формулировать требования к логике работы можно на уровне контрактов.
Когда требований становится много, писать их прямо в объявлении функции становится неудобно и плохо читаемо. Длинные цепочки условий в requires быстро превращаются в шум, за которым сложно понять, что же в итоге хотел донести автор, поэтому в язык были введены концепты. Concept — это, по сути, именованный набор требований, которому мы даём осмысленное имя и затем используем его как строительный блок уже для других вычислений. Пусть мы хотим выразить идею «тип T может быть неявно преобразован к типу U». Мы можем оформить это как концепт:
template<typename T, typename U>
concept CanConvertTo = std::is_convertible_v<T, U>;
После этого имя CanConvertTo становится частью нашего словаря концептов, и мы можем использовать его в шаблонных параметрах, не раскрывая каждый раз детали проверки.
template<CanConvertTo<int> T>
void resolve(T value);
А можем пойти ещё дальше и воспользоваться упрощенным синтаксисом с auto, который делает код почти обычным и почти понятным, даже не сильно искушенному в языке человеку:
void foo(CanConvertTo<int> auto value);
С точки зрения читающего этот код, мы избавились даже от объявления шаблонов и свели ограничения к тому, что функция принимает не «какой-то шаблонный тип», а значение, которое можно преобразовать к int. То, что раньше приходилось выражать через двухэтажный шаблон, теперь выражено прямо в сигнатуре, без необходимости читать реализацию или комментарии.
За всеми этими кульбитами с шаблонами, ограничениями и новым синтаксисом важно помнить, что концепты — это не какой-то особый вид типов и не новая категория сущностей. Concept, как до этого часть обработки шаблонов, — это просто предикат времени компиляции, то есть фактически это булево выражение, зависящее от параметров шаблона. Именно поэтому его можно использовать не только в списке параметров функции, но и в самых разных местах кода, например, в static_assert, чтобы зафиксировать важное условие выполнения логики:
static_assert(CanConvertTo<int> T);
Таким утверждением мы явно заявляем: дальнейший код имеет смысл только в том случае, если T может быть конвертирован к int и если это перестанет быть верным, например, после чьего-то рефакторинга, то ошибка возникнет сразу и в понятном месте. Точно так же концепт можно использовать в if constexpr, чтобы выбирать разные ветки реализации в зависимости от свойств типа, или сохранить результат проверки в constexpr bool, если это улучшает читаемость. И во всех этих случаях концепты работают как именованные, проверяемые компилятором условия, а не как неформальные договорённости между разработчиками.
Сложные requires и посложнее
Наконец, стоит рассмотреть более сложные случаи, когда requires используются не для одной-двух-трех проверок, а как полноценный язык описания требований, но тут важно понимать, что requires умеют проверять не только корректность выражений, но и само существование типов, вложенных объявлений и связанных с ними операций. Кроме того, внутри одного requires-блока можно объединять несколько условий, и все они должны быть выполнены одновременно. Сложно? Давайте разберемся.
Допустим нас интересует не просто тип T, а то, что он ведёт себя как контейнер с определённым интерфейсом. Мы можем проверить, что у типа существует вложенный тип iterator, и что разыменование такого итератора даёт значение, приводимое к int:
requires {
typename T::iterator;
{ *std::declval<T::iterator>() } -> CanConvertTo<int>;
}
Cтрока — typename T::iterator; — это требование к типу. Она означает: у T должен существовать вложенный тип с таким именем. Если его нет, условие requires не выполнено, и соответствующая функция или шаблон просто не рассматриваются компилятором. Вторая строка уже проверяет выражение: мы берём воображаемый итератор этого типа, разыменовываем его и проверяем, что результат можно привести к int. Таким образом, мы одновременно накладываем архитектурное требование (должен быть класс, которые содержит итератор), структурное требование (итератор должен поддерживать операцию *) и семантическое (результат должен конвертиться к int).
Такие requires-блоки уже можно рассматривать как компактное описание мини-интерфейса, когда «этот контейнер» не абстрактный, а с точно фиксированным поведением, какие элементы интерфейса нам нужны и в каком виде, причём все требования объединяются логическим И: если не выполнено хотя бы одно, весь блок считается ложным.
К тем же составным требованиям относятся проверки возвращаемых типов и свойств операций, как в примере с -> std::convertible_to<...>, который задаёт ограничения на результат выражения. Там же можно указывать noexcept, получив другое поведение и другой контракт, если для нас принципиально, чтобы операция не выбрасывала исключений. В конечном итоге можно формулировать очень точные контракты (тут тоже важно не перестараться): не просто «операция существует», а «она существует, возвращает нужный тип и не выбрасывает исключения».
Это приводит нас к тому, что такие вложенные и составные requires-блоки выносятся в отдельные концепты с говорящими именами, после чего используются в сигнатурах функций и классов, позволяя сохранить баланс между выразительностью языка и читаемостью кода: сложная логика проверки остаётся в одном месте, а интерфейс и логика работы там, где была.
Итог
Если попробовать подвести итог, то requires и концепты дают программисту язык для разговора с компилятором на уровне четких условий, а не размытых образов в стиле, "если этот не подошел, пробуй дальше". Также как мы можем управлять перегрузками, точно описывать ожидания от типов и проверять их автоматически во время компиляции, мы теперь можем, не прибегая к громоздким и сложным шаблонным приёмам вроде SFINAE, описывать какие функции и с каким результатом нам нужны.
В результате код становится одновременно строже и яснее: строже, потому что требования проверяются формально, и яснее, потому что эти требования теперь видны прямо в интерфейсе, а не спрятаны в деталях реализации. А вот насколько строже и насколько яснее разберем в следующей статье...
upd: благодарю @Serpentineза редактуру и вычитку
Комментарии (12)

AbitLogic
02.01.2026 15:39"Здесь мы явно говорим, — эта функция существует только для тех типов T, которые удовлетворяют концепту std::equality_comparable, и если попытаться вызвать её с типом, у которого нет оператора ==, компилятор не будет пытаться «протащить» нас внутрь шаблона, а сразу сообщит: ограничение не выполнено — данный тип не является сравнимым на равенство."
Очевидно какая-то попытка получить аналог трейтов из Rust, аля PartialEq, только в отсутствие сильной типизации на уровне компилятора прикрутить типы для типов выглядит как костыль

dalerank Автор
02.01.2026 15:39Тут просто важно напомнить, откуда всё это взялось. Плюсы все же очень старый язык (40+?) и он десятилетиями развивался так, чтобы ничего не ломать из уже написанного кода, ну и он тоже из ниоткруда не появился, а взял часть сях, часть smalltalk, tcl и чего там еще было в начале, поэтому он не может просто взять и «переделать систему типов заново», вы банально все сломаете и даже ваша кошка будет смотреть на вас осуждающе. Поэтому все новые штуки, вроде концептов concepts, приходится аккуратно надстраивать поверх старых шаблонов, SFINAE и прочего исторического багажа и ничего с этим не поделать. Может из-за этого они иногда выглядят как костыли, хотя по сути это эволюция.
Rust оказался, кмк, в куда более удобной позиции, поскольку стартовал с чистого листа. Можно было сразу сказать: вот у нас traits, вот тут такие правила, вот вам такая строгая модель типов и никаких компромиссов ради кода, написанного дцать а то и больше лет назад, поэтому всё выглядит «правильно».
При этом rust на самом деле очень много взял из C++, тотже RAII, zero-cost abstractions, общее мышление про производительность, но он смог взять эти идеи уже в отшлифованном виде и встроить их в язык сразу, а не задним числом и не через три п.... извините за мой французский.
Так что концепты в плюсах это не попытка догнать rust, а попытка как-то наконец-то официально оформить то, чем люди пользовались годами, если не десятилетиями, через enable_if, type traits и прочие трюки. Ну да, выглядит менее элегантно, но работает и не ломает экосистему.
В общем, rust просто повезло родиться позже и у хороших родителей и навеное это нормально. Если я тут что-то про rust обидное сказал, прошу извинить, язык не использую, иногда почитываю блоги.

AbitLogic
02.01.2026 15:39В принципе у Вас здоровое мнение, ничего плохого не вижу, проблемой плюсов тоже считаю легаси, но взгляните на Си, они хоть и имеют легаси куда большую, я бы предпочтитал писать на них - потому что не метались за совершенное, я открываю С++ теряюсь в их glvalue, xvalue, prvalue, одна move-семантика чего стоит, у вас на руках была нормальная move-семантика, нет нужно изобретать свою, лишь бы работало старое, дальше initialization list, что это? В каких скобках {} или () какой из убойного набора конструкторов вызовется, ну честно, какие-то гикки до хрипоты спорят... Как на этом писать, в Rust невозможны вопросы "что выведет эта программа", там все инструкции явные, сильная, статическая, строгая типизация, никаких неявных преобразований, что написано то и будет

feelamee
02.01.2026 15:39в Rust невозможны вопросы "что выведет эта программа", там все инструкции явные
Думаю вы сильно ошибаетесь считая что в Rust все определенно ясно. Да и Rust вроде бы не делает на этом упор как Zig.
Пара примеров неочевидных вещей:
Загадка. Одна из этих строк не скомпилируется. Какая?
let a1: _ = [b"a", b"a" as &[u8]]; let b1: _ = [b"a" as &[u8], b"a"]; let a2: [_; 2] = [b"a", b"a" as &[u8]]; let b2: [_; 2] = [b"a" as &[u8], b"a"]; let a3: [&[u8]; 2] = [b"a", b"a" as &[u8]]; let b3: [&[u8]; 2] = [b"a" as &[u8], b"a"];Имеете доступ к структуре за иммутабельной ссылкой, но хотите мутировать ее поле? Отрицаете любые авторитеты и хотите назло маме отморозить уши? Вас спасет баг rustc, заставляющий его автодополнять unsafe перед фигурными скобками:
struct Struct { field: i32, } fn mutate_behind_immutable_reference(r: &Struct) { let r = &mut { r.field }; *r = 123; }И никакой borrow checker вам больше не помеха!
Такие скользские и неочевидные места нормальны и появляются когда язык или технология развивается. С этим ничего не поделаешь т.к. невозможно знать в самом начале как и что будет добавлено в язык в будущем. Впрочем у Rust есть твердный фундамент из базовых концепций, который сильно помогает с этим
Примеры взяты из https://t.me/alisa_rummages
Там еще парочка есть
mayorovp
02.01.2026 15:39Имеете доступ к структуре за иммутабельной ссылкой, но хотите мутировать ее поле? Отрицаете любые авторитеты и хотите назло маме отморозить уши? Вас спасет баг rustc, заставляющий его автодополнять unsafe перед фигурными скобками
Превращения неизменяемой ссылки в изменяемую тут нет, как и автодополнения unsafe. В приведённом вами коде создаётся ссылка на временный объект. Но более понятным код от этого, увы, не становится.

Mingun
02.01.2026 15:39ничего с этим не поделать
Поделать просто -- не пытаться компилировать все юниты компиляции в одной модели, а разделить -- это компилируем по старым правилам, а это по новым. Уровень оптимизации ведь для разных файлов по разному настраивать можно? Почему нельзя настроить и остальное? Просто удивительно, что все вокруг это уже поняли, и только Комитет упорно пытается впихнуть невпихуемое из года в год. Худо-бедно у него это получается, но все больше разработчиков вопрошает, что там курят.

dalerank Автор
02.01.2026 15:39Вы не один это видите - это понимают вообще все, в том числе и комитет (я както говорил на конференции с @antoshkka об этом, там работают очень умные люди, которые прекрасно видят и технический долг и архитектурные проблемы языка) Радикальный разрыв тут просто невозможен, пока язык реально используется в проде, и именно поэтому вместо красивого перезапуска мы видим concepts, modules и constraints, в виде медленных шажков. Разбивка по файлам, о которой Вы сказали и TU это чисто про генерацию кода, а не про семантику языка (name lookup, overload resolution, правила шаблонов, ADL, ODR, ABI) должно быть одинаковым во всей программе, иначе компоновка будет давать разные результаты при каждой перекомпиляции (это один из моментов), если ввести разные модели языка, условно старую и новую, то у нас отваливается ODR (тут вам больше ребята с конференций расскажут). И вы забываете про вендоров, если ктото из большой тройки говорит - нет, мы не можем это сделать, комитет под козырек и откладывает на следующий стандарт.

Jijiki
02.01.2026 15:39на самом деле не поделать, потомучто, обратная совместимость, Раст пользуется миром с/с++, я по началу тоже думал всё просто, поэтому ввели концепты, но модели исполнения кода совершенно разные, образно у нас есть Форт на стероидах, и прямое исполнение
проблема если учесть нового игрока Раст звучит теперь так, надо изменить неизменяемое, из-за модели исполнения Раста, это практически невозможно
Раст кстати проще С++, из-за доказательств перед исполнением, Раст практически функционален как мне кажется, тут не совсем даже синглтон как в С++, оно вроде похоже на синглтон из мира С++, но тут нормально так сделали, что даже не чувствуется тех проблем ), тем кому не нравятся синглтоны и зависимые структуры данных, даже не знаю, как в стиле ООП писать на Раст
потестил Раст всё в одной структурке компактно новейший Opengl, новейшие воксели )
но кстати если забить на Раст и подумать о Хаскель, то многое пофиксили поидее уже давно, правда я не смотрел как там пишут библиотеки, тоже наверно бинды, над низкоуровневыми интерфейсами
да и редактор Zed, оказался вообще клёвым ), такими темпами Раст действительно становится удобным инструментом, семантика мув тут просто классно работает

Astrowalk
02.01.2026 15:39Всё верно. Туториал хороший, но я тут в тысячный раз восславил Кнута за своё решение несколько лет назад перейти на Rust.
Adequateeee
Не плохо, мне зашло