Я хотел написать этот пост ещё в июле, но никак не мог, о ирония, решить, как его назвать. Удачные термины пришли мне в голову только после доклада Кейт Грегори на CppCon, и теперь я наконец могу рассказать вам, как не надо называть функции.
Бывают, конечно, названия, которые вообще не несут информации, типа int f(int x)
. Ими пользоваться тоже не надо, но речь не о них. Порой бывает, что вроде бы и информации в названии полно, но пользы от неё абсолютно никакой.
Пример 1: std::log2p1()
В C++20 в заголовок добавили несколько новых функций для битовых операций, среди прочих std::log2p1
. Выглядит она вот так:
int log2p1(int i)
{
if (i == 0)
return 0;
else
return 1 + int(std::log2(x));
}
То есть для любого натурального числа функция возвращает его двоичный логарифм плюс 1, а для 0 возвращает 0. И это не школьная задачка на оператор if/else, это действительно полезная вещь — минимальное число бит, в которое поместится данное значение. Вот только догадаться об этом по названию функции практически невозможно.
Пример 2: std::bless()
Сейчас будет не про названия
Небольшое отступление: в С++ арифметика указателей работает только с указателями на элементы массива. Что, в принципе, логично: в общем случае набор соседних объектов неизвестен и “в десяти байтах справа от переменной i
” может оказаться что угодно. Это однозначно неопределённое поведение.
int obj = 0;
int* ptr = &obj;
++ptr; // Неопределённое поведение
Но такое ограничение объявляет неопределённым поведением огромное количество существующего кода. Например, вот такую упрощённую имплементацию std::vector<T>::reserve()
:
void reserve(std::size_t n)
{
// выделяем память под наши объекты
auto new_memory = (T*) ::operator new(n * sizeof(T));
// переносим их туда
…
// обновляем буфер
auto size = this->size();
begin_ = new_memory; // Неопределённое поведение
end_ = new_memory + size; // Ещё раз неопределённое поведение
end_capacity_ = new_memory + n; // и ещё раз
}
Мы выделили память, перенесли все объекты и теперь пытаемся убедиться, что указатели указывают куда надо. Вот только последние три строчки неопределены, потому что содержат арифметические операции над указателями вне массива!
Разумеется, виноват тут не программист. Проблема в самом стандарте C++, который объявляет неопределённым поведением этот очевидно разумный кусок кода. Поэтому P0593 предлагает исправить стандарт, добавив некоторым функциям (вроде ::operator new
и std::malloc
) способность создавать массивы по мере необходимости. Все созданные ими указатели будут магическим образом становиться указателями на массивы, и с ними можно будет совершать арифметические операции.
Всё ещё не про названия, потерпите секундочку.
Вот только иногда операции над указателями требуются при работе с памятью, которую не выделяла одна из этих функций. Например, функция deallocate()
по сути своей работает с мёртвой памятью, в которой вообще нет никаких объектов, но всё же должна сложить указатель и размер области. На этот случай P0593 предлагал функцию std::bless(void* ptr, std::size_t n)
(там была ещё другая функция, которая тоже называется bless
, но речь не о ней). Она не оказывает никакого эффекта на реально существующий физический компьютер, но создаёт для абстрактной машины объекты, которые разрешили бы использовать арифметику указателей.
Название std::bless
было временным.
Так вот, название.
В Кёльне перед LEWG поставили задачу — придумать для этой функции название. Были предложены варианты implicitly_create_objects()
и implicitly_create_objects_as_needed()
, потому что именно это функция и делает.
Мне эти варианты не понравились.
Пример 3: std::partial_sort_copy()
Пример взят из выступления Кейт
Есть функция std::sort
, которая сортирует элементы контейнера:
std::vector<int> vec = {3, 1, 5, 4, 2};
std::sort(vec.begin(), vec.end());
// vec == {1, 2, 3, 4, 5}
Ещё есть std::partial_sort
, которая сортирует только часть элементов:
std::vector<int> vec = {3, 1, 5, 4, 2};
std::partial_sort(vec.begin(), vec.begin() + 3, vec.end());
// vec == {1, 2, 3, ?, ?} (либо ...4,5, либо ...5,4)
И ещё есть std::partial_sort_copy
, которая тоже сортирует часть элементов, но при этом старый контейнер не меняет, а переносит значения в новый:
const std::vector<int> vec = {3, 1, 5, 4, 2};
std::vector<int> out;
out.resize(3);
std::partial_sort_copy(vec.begin(), vec.end(),
out.begin(), out.end());
// out == {1, 2, 3}
Кейт утверждает, что std::partial_sort_copy
— так себе название, и я с ней согласен.
Название имплементации и название результата
Ни одно из перечисленных названий не является, строго говоря, неверным: они все прекрасно описывают то, что делает функция. std::log2p1()
действительно считает двоичный логарифм и прибавляет к нему единицу; implicitly_create_objects()
имплицитно создаёт объекты, а std::partial_sort_copy()
частично сортирует контейнер и копирует результат. Тем не менее, все эти названия мне не нравятся, потому что они бесполезны.
Ни один программист не сидит и не думает “вот бы мне взять двоичный логарифм, да прибавить бы к нему единицу”. Ему нужно знать, во сколько бит поместится данное значение, и он безуспешно ищет в доках что-нибудь типа bit_width
. К моменту, когда до пользователя библиотеки доходит, при чём тут вообще двоичный логарифм, он уже написал свою имплементацию (и, скорее всего, пропустил проверку для ноля). Даже если каким-то чудом в коде оказалось std::log2p1
, следующий, кто увидит этот код, опять должен понять, что это и зачем оно нужно. У bit_width(max_value)
такой проблемы бы не было.
Точно так же никому не надо “имплицитно создавать объекты” или “проводить частичную сортировку копии вектора” — им нужно переиспользовать память или получить 5 наибольших значений в порядке убывания. Что-то типа recycle_storage()
(что тоже предлагали в качестве названия std::bless
) и top_n_sorted()
было бы гораздо понятнее.
Кейт использует термин название имплементации для std::partial_sort_copy()
, но он прекрасно подходит и к двум другим функциям. Имплементацию их названия действительно описывают идеально. Вот только пользователю нужно название результата — то, что он получит, вызвав функцию. До её внутреннего устройства ему нет никакого дела, он просто хочет узнать размер в битах или переиспользовать память.
Называть функцию на основании её спецификации — значит создавать на ровном месте непонимание между разработчиком библиотеки и её пользователем. Всегда нужно помнить, когда и как функция будет использоваться.
Звучит банально, да. Но, судя по std::log2p1()
, это далеко не всем очевидно. К тому же порой всё не так просто.
Пример 4: std::popcount()
std::popcount()
, как и std::log2p1()
, в C++20 предлагается добавить в <bit>
. И это, разумеется, чудовищно плохое название. Если не знать, что эта функция делает, догадаться невозможно. Мало того, что сокращение сбивает с толку (pop в названии есть, но pop/push тут ни при чём) — расшифровка population count (подсчёт населения? число популяций?) тоже не помогает.
С другой стороны, std::popcount()
идеально подходит для этой функции, потому что она вызывает ассемблерную инструкцию popcount. Это не то что название имплементации — это полное её описание.
Тем не менее, в данном случае разрыв между разработчиками языка и программистами не так уж и велик. Инструкция, считающая количество единиц в двоичном слове, называется popcount с шестидесятых. Для человека, хоть сколько-нибудь разбирающегося в операциях с битами, такое название абсолютно очевидно.
Кстати, хороший вопрос: придумывать ли названия, удобные для новичков, или оставить привычные для олдфагов?
Хэппи-энд?
P1956 предлагает переименовать std::log2p1()
в std::bit_width()
. Это предложение, вероятно, будет принято в C++20. std::ceil2
и std::floor2
тоже переименуют, в std::bit_ceil() and std::bit_floor() соответственно. Их старые названия тоже были не очень, но по другим причинам.
LEWG в Кёльне не выбрала ни implicitly_create_objects[_as_needed]
, ни recycle_storage
в качестве названия для std::bless
. Эту функцию решили вообще не включать в стандарт. Тот же эффект может быть достигнут эксплицитным созданием массива байтов, поэтому, дескать, функция не нужна. Мне это не нравится, потому что вызов std::recycle_storage()
был бы читаемее. Другая std::bless()
всё ещё существует, но теперь называется start_lifetime_as
. Это мне нравится. Она должна войти в C++23.
Разумеется, std::partial_sort_copy()
уже не переименуют — под этим названием она вошла в стандарт ещё в 1998. Но хотя бы std::log2p1
исправили, и то неплохо.
Придумывая названия функций, нужно думать о том, кто ими будет пользоваться и чего он от них захочет. Как выразилась Кейт, именование требует эмпатии.