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)
Flux
01.06.2023 17:24+4Многомерный индексатор конечно был ожидаемой фичей.
Вы не можете просто взять и написать
arr[0, 1, 2]
Вообще-то могу, посредством перегрузки оператора запятой для упаковки индексов в тапл. Костыль конечно, но до 17-го стандарта работает, в 20-м задепрекейчен ради этой самой фичи в 23-м.
threepointsix
01.06.2023 17:24overload the subscript operator with a brace-enclosed list (so you could say
c[{1,2,3}]
)Перегрузить оператор индексирования со списком, заключенным в фигурные скобки (чтобы вы могли написать
с[{1,2,3}]
).Вы кажется текст на англ забыли удалить
buldo
01.06.2023 17:24А в C++ планируется что-то типа шарповых Property? Чтобы за одним именем скрывался и и метод чтения и метод записи?
Я пытался найти какой-то устоявшийся паттерн для работы с полями класса в C++, но как-то ничего не нашёл лучше пары методов getSmth() / setSmth(). Может кто подскажет, как в современных плюсах правильно и канонично такое реализовывать?
cdriper
01.06.2023 17:24+2вот это лучший ответ на ваш вопрос
SIISII
01.06.2023 17:24+2Костыльно-с. Как, впрочем, половина в Це++. Постоянно удивляюсь: добавить что-то навороченное комитет по стандартизации может, а простое и очевидное... религия, что ли, не позволяет?..
cdriper
01.06.2023 17:24-2property это сахар
уверен, что предложений был уже миллион, но без этого сахара много лет жили и еще столько же проживем
SIISII
01.06.2023 17:24+2Ну дык и Си -- тоже сахар. И ассемблер тоже. Что мешало писать прямо в машинном коде?
artemSitnikov
01.06.2023 17:24Всё правильно: у нас же есть форточка, можно лазить через неё, а дверь это сахар. Много лет жили без двери и ещё столько же проживём.
qw1
01.06.2023 17:24Дверь это не сахар, потому что у неё, по сравнению с форточкой, лучше объективные измеримые характеристики: пропускная способность в человеках в час и энергозатраты на один проход.
SIISII
01.06.2023 17:24Тогда property -- тоже не сахар, потому что написать код с его помощью быстрее, проще, понятнее и безошибочнее, чем костылями.
qw1
01.06.2023 17:24Свойства — это прежде всего инструмент. В языках типа c# (и в java через костыли get/set) взамодействие с объектами через свойства очень часто является обязательным требованием у фреймворков сериализации (DataContractSerializer), ORM (NHibernate) или биндинга на GUI (WPF). Потому что рантайм может разными способами перехватывать вызовы методов и таким образом реагировать на изменение свойств. В C++ нет динамического рантайма, нет JIT-компиляции, нет рефлексии, поэтому свойства там не будут утилитарные, а останется чисто синтаксический сахарок.
SIISII
А в Паскале многомерные массивы были 50+ лет назад. Да и модули Турбо Паскаля вменяемее и существуют лет 30...
qw1
В Си многомерные массивы были 40+ лет назад. Но тут речь идёт о пользовательских перегрузках операторов. В Паскале нет возможности, чтобы
Screen[x,y] := RGB(1,2,0);
ставил точку на экране по указанным координатам.KanuTaH
Насколько я помню, даже в относительно развивающемся FreePascal до сих пор в принципе невозможно определить оператор
[]
для объекта произвольного типа. С точки зрения Паскаля[]
- это вообще не оператор.qw1
В дельфи есть хорошая замена через индексируемые свойства
Получается, насчёт примера со Screen двумя сообщениями выше я был не прав.
Можно обращаться к объекту Screen:
а если описать как
то и просто
SIISII
Вообще, с терминологией могут быть проблемы. Я лично придерживаюсь того, что операторы -- это if, while и т.п., ну а плюсы-минусы -- это операции. Соответственно, присваивание в Паскале -- оператор, а в Си -- операция. Ну а что возможности переопределения в Дельфях/ФриПаскале сильно ограничены -- тут да; развитие языка, как по мне, пошло по абсолютно неверному пути (стали подражать Жабе, похоже, -- а она ущербна по определению, ибо "управляемый" язык для работы на виртмашине, а не относительно низкоуровневый для работе на голом железе, как и классический Паскаль, и Си).
qw1
Проблема русского языка. На английском это statements и operators.
Mingun
Проблема перевода. Назвали бы по-нормальному — инструкция (statement) и оператор (operator) — и никакой путаницы.
SIISII
О терминах не спорят, о терминах договариваются.
SIISII
Массивы массивов были, многомерные массивы -- нет. Ну а что в Паскале много чего нет -- оно понятно. Идеального языка вообще нет (и быть не может, по большому счёту, ибо требования противоречивы).
qw1
Но ведь на практике вообще нет никакой разницы, кроме синтаксиса A[x][y] вместо A[x,y].
SIISII
Ну, более громоздкий и менее читаемый синтаксис тоже не есть хорошо. Да и с переопределением операций проблемы.
sergio_nsk
Где так там громоздкость? Принято ставить пробел после запятой, и вот A[x][y] и A[x, y] уже равны. Это сделано не из-за громоздкости, а ради перегрузки оператора.
qw1
Нет, всё же оператор двойного индекса это синтаксический сахар.
Потому что без проблем для опеределения пользовательской операции A[x][y] можно перегрузить A[x], чтобы возвращал std::pair<A&&, decltype(x)> и к этому классу (необязательно std::pair, можно написать свой, чтобы не путался с STL) сделать перегруженный operator[], где уже будет доступно 2 параметра x и y. Из-за форвардинга A&& на этом нет никакого оверхеда, в сравнении с прямой перегрузкой оператора [] с двумя индексами.