Эта статья была написана Сайедом Атифом Мехди, членом группы технического контента Educative.

Введение

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

В этом блоге мы будем обсуждать concept на различных примерах.

concept

concept — это новая функция языка, представленная в C++20. Он определен в заголовке <concepts>. Он предоставляет способ определения требований к аргументам шаблона, что позволяет создавать более краткий и удобочитаемый код. Это достигается за счет предоставления программисту возможности указывать ограничения на параметры шаблона.

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

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

Определение concept

concept можно легко определить следующим образом:

template <typename T>
concept Integral = std::is_integral_v<T>;

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

template <Integral T>
T Add(T lhs, T rhs) 
{
    return lhs + rhs;
}

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

int main()
{
/*
Integral Types are: 

char , signed char , unsigned char -8 bits
short int , signed short int , and unsigned short int -16 bits
int , signed int , and unsigned int -32 bits
long int , signed long int , and unsigned long int -32 bits (OpenVMS)
long int , signed long int , and unsigned long int -64 bits (Digital UNIX)
signed __int64 (Alpha) and unsigned __int64 (Alpha)-64 bits
enum -32 bits
*/
    std::cout<<"The sum of two integers is "<< Add(10, 20)<<"\n";
    std::cout<<"The sum of two 8 bit characters converted to ASCII is "<< Add('a', 'b')<<"\n";
    //Following line will generate an error as floating point is not an integral value.
//    std::cout<<"The sum of two doubles is "<< Add(10.2, 20.3)<<"\n";  

    return 0;
}

В строках 14–15 приведенного выше кода сумма двух int и двух char была выполнена с помощью функции Add. Код успешно сгенерирует правильный вывод, как показано ниже. Помните, что символы ASCII являются 8-битными, и будет переполнение.

Однако, если строка 17 раскомментирована, код выдаст ошибку времени компиляции, как показано ниже. Функция Add определена для целых значений, но не для значений с плавающей запятой.

Включая список ограничений

concept также можно использовать для включения списка ограничений. Это можно сделать с помощью requires. Следующий код определяет концепцию String, которая требует, чтобы тип T имел функцию-член c_str, возвращающую const char *.

Функция шаблона print_string определяется с использованием концепции String. Важно отметить использование requires в определении функции. Это гарантирует, что T будет следовать концепции String. Функция print_string печатает value с помощью функции c_str().

template <typename T>
concept String = requires(T s) 
{
    { s.c_str() } -> std::convertible_to<const char*>;
};

template <typename T>
requires String<T>
void print_string(T value) 
{
    std::cout << value.c_str();
}

Пользовательские типы данных

Давайте возьмем еще один пример, объясняющий, как концепция с оператором + работает с определенным типом. В этом коде requires используется для включения списка ограничений.

template <typename T>
concept Addable = requires(T x, T y) 
{
    {x + y} -> std::convertible_to<T>;
};

В приведенном выше примере концепция Addable требует двух объектов типа T и помечена как requires. Выражение {x + y} должно быть допустимым оператором, а результат оператора может быть преобразован в тот же тип T, что и x и y.

Используя эту концепцию Addable, мы можем написать функцию, которая требует добавления аргументов.

template <Addable T>
T Sum(T a, T b) 
{
    return a + b;
}

В приведенном выше примере функция Sum принимает два аргумента типа T и требует, чтобы T удовлетворяло концепции Addable. Это означает, что оператор + гарантированно доступен для аргументов, и функцию можно использовать с любым типом, удовлетворяющим концепции.

Следующий код использует концепцию и функцию, определенные выше, для вычисления суммы двух чисел Rational.

struct Rational 
{
    int Numerator;
    int Denominator;
    
    Rational operator + (const Rational & rhs) const 
    {
        return {Numerator * rhs.Denominator + Denominator * rhs.Numerator, Denominator * rhs.Denominator};
    }
};

template <>
struct std::common_type<Rational> 
{
    using type = Rational;
};

int main()
{
    Rational num1 {1, 2};
    Rational num2 {1, 3};
    Rational result = Sum (num1, num2);
    std::cout <<"The sum of two rational numbers is "<< result.Numerator << "/" << result.Denominator <<"\n"; 
    // Output: The sum of two rational numbers is 5/6
    std::cout<<"The sum of two integers is "<< Sum(10, 20)<<"\n";
    // Output: The sum of two integers is 30
    std::cout<<"The sum of two doubles is "<< Sum(10.5, 20.9)<<"\n";
    // Output: The sum of two doubles is 31.4
    return 0;
}

В строках 1–10 определен пользовательский тип Rational, представляющий рациональное число. В структуре реализован оператор + в строках 6–9, удовлетворяющий требованиям концепции Addable.

Наконец, была предоставлена ​​специализация std::common_type, чтобы указать, что общий тип двух объектов Rational также является Rational. В функции main созданы и инициализированы два объекта типа Rational. Функция Sum была вызвана с использованием этих двух объектов. Функция вызывает перегруженный оператор + и возвращает результат сложения, который сохраняется в другом объекте result и выводится на консоль.

Кроме того, два примера примитивных типов данных, int и double, также были предоставлены для уточнения использования concept. Поскольку в этом примере был определен concept оператора +, concept можно использовать для обеспечения доступности других операторов для разных типов данных.

Уточнение

Возьмем другой пример, где concept можно использовать для уточнения. В следующем коде определен Range, который требует, чтобы тип T реализовывал функции begin и end, возвращающие iterator того же типа.

template <typename T>
concept Range = requires(T t) 
{
    { t.begin() } -> std::same_as<typename T::iterator>;
    { t.end() } -> std::same_as<typename T::iterator>;
};

template <Range R>
void print_range(const R & lhs) 
{
    for (auto it = lhs.begin(); it != lhs.end(); it++) 
        std::cout << *it << " ";
}

Функция print_range в строках 9–13 реализует концепцию Range для вывода всего содержимого контейнера, начиная с первого и заканчивая последним элементом.

int main() {
    // Initializing a vector with C++20 initializer syntax
    std::vector<int> vec{ 1, 2, 3, 4, 5 };
    print_range(vec);
    //Output: 1 2 3 4 5
    //int array[] = {1,2,3,4,5};
    //print_range(array);
    // Compile-time error: no matching function for call to ‘print_range(int [5])’

    return 0;
}

В строке 3 определено и инициализировано vector. В строке 4 была вызвана функция print_range для печати vector. Выход по желанию. Однако, когда строки 6–7 раскомментированы, во время компиляции генерируется ошибка, указывающая на то, что для массива int не существует соответствующей функции, поскольку в нем нет функций begin и end. Это предотвращает любую ошибку времени выполнения, которая могла привести к прекращению выполнения.

Заключение

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

Мы надеемся, что этот блог послужил толчком к изучению C++ 20. Для дальнейшего чтения, пожалуйста, перейдите к следующим курсам:

Как всегда, приятного обучения!