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

Суффиксы для литералов, представляющие size_t и ptrdiff_t

std::size_t представляет собой беззнаковый тип данных (как минимум 16 бит), который может содержать максимальный размер объекта любого типа. Он может безопасно хранить индекс массива на любой платформе. Именно этот тип возвращают операторы sizeof, sizeof... и alignof.

std::ptrdiff_t является знаковым типом данных (как минимум 17 бит), который представляет собой тип результата вычитания двух указателей.

В C++23 они получили свои собственные суффиксы для целочисленных литералов.

Суффикс

Представляемый тип

Пример

uz, uZ, Uz или UZ

std::size_t

auto a = 42uz;

z или Z

знаковый std::size_t (std::ptrdiff_t)

auto b = -42z;

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

std::vector<int> v {1, 1, 2, 3, 5, 8};
for(auto i = 0u; i < v.size(); ++i)
{
   std::cout << v[i] << '\n';
}

Результатом выведения типа переменной i является unsigned int. Этот код прекрасно работает в 32-битной системе, где оба unsigned int и size_t, (возвращаемый тип функции-члена size()) являются 32-битными. Но в 64-битной системе вы получите предупреждение, а значение будет усечено, потому что unsigned int остался 32-битным, а size_t стал 64-битным.

В свою очередь, мы можем написать следующее:

std::vector<int> v {1, 1, 2, 3, 5, 8};

auto m = std::max(42, std::ssize(v)); // компилируется в 32-битной системе, но не работает в 64-битной
std::vector<int> v {1, 1, 2, 3, 5, 8};

auto m = std::max(42ll, std::ssize(v)); // компилируется в 64-битной системе, но не работает в 32-битной

Ни одна из этих двух версий не будет работать одновременно на 32-битных и 64-битных платформах.

Именно здесь в полной мере раскрывается польза новых суффиксов:

std::vector<int> v {1, 1, 2, 3, 5, 8};
for(auto i = 0uz; i < v.size(); ++i)
{
   std::cout << v[i] << '\n';
}
auto m = std::max(42z, std::ssize(v));

Этот код будет одинаково хорошо работать на всех платформах.

Больше информации:

Многомерный оператор индексирования

Время от времени нам приходится работать с многомерными контейнерами (или представлениями). Доступ к элементам в одномерном контейнере можно выполнить с помощью оператора индексирования (например, arr[0] или  v[i]). Но в случае с многомерными типами оператор индексирования работает не очень хорошо. Вы не можете просто взять и написать arr[0, 1, 2]. У вас есть следующие альтернативы:

  • Определить функцию для доступа к элементам, например at(), с любым количеством параметров (чтобы вы могли написать c.at(0, 1, 2)).

  • Перегрузить оператор вызова (чтобы можно было написать с(0, 1, 2)).

  • Перегрузить оператор индексирования со списком, заключенным в фигурные скобки (чтобы вы могли написать с[{1,2,3}]).

  • Использовать цепочку операторов доступа к массиву с одним аргументом (как, например, с [0] [1] [2]), что, вероятно, является самым худшим вариантом из вышеперечисленных.

Чтобы лучше вникнуть в суть проблемы, давайте рассмотрим класс матрицы (представляющий двумерный массив). Упрощенная реализация и использование будут выглядеть следующим образом:

template <typename T, size_t R, size_t C>
struct matrix
{
   T& operator()(size_t const r, size_t const c) noexcept
   {
      return data_[r * C + c];
   }
   T const & operator()(size_t const r, size_t const c) const noexcept
   {
      return data_[r * C + c];
   }
   static constexpr size_t Rows = R;
   static constexpr size_t Columns = C;
private:
   std::array<T, R* C> data_;
};
int main()
{
   matrix<int, 2, 3> m;
   for (size_t i = 0; i < m.Rows; ++i)
   {
      for (size_t j = 0; j < m.Columns; ++j)
      {
         m(i, j) = i * m.Columns + (j + 1);
      }
   }
   for (size_t i = 0; i < m.Rows; ++i)
   {
      for (size_t j = 0; j < m.Columns; ++j)
      {
         std::cout << m(i, j) << ' ';
      }
      std::cout << '\n';
   }
}

Мне никогда не нравился синтаксис m(i, j), но, как мне кажется, это было лучшее, что мы могли сделать до C++23. Теперь мы можем перегрузить оператор индексации с несколькими параметрами:

T& operator[](size_t const r, size_t const c) noexcept
{
   return data_[r * C + c];
}
T const & operator[](size_t const r, size_t const c) const noexcept
{
   return data_[r * C + c];
}

Теперь мы можем использовать новую реализацию matrix следующим образом:

int main()
{
   matrix<int, 3, 2> m;
   for (size_t i = 0; i < m.Rows; ++i)
   {
      for (size_t j = 0; j < m.Columns; ++j)
      {
         m[i, j] = i * m.Columns + (j + 1);
      }
   }
    
   for (size_t i = 0; i < m.Rows; ++i)
   {
      for (size_t j = 0; j < m.Columns; ++j)
      {
         std::cout << m[i, j] << ' ';
      }
       
      std::cout << '\n';
   }    
}

Как бы я хотел, чтобы это существовало еще двадцать лет назад!

Больше информации:

Функция-член contains() для string/string_view

С++ 20 добавил функции-члены starts_with() и ends_with() для std::basic_string и std::basic_string_view. Они позволяют нам проверить, начинается ли строка с данного префикса или заканчивается ли данным суффиксом.

int main()
{
   std::string text = "lorem ipsum dolor sit amet";
   std::cout << std::boolalpha;
   std::cout << text.starts_with("lorem") << '\n'; // true
   std::cout << text.starts_with("ipsum") << '\n'; // false
   std::cout << text.ends_with("dolor") << '\n';   // false
   std::cout << text.ends_with("amet") << '\n';    // true
}

К сожалению, они не могут проверить, содержит ли строка заданную подстроку. Конечно, мы можем сделать это с помощью функции find(). Но она возвращает позицию первого символа найденной подстроки или npos, в противном случае. Поэтому нам нужно организовывать проверку следующим образом:

std::cout << (text.find("dolor") != std::string::npos) << '\n';

Я нахожу эту конструкцию громоздкой и неэлегантной, особенно когда вам просто нужно узнать, содержит ли строка определенную подстроку или символ.

В C++23 ситуация наконец изменилась, так как теперь мы можем сделать это с помощью новой функции-члена contains(). Эта функция позволяет нам проверить, присутствует ли символ или подстрока в любом месте интересующей нас строки. По сути это то же самое, что и find(x) != npos. Но новый синтаксис лучше и согласуется с starts_with() и ends_with().

std::cout << text.contains("dolor") << '\n';

Больше информации:


Приглашаем на открытое занятие, посвященное знакомству с Boost. На этом уроке вы узнаете, как подключать boost в проект с помощью cmake. Также познакомитесь подробнее с библиотеками boost и научитесь их использовать. Записаться на открытый урок можно на странице курса "C++ Developer. Professional".

Комментарии (24)


  1. SIISII
    01.06.2023 17:24
    +5

    А в Паскале многомерные массивы были 50+ лет назад. Да и модули Турбо Паскаля вменяемее и существуют лет 30...


    1. qw1
      01.06.2023 17:24

      В Си многомерные массивы были 40+ лет назад. Но тут речь идёт о пользовательских перегрузках операторов. В Паскале нет возможности, чтобы Screen[x,y] := RGB(1,2,0); ставил точку на экране по указанным координатам.


      1. KanuTaH
        01.06.2023 17:24

        Насколько я помню, даже в относительно развивающемся FreePascal до сих пор в принципе невозможно определить оператор [] для объекта произвольного типа. С точки зрения Паскаля [] - это вообще не оператор.


        1. qw1
          01.06.2023 17:24
          +2

          В дельфи есть хорошая замена через индексируемые свойства


          property ByIndex[i: integer]: TField read GetFieldByIndex write SetFieldByIndex;
          property ByXY[X: integer; Y: integer]: TRGB read GetByXY write SetByXY;

          Получается, насчёт примера со Screen двумя сообщениями выше я был не прав.
          Можно обращаться к объекту Screen:


          Screen.ByXY[1,2] := RGB(1,2,3);

          а если описать как


          property ByXY[X: integer; Y: integer]: TRGB read GetByXY write SetByXY; default;

          то и просто


          Screen[1,2] := RGB(1,2,3);


        1. SIISII
          01.06.2023 17:24

          Вообще, с терминологией могут быть проблемы. Я лично придерживаюсь того, что операторы -- это if, while и т.п., ну а плюсы-минусы -- это операции. Соответственно, присваивание в Паскале -- оператор, а в Си -- операция. Ну а что возможности переопределения в Дельфях/ФриПаскале сильно ограничены -- тут да; развитие языка, как по мне, пошло по абсолютно неверному пути (стали подражать Жабе, похоже, -- а она ущербна по определению, ибо "управляемый" язык для работы на виртмашине, а не относительно низкоуровневый для работе на голом железе, как и классический Паскаль, и Си).


          1. qw1
            01.06.2023 17:24
            +1

            Проблема русского языка. На английском это statements и operators.


            1. Mingun
              01.06.2023 17:24

              Проблема перевода. Назвали бы по-нормальному — инструкция (statement) и оператор (operator) — и никакой путаницы.


              1. SIISII
                01.06.2023 17:24

                О терминах не спорят, о терминах договариваются.


      1. SIISII
        01.06.2023 17:24

        Массивы массивов были, многомерные массивы -- нет. Ну а что в Паскале много чего нет -- оно понятно. Идеального языка вообще нет (и быть не может, по большому счёту, ибо требования противоречивы).


        1. qw1
          01.06.2023 17:24

          Массивы массивов были, многомерные массивы — нет

          Но ведь на практике вообще нет никакой разницы, кроме синтаксиса A[x][y] вместо A[x,y].


          1. SIISII
            01.06.2023 17:24

            Ну, более громоздкий и менее читаемый синтаксис тоже не есть хорошо. Да и с переопределением операций проблемы.


            1. sergio_nsk
              01.06.2023 17:24

              Где так там громоздкость? Принято ставить пробел после запятой, и вот A[x][y] и A[x, y] уже равны. Это сделано не из-за громоздкости, а ради перегрузки оператора.


              1. qw1
                01.06.2023 17:24

                Нет, всё же оператор двойного индекса это синтаксический сахар.
                Потому что без проблем для опеределения пользовательской операции A[x][y] можно перегрузить A[x], чтобы возвращал std::pair<A&&, decltype(x)> и к этому классу (необязательно std::pair, можно написать свой, чтобы не путался с STL) сделать перегруженный operator[], где уже будет доступно 2 параметра x и y. Из-за форвардинга A&& на этом нет никакого оверхеда, в сравнении с прямой перегрузкой оператора [] с двумя индексами.


  1. Flux
    01.06.2023 17:24
    +4

    Многомерный индексатор конечно был ожидаемой фичей.

    Вы не можете просто взять и написать arr[0, 1, 2]

    Вообще-то могу, посредством перегрузки оператора запятой для упаковки индексов в тапл. Костыль конечно, но до 17-го стандарта работает, в 20-м задепрекейчен ради этой самой фичи в 23-м.


  1. threepointsix
    01.06.2023 17:24

    overload the subscript operator with a brace-enclosed list (so you could say c[{1,2,3}])

    Перегрузить оператор индексирования со списком, заключенным в фигурные скобки (чтобы вы могли написать с[{1,2,3}]).

    Вы кажется текст на англ забыли удалить


  1. buldo
    01.06.2023 17:24

    А в C++ планируется что-то типа шарповых Property? Чтобы за одним именем скрывался и и метод чтения и метод записи?

    Я пытался найти какой-то устоявшийся паттерн для работы с полями класса в C++, но как-то ничего не нашёл лучше пары методов getSmth() / setSmth(). Может кто подскажет, как в современных плюсах правильно и канонично такое реализовывать?


    1. cdriper
      01.06.2023 17:24
      +2

      вот это лучший ответ на ваш вопрос

      https://stackoverflow.com/a/72638697


      1. SIISII
        01.06.2023 17:24
        +2

        Костыльно-с. Как, впрочем, половина в Це++. Постоянно удивляюсь: добавить что-то навороченное комитет по стандартизации может, а простое и очевидное... религия, что ли, не позволяет?..


        1. cdriper
          01.06.2023 17:24
          -2

          property это сахар

          уверен, что предложений был уже миллион, но без этого сахара много лет жили и еще столько же проживем


          1. SIISII
            01.06.2023 17:24
            +2

            Ну дык и Си -- тоже сахар. И ассемблер тоже. Что мешало писать прямо в машинном коде?


          1. artemSitnikov
            01.06.2023 17:24

            Всё правильно: у нас же есть форточка, можно лазить через неё, а дверь это сахар. Много лет жили без двери и ещё столько же проживём.


            1. qw1
              01.06.2023 17:24

              Дверь это не сахар, потому что у неё, по сравнению с форточкой, лучше объективные измеримые характеристики: пропускная способность в человеках в час и энергозатраты на один проход.


              1. SIISII
                01.06.2023 17:24

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


                1. qw1
                  01.06.2023 17:24

                  Свойства — это прежде всего инструмент. В языках типа c# (и в java через костыли get/set) взамодействие с объектами через свойства очень часто является обязательным требованием у фреймворков сериализации (DataContractSerializer), ORM (NHibernate) или биндинга на GUI (WPF). Потому что рантайм может разными способами перехватывать вызовы методов и таким образом реагировать на изменение свойств. В C++ нет динамического рантайма, нет JIT-компиляции, нет рефлексии, поэтому свойства там не будут утилитарные, а останется чисто синтаксический сахарок.