Название имплементации и название результата

в 13:53, , рубрики: c++, C++20, в информатике две сложных проблемы, Программирование

Название имплементации и название результата - 1

Я хотел написать этот пост ещё в июле, но никак не мог, о ирония, решить, как его назвать. Удачные термины пришли мне в голову только после доклада Кейт Грегори на 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 исправили, и то неплохо.

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

Автор: synedra

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js