![](https://habrastorage.org/getpro/habr/upload_files/c6b/103/31d/c6b10331db5b788ee472f77d4bcee44f.png)
Несколько лет назад среди C++ блоггеров завирусилась первоапрельская шутка о том, что C++ задепрекейтил указатели (например, Fluent C++ — в C++ больше не будет указателей). Что ж, поскольку C++ почти никогда ничего не депрекейтит, это была полная умора. Однако действительно ли нам до сих пор так необходимы указатели? Именно этот вопрос я и хочу осветить в сегодняшней статье.
О чем говорят указатели
В C++ указатель сигнализирует о том, что параметр может не иметь значения. Всякий раз, когда функция получает указатель, мы должны реализовать в ее теле проверку, является ли параметр nullptr
. К сожалению, я постоянно вижу код, в котором этой проверкой пренебрегают. Никакая документация и комментарии типа “valid non-null object is required” не избавляют нас от необходимости совершать эту проверку.
Я также видел случаи, когда проверка на nullptr
в функции была опущена попросту потому что было сложно решить, что делать в случае nullptr
. Например, когда nullptr
получает функция, которая возвращает void
.
Но самое главное, что с этой проверкой мы неминуемо получаем дополнительные накладные расходы. Компилятор, по крайней насколько мне известно, не способен оптимизировать такую проверку даже в небольшой программе. Мы поговорим об этом подробнее чуть позже.
Используйте ссылки вместо указателей
Как только мы переходим на ссылки, необходимость проверки и бесконечные комментарии канут в Лету. В отличие от указателя, ссылка говорит нам, что на данном этапе требуется валидный объект.
Самый простой подход — по-прежнему получать указатели на границах API, если, например, у нас нет возможности изменить API. Но затем, в теле функции первым делом выполнять проверку на nullptr
и делать return
, если указатель null
. Если он валидный, разыменовывать указатель и сохранять в ссылке.
bool DoSomeStuff(int* data)
{
if(nullptr == data) { return false; }
int& refData = *data;
return HandleData(refData);
}
Таким образом, у нас хотя бы есть возможность сделать наш код и внутреннее API чуть-чуть опрятнее. Возможно, в следующем релизе мы сможем наводить порядок и в общедоступном API.
Постойте, мне нужен опциональный параметр
Хорошо, допустим, мы изменим все указатели на ссылки. Но что, если нам нужен параметр, который можно не предоставлять (maybe parameter)? Ничего не приходит на ум? Именно, опциональность! В этом случае C++17 предоставляет нам std::optional
. Поэтому, пожалуйста, перестаньте злоупотреблять указателями, когда хотите указать, что параметр является необязательным. Не нужно преобразовывать int
в int
* только для того, чтобы для сравнения было доступно значение nullptr
.
bool DoSomeStuff(std::optional<int> data)
{
if(data.has_value()) { return HandleData(data.value()); }
return false;
}
Тип данных std::optional
подходит для этих целей гораздо лучше, чем указатели. Такие функции, как get_value_or
, избавляют нас от необходимости добавления if
, который будет устанавливать либо переданное значение, либо значение по умолчанию.
Хорошо, а что на счет массивов? Допустим, мы хотим передать в функцию массив — здесь мы не можем использовать ссылки, за исключением тех случаев, когда мы делаем это через шаблон. О, и, пожалуйста, не вспоминайте о std::array
, потому что я хочу, чтобы эту функцию можно было вызывать с различными размерами массивов. Похоже здесь нам уж точно не обойтись без указателей!
void IWantPointers(const char* data, const size_t length)
{
for(int i = 0; i < length; ++i) { std::cout << data[i]; }
}
void Use()
{
char data[]{"Hello, Pointers\n"};
IWantPointers(data, sizeof(data));
}
span и string_view спешат на помощь
А вот и нет. По крайней мере, нам не нужны указатели в API функции. C++20 предоставляет нам std::span
для случаев, когда мы хотим передать массив или непрерывный контейнер (в этом примере мы также можем использовать std::string_view
из C++17). Преимущество std::span
заключается в том, что он содержит информацию о количестве элементов данных. Таким образом, нам не нужен дополнительный параметр под размер массива и нужно на порядок меньше sizeof
.
void IWantPointers(std::span<const char> data)
{
for(const auto& c : data) { std::cout << c; }
}
void Use()
{
char data[]{"Hello, Pointers\n"};
IWantPointers(data);
}
Думаю, сейчас мы находимся на этапе, когда можно сказать, что у нас практически нет необходимости задействовать указатели в API верхнего уровня. С такими вспомогательными типами, как std::optional
и std::span
, наша жизнь стала намного лучше. И да, указатели по-прежнему существуют в C++, и что бы я здесь не говорил, их право на существование неоспоримо. К примеру, std::span
принимает и возвращает указатель.
Почему меня это так волнует?
Ну, мне просто нравятся чистые и выразительные API. Что мне еще нравится, так это эффективный код. Взгляните на следующий пример в Compiler Explorer и убедитесь сами. Мы видим программу целиком, включая main
. Функции Fun
, которая принимает указатель и проверяет на nullptr
, потребовалось 7 инструкций при -O3. Версии без проверки, как и версии со ссылкой, потребовалось всего 3 инструкции. И это учитывая, что компилятор видит всю программу целиком! Самое интересное — это Opt. Здесь я использую std::optional
в связке с get_value_or
. То есть происходит проверка значения. Однако и Clang, и GCC удается скомпилировать эту функцию в 6 строк ассемблерного кода. Неплохо, правда? Здесь отсутствует библиотечная часть, поэтому мы еще и получаем дополнительные накладные расходы за сам optional.
Так нужны ли нам указатели?
Что ж, надеюсь, я показал вам, что мы, по крайней мере, нуждаемся в них реже, чем раньше. Указатели по-прежнему являются важной частью C++, но во многих местах мы можем использовать более подходящие типы данных.
Идея модульного тестирования прочно укрепилась среди лучших практик по разработке ПО. Во многих языках это выражено в наличии специальных инструментов, которые могут поставляться в составе стандартной библиотеки. Но в языке С++ ситуация иная — имеется несколько популярных сторонних библиотек, с помощью которых пишутся модельные тесты. Одна из таких библиотек называется GoogleTest. Приглашаем на открытое заняте, на котором мы рассмотрим, как выглядит использование этой библиотеки для тестирования C++ кода. Регистрация на занятие.