JavaRush /Курсы /C++ SELF /Стандартные concepts — std:...

Стандартные concepts — std::integral, std::floating_point, std::ranges::range

C++ SELF
57 уровень , 4 лекция
Открыта

1. Зачем нужны стандартные concepts

Когда начинаешь писать шаблоны, хочется верить в сказку: «Один раз написал template <typename T> — и работает для всего на свете». Реальность быстро возвращает в мир людей: внутри шаблона вы делаете x / 2 или x.size(), а потом кто-то вызывает вашу функцию со std::string или std::vector, и компилятор устраивает вам распечатку на 200 строк (и, конечно, на английском, потому что компилятор тоже разработчик).

Стандартные concepts нужны не чтобы «усложнить жизнь», а чтобы сделать код честнее: если функция предназначена только для целых чисел — пусть это будет видно прямо в сигнатуре. Если алгоритм смысл имеет только для вещественных — то же самое. Это и про читаемость, и про понятные ошибки, и про правильный контракт API. Исторически идеи «стандартных library concepts» закреплялись отдельными предложениями и решениями комитета, и в документах WG21 вы регулярно встретите упоминания “Standard library concepts” как отдельного блока работы.

Как constraints “фильтруют” типы

Когда вы видите template <std::integral T>, важно помнить: это не runtime-проверка и не if. Компилятор не запускает программу, чтобы проверить тип. Он просто решает: “эта функция вообще существует для данного T или нет”.

Вот полезная mental-модель:

flowchart TD
    A[Есть вызов функции-шаблона] --> B[Компилятор подбирает T]
    B --> C{Проверка constraints}
    C -->|выполнены| D[Инстанцирование тела шаблона]
    C -->|не выполнены| E[Функция не подходит / ошибка компиляции]

Смысл в том, что std::integral, std::floating_point и std::ranges::range — это “врата”, через которые тип либо проходит, либо нет.

2. std::integral: только целые числа

Когда мы говорим «целое число» в C++, мы обычно думаем про int. Но в языке их целая семья: short, long, long long, все unsigned-варианты, плюс char (да-да) и даже bool (и это иногда неожиданно). В шаблонах вы часто хотите ограничить функцию именно такими типами: например, для индексов, счётчиков, битовых операций, арифметики с %, целочисленного деления и так далее.

std::integral — это стандартный concept из заголовка <concepts>, который означает: «T — целочисленный тип». В учебной практике воспринимайте это как категорию типов, а не «проверку на int». То есть std::integral<int> и std::integral<long long> будут истинны, и это как раз то, что нам нужно.

Небольшая шпаргалка по смыслу:

Concept Про что Типичные представители
std::integral
«целые числа»
int, long long, unsigned, char, bool
std::floating_point
«вещественные числа»
float, double, long double

Минимальный пример: функция только для целых

Начнём с максимально простого и «честного» примера. Пусть у нас есть функция, которая считает количество “шагов”, и по смыслу должна работать только с целыми.

#include <concepts>
#include <iostream>

void print_twice(std::integral auto x) {
    std::cout << (x * 2) << '\n'; // например: 14
}

int main() {
    print_twice(7);
    // print_twice(3.14); // не компилируется: 3.14 не integral
}

Обратите внимание на форму std::integral auto x. Это «ограниченный placeholder»: читается почти как обычная функция, но контракт встроен прямо в тип параметра.

Пример в DataBox: читаем ровно N значений

Давайте закрепим идею на приложении. Представим, что по ходу курса мы делаем консольную утилиту DataBox — маленький анализатор данных. Он читает числа и печатает статистику. До concepts мы обычно писали int-версию и double-версию отдельно, а теперь хотим аккуратно обобщить.

Для начала введём функцию, которая читает ровно N значений. N — это число элементов, и это классический кандидат на std::integral.

#include <concepts>
#include <iostream>
#include <vector>

std::vector<int> read_n_values(std::integral auto n) {
    std::vector<int> v;
    for (int i = 0; i < n; ++i) {
        int x = 0;
        std::cin >> x;
        v.push_back(x);
    }
    return v;
}

Да, внутри мы всё ещё используем int i, и да, тут есть нюансы (например, если nlong long). Но сейчас нам важен смысл: параметр количества должен быть целым, и это видно из сигнатуры.

Нюанс: bool тоже integral

В какой-то момент вы можете удивиться: «Почему моя функция принимает true?» А потому что bool относится к целочисленным типам. И это не баг: в C++ исторически так сложилось, и concept отражает реальность языка.

В практическом коде это означает простую дисциплину: если функция принимает «целое число, но не bool», то одного std::integral мало — нужно добавлять дополнительные ограничения (но это уже тема про комбинации constraints и более точные требования; сегодня мы держим фокус на стандартных кирпичиках).

3. std::floating_point: для вещественных чисел

Вещественные числа — это те, у которых есть дробная часть и связанные с ней радости жизни: погрешности, NaN, бесконечности и «почему 0.1 + 0.2 не равно 0.3». Вы уже работали с double и обсуждали точность, и поэтому логично иногда говорить компилятору: «Сюда только вещественные».

std::floating_point — стандартный concept из <concepts>, который означает: «тип — float, double или long double (с учётом cv-квалификаторов)». Это очень полезно, когда вы делаете среднее значение, дисперсию, нормализацию, проценты, работу с корнями и степенями — то есть всё, где целочисленное деление сломает смысл.

Пример: среднее значение только для вещественных

Среднее — идеальная демонстрация: на целых числах (1 + 2) / 2 даёт 1, а не 1.5. Иногда это ожидаемо, а иногда — тихая логическая ошибка.

#include <concepts>
#include <iostream>

std::floating_point auto mean2(std::floating_point auto a,
                               std::floating_point auto b) {
    return (a + b) / 2; // 2 здесь тоже floating в контексте выражения
}

int main() {
    std::cout << mean2(1.0, 2.0) << '\n'; // 1.5
    // std::cout << mean2(1, 2) << '\n';  // не компилируется
}

Здесь мы сделали интересную вещь: и параметры, и тип результата — через constrained auto. Это читается почти как “псевдокод”, и в этом его сила.

Пример в DataBox: среднее по данным

Теперь давайте подумаем как авторы DataBox. Мы хотим уметь считать среднее по std::vector<double>.

#include <concepts>
#include <iostream>
#include <vector>

double mean(const std::vector<double>& v) {
    double sum = 0.0;
    for (double x : v) {
        sum += x;
    }
    return v.empty() ? 0.0 : (sum / v.size());
}

Пока это не шаблон. Но мысль такая: «Среднее» почти всегда просит вещественный тип. Если мы потом захотим обобщить на T, то std::floating_point будет естественным ограничением.

4. Когда integral и floating_point дают разный смысл

Ограничения становятся особенно понятными, когда вы видите две реализации рядом. Это как две двери: “Вход для целых” и “Вход для вещественных”. И компилятор — ваш охранник (не самый дружелюбный, но справедливый).

Представим, что DataBox умеет делать “нормализацию на 100” (например, перевод в проценты). На целых числах это странно: вы быстро потеряете дробную часть. На вещественных — нормально.

#include <concepts>
#include <iostream>

std::floating_point auto to_percent(std::floating_point auto x) {
    return x * 100.0;
}

int main() {
    std::cout << to_percent(0.123) << '\n'; // 12.3
    // std::cout << to_percent(5) << '\n';  // не компилируется
}

И вот тут наступает тот момент, когда constraints не “запрещают”, а защищают смысл. Вы буквально говорите: «Если ты передал сюда int, то, дружище, давай остановимся и подумаем».

5. std::ranges::range: всё, по чему можно пройтись

До ranges вы уже много раз обходили std::vector циклом for, а позже узнали про range-for: for (auto& x : v). На уровне ощущений это воспринимается так: “если по объекту можно пробежаться в range-for, значит он контейнер (или что-то похожее)”. В C++ это обобщают словом range: объект, у которого можно получить начало и конец (условно, begin и end).

std::ranges::range — стандартный concept из <ranges>. Он означает: «для объекта можно получить итераторы начала и конца (через механизмы ranges)». То есть это не обязательно std::vector; это может быть std::array, std::string, std::list, какой-то view, или даже ваш собственный тип, если вы сделали ему begin/end.

В документах WG21 и в обсуждениях LWG регулярно фигурируют “range concepts” и требования к ranges::begin/ranges::end — это фундаментальная часть современного STL.

Простая функция: посчитать элементы в любом range

Да, у контейнеров часто есть .size(). Но у range в общем случае .size() может и не быть. Поэтому начнём с универсального подхода: просто пройтись и посчитать.

#include <cstddef>
#include <ranges>

std::size_t count_elements(const std::ranges::range auto& r) {
    std::size_t n = 0;
    for (const auto& x : r) {
        (void)x;
        ++n;
    }
    return n;
}

Здесь важны две мысли. Во-первых, std::ranges::range auto — это прямой контракт: “дай мне то, что можно обходить”. Во-вторых, мы не требуем ничего лишнего: ни size(), ни случайного доступа, ни индексирования.

Пример в DataBox: превью для любого range

Сделаем следующий шаг: напишем функцию, которая печатает первые несколько элементов любого range. Это удобно для отладки DataBox: мы прочитали данные — хотим быстро убедиться, что они “похожи на правду”.

#include <iostream>
#include <ranges>

void print_preview(const std::ranges::range auto& r, int limit) {
    int shown = 0;
    for (const auto& x : r) {
        if (shown >= limit) break;
        std::cout << x << ' ';
        ++shown;
    }
    std::cout << '\n';
}

Заметьте: limit мы оставили int. Здесь мы ещё не “полируем” всё до идеала — нам важно увидеть пользу std::ranges::range в реальном коде.

6. Мини-версия DataBox с constraints

Сейчас мы сделаем маленький, но цельный кусок приложения, который вы можете расширять дальше. Он будет читать N, затем N чисел, печатать превью и сумму. Мы специально выберем тип данных параметром шаблона: int или double. И ограничим этот тип стандартными concepts.

Функция чтения: только числа

Мы хотим разрешить только “числовые типы”, и на текущем этапе курса мы можем понимать это как “целые или вещественные”. Запишем constraint прямо в объявлении.

#include <concepts>
#include <iostream>
#include <vector>

template <class T>
requires (std::integral<T> || std::floating_point<T>)
std::vector<T> read_n(std::integral auto n) {
    std::vector<T> v;
    for (int i = 0; i < n; ++i) {
        T x{};
        std::cin >> x;
        v.push_back(x);
    }
    return v;
}

Обратите внимание на аккуратный баланс. Мы отдельно ограничили T как “число”, а n как “целое”. Это два разных смысла, и их полезно выражать разными constraints, а не одной большой кашей.

Сумма для range: принимаем любой range чисел

Теперь хотим суммировать что угодно, лишь бы это было range, и элементы можно было складывать. Мы сегодня держим фокус на стандартных concepts, поэтому не будем писать свой Addable — просто ограничим типы данных “числами” (integral или floating). Это честно для DataBox.

#include <concepts>
#include <ranges>

template <class R>
requires std::ranges::range<R>
auto sum_range(const R& r) {
    using T = std::ranges::range_value_t<R>;

    static_assert(std::integral<T> || std::floating_point<T>,
                  "sum_range: range elements must be integral or floating_point");

    T sum{};
    for (const auto& x : r) {
        sum += x;
    }
    return sum;
}

Да, тут мы слегка “подглядели” в std::ranges::range_value_t<R>. Но по смыслу это просто “тип элемента range”. Если воспринимать это как “T — тип того, что лежит внутри”, будет достаточно для понимания.

main: связываем всё в один сценарий

Соберём короткий main, который можно реально запустить.

#include <iostream>
#include <vector>

int main() {
    int n = 0;
    std::cin >> n;

    auto data = read_n<double>(n);

    print_preview(data, 5); // например: 1.1 2.2 3.3 4.4 5.5
    std::cout << sum_range(data) << '\n'; // например: 16.5
}

Это уже похоже на маленькую утилиту: “прочитал → показал → посчитал”. И самое приятное: если кто-то попробует сделать read_n<std::string>(n), компилятор остановит это раньше, чем вы успеете сказать “ну я просто попробовал”.

7. Типичные ошибки

Ошибка №1: путать std::integral с “это только int”.
Такое мышление обычно тянется из самых первых задач, где вы всё делали на int. В реальном C++ целых типов много, и std::integral честно принимает их все. Если в вашем API вы действительно хотите только int, то нужно писать int, а не concept. Concept — это именно “категория типов”.

Ошибка №2: использовать std::floating_point «на всякий случай», а потом удивляться, что int не проходит.
Это классическая история: вы писали формулу, она “вроде математическая”, значит “пусть будет double”. А потом выясняется, что параметр — это индекс, количество, размер, номер страницы, и это должно быть целое. Concepts хороши тем, что заставляют вас честно назвать смысл параметра.

Ошибка №3: думать, что std::ranges::range означает “у объекта есть .size()”.
Range — это про возможность получить начало и конец и пройтись. .size() может быть, а может не быть. Если вы пишете универсальный код, не подменяйте понятия. Если вам принципиально нужен размер, это уже другое ограничение (и его обычно выражают отдельными range-концептами или дополнительными требованиями).

Ошибка №4: забыть подключить нужные заголовки и обвинить concepts в “капризности”.
Для std::integral и std::floating_point нужен <concepts>. Для std::ranges::range нужен <ranges>. Если вы забудете заголовок, компилятор честно скажет, что не знает таких имён — и будет прав. Иногда это выглядит как “сломалось всё”, но на деле это просто дисциплина include’ов.

Ошибка №5: сделать ограничение на аргумент, но не проверить тип элементов range.
Например, вы написали sum_range(const std::ranges::range auto& r) и внутри делаете sum += x. Range-то есть, но элементы могут быть чем угодно — хоть std::string. Если вы заранее знаете, что хотите суммировать только числа, добавляйте проверку на тип элементов (как мы сделали через static_assert). Это не “паранойя”, это превращение требований в контракт.

1
Задача
C++ SELF, 57 уровень, 4 лекция
Недоступна
Фильтр чётности
Фильтр чётности
1
Задача
C++ SELF, 57 уровень, 4 лекция
Недоступна
Среднее измерений
Среднее измерений
1
Задача
C++ SELF, 57 уровень, 4 лекция
Недоступна
Счётчик символов
Счётчик символов
1
Задача
C++ SELF, 57 уровень, 4 лекция
Недоступна
Сумма показаний
Сумма показаний
1
Опрос
CTAD и основы concepts, 57 уровень, 4 лекция
Недоступен
CTAD и основы concepts
CTAD и основы concepts
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ