Будущее и настоящее. C++20 — новые изменения и возможности

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

Изначально язык С++ унаследован от языка С и назывался Си с классами, что позволяло работать в объектно-ориентированной парадигме. Это было в далёком 1980 году. С этого момента язык постоянно развивался и совершенствовался, особенно начиная с С++11 версии и выше. Стали появляться возможности обобщённого и функционального программирования, также новые версии языка дали возможность использовать многопоточное и асинхронное программирование. Новая версия языка соответствует тренду развития и также предоставляет новые возможности, некоторые из которых мы рассмотрим в этой статье.

Оператор трехстороннего сравнения <=>

Оператор трёхстороннего сравнения, часто называемый spaceship operator, определён для двух переменных A и B, где A < B, A == B и A > B. В итоге компилятор может сгенерировать код для шести вариантов сравнения: <, <=, ==, !=, >, >=. Далее приведён пример из блога компании Microsoft (https://devblogs.microsoft.com/cppblog/simplify-your-code-with-rocket-science-c20s-spaceship-operator/):

struct Basics {
  int i;
  char c;
  float f;
  double d;
  auto operator<=>(const Basics&) const = default;
};

struct Arrays {
  int ai[1];
  char ac[2];
  float af[3];
  double ad[2][2];
  auto operator<=>(const Arrays&) const = default;
};

struct Bases : Basics, Arrays {
  auto operator<=>(const Bases&) const = default;
};

int main() {
  constexpr Bases a = { { 0, 'c', 1.f, 1. },
                        { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };
  constexpr Bases b = { { 0, 'c', 1.f, 1. },
                        { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };

  static_assert(a == b);
  static_assert(!(a != b));
  static_assert(!(a < b));
  static_assert(a <= b);
  static_assert(!(a > b));
  static_assert(a >= b);
}

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

Избавляемся от макросов

В целом, разработчики стандарта стараются исключить препроцессор. Как следствие, в новой версии можно не пользоваться макросами FILE и LINE, а взамен использовать std::source_location. Например:

void log() {
    const std::experimental::source_location location = std::experimental::source_location::current();
    std::cout << "FName: " << location.file_name() << std::endl;
    std::cout << "Line:  " << location.line() << std::endl;
std::cout << "Func:  " << location.function_name() << std::endl;
}

Как мы видим, код становится более единообразным, в одном стиле, с расширяемым функционалом.

Явные константы

В C++11 было введено ключевое слово constexpr, определяющее константное выражение, вычисляемое во время компиляции. Это позволило оптимизировать код и зачастую более корректно определять поведение функции еще во время компиляции.

constexpr int foo(int factor) {
    return 123 * factor;
}

const int const_factor = 10;
int non_const_factor = 20;

const int first = foo(const_factor);
const int second = foo(non_const_factor);

Здесь выражение const int first = foo(const_factor); будет вычислено в режиме компиляции, так как const_factor для компилятора является константой, а вот выражение const int second = foo(non_const_factor); будет вычислено в работающей программе, так как non_const_factor не является константой и к моменту вызова может быть любым.

В С++20 добавлен спецификатор consteval, который объявляет функцию или шаблон функции как константную функцию, то есть каждый потенциально оцениваемый вызов функции должен (прямо или косвенно) создавать выражение константы времени компиляции.

consteval int sqr ( int n )  { 
  return n * n ; 
} 
constexpr  int r = sqr ( 100 ) ;   // Все в порядке

int x =  100 ; 
int r2 = sqr ( х ) ;   // Ошибка: вызов не создает константу

consteval int sqrsqr ( int n )  { 
  return sqr ( sqr ( n ) ) ;  // На данном этапе непостоянное выражение, но все в порядке 
}

constexpr  int dblsqr ( int n )  { 
  return  2 * sqr ( n ) ;  // Ошибка: закрывающая функция не является consteval и sqr (n) не является константой 
}

С другой стороны проверки константного выражения находится новое ключевое слово constinit, которое говорит компилятору, что объект будет статически инициализирован с постоянным значением.

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

Строковые литералы как параметры шаблона

Начиная с C ++ 20, вы можете использовать строку в качестве параметра шаблона, не являющегося типом. Идея состоит в том, чтобы использовать стандартную строку basic_fixed_string, которая имеет конструктор constexpr. Конструктор constexpr позволяет ему создать экземпляр фиксированной строки во время компиляции.

template<std::basic_fixed_string T>
class Foo {
    static constexpr char const* Name = T;
public:
    void hello() const;
};

int main() {
    Foo<"Hello!"> foo;
    foo.hello();
}

Вроде бы мелочь, а приятно — не нужно производить обходных маневров и использовать лишнюю память.

malloc стал безопасен

В предыдущих версиях использование низкоуровневых функций, унаследованных из языка Си, не рекомендовалось. Проблема в том, что Си оперирует байтами, а в С++ происходит работа с объектами со своим временем жизни и областью видимости. До С++ 20 время жизни объекта начиналось после вызова оператора new. В новой версии все изменилось — принято считать, что набор низкоуровневых функций — memcpy, memmove, malloc, aligned_alloc, calloc, realloc, bit_cast, начинает время жизни объекта. Т. е. следующий код будет валиден:

struct X { int a, b; };

X *make_x() {
  X *p = (X*)malloc(sizeof(struct X));
  p->a = 1;
  p->b = 2;
  return p;
}

Т. е. у нас появляется обратная совместимость с языком Си, но относительно С++ в новой трактовке.

Различные улучшения лямбда

Относительно недавно в языке С++ появилась возможность использовать некоторые элементы из функционального программирования. Один из них — это Lambda-функции. Новый стандарт создает много улучшений для Lambda. Например:

1.Разрешить [=,this] как лямбда-захват и исключить неявный захват через [=].

До С++20, чтобы захватить все внешние переменные по значению для Lambda, использовалось [=].

struct Lambda {
     auto foo () {
         return [ = ] {std :: cout << s << std :: endl; };
    }
    std :: string s;
};

В С++20 это нежелательно — компилятор будет выдавать предупреждения, поэтому предпочтительно использовать другой синтаксис - [=,this].

struct LambdaCpp20 {
     auto foo () {
         return [ = , this ] {std :: cout << s << std :: endl; };
    }
    std :: string s;
};

2.Шаблонные лямбды

На первый взгляд, это нововведение выглядит необычно, поскольку возникает вопрос: «Зачем нам нужны лямбда-выражения шаблонов»? Ведь когда вы пишете общую лямбду [] (auto x) {return x; }, компилятор автоматически генерирует класс с шаблонным оператором вызова, например:

template  < typename T > Оператор 
T (T x) const {
     return x;
}

Но иногда необходимо определить лямбду, которая будет работать только для одного типа, например, для std::vector, тогда нам на помощь приходят лямбда-шаблоны, где вместо параметра типа можно также использовать концепции, например:

auto foo = [] < typename T > (std::vector <T>  const & vec) { 
         // делаем специфичные для вектора вещи
};

Новые атрибуты [[likely]] и[[unlikely]]

В C++20 мы получаем новые атрибуты [[likely]] и [[unlikely]], которые позволяют подсказывать оптимизатору, является ли путь выполнения более или менее вероятным.

for(size_t i=0; i < v.size(); ++i){
    if (v[i] < 0) [[likely]] sum -= sqrt(-v[i]);
    else sum += sqrt(v[i]);
}

Заключение

Это только небольшая часть новых возможностей стандарта. Безусловно, каждое из перечисленных нововведений в язык можно детально обсуждать в целой статье, что делается многими авторами. Следует упомянуть, что не вошло в данный обзор: • coroutines — ещё одна важная функция; • новая библиотека синхронизации; • кооперативное прерывание потоков; • using enum для уменьшения шума от разделения пространств имен; • частичный отказ от volatile; • концепции; • использование модулей вместо #include.

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