Мистическое ограничение на std:: binary_search
Описание проблемы:
Рассмотрим некоторую структуру, имеющую член std::string name
. Для ясности пусть пусть a struct Human
, представляя информацию о людях. Помимо name
он может также содержать много других элементов данных.
Пусть есть контейнер std::vector<Human> vec
, где объекты уже отсортированы по name
. Также для ясности предположим, что все имена уникальны.
Проблема заключается в следующем: наличие некоторой строки nameToFind
выяснить, существует ли элемент в массиве с таким именем.
Решение и мой прогресс:
Очевидное и естественное решение, похоже, выполняет двоичный поиск с использованием функции std::binary_search
. Но есть проблема: тип искомого элемента (std::string
) отличается от типа элементов в контейнере (Human
), а std:: binary_search требует правила для сравнения этих элементов. Я попытался решить это тремя способами, описанными ниже. Первые два представлены только для иллюстрации эволюции моего решения и проблем, с которыми я столкнулся. Мой главный вопрос относится к третьему.
Попытка 1: конвертировать std::string
в Human
.
Напишите функцию сравнения:
bool compareHumansByNames( const Human& lhs, const Human& rhs )
{
return lhs.name < rhs.name;
}
Затем добавьте конструктор, который строит объект Human
из std::string
:
struct Human
{
Human( const std::string& s );
//... other methods
std::string name;
//... other members
};
и используйте binary_search в следующей форме:
std::binary_search( vec.begin(), vec.end(), nameToFind, compareHumansByNames );
Кажется, работает, но появляется две большие проблемы:
Во-первых, как инициализировать другие члены данных, но Human::name
, особенно в случае, когда у них нет конструктора по умолчанию? установка магических значений может привести к созданию объекта, который является семантически незаконным.
Во-вторых, мы должны объявить этот конструктор как не explicit
, чтобы разрешить неявные преобразования во время алгоритма. Плохие последствия этого хорошо известны.
Кроме того, такой временный объект Human
будет построен на каждой итерации, что может оказаться довольно дорогостоящим.
Попытка 2: конвертировать Human
в std::string
.
Мы можем попытаться добавить operator string ()
в класс Human
, который возвращает его name
, а затем использовать сравнение для двух std::string
s. Однако этот подход также неудобен по следующим причинам:
Во-первых, код не будет компилироваться сразу из-за обсуждаемой проблемы здесь. Нам придется немного поработать, чтобы компилятор использовал соответствующий operator <
.
Во-вторых, что означает "преобразовать человека в строку"? Существование такого преобразования может привести к семантически неправильному использованию класса Human
, что нежелательно.
Попытка 3: сравнение без конверсий.
Лучшим решением, которое я получил до сих пор, является создание
struct Comparator
{
bool operator() ( const Human& lhs, const std::string& rhs )
{
return lhs.name < rhs;
}
bool operator() ( const std::string& lhs, const Human& rhs )
{
return lhs < rhs.name;
}
};
и использовать двоичный поиск как
binary_search( vec.begin(), vec.end(), nameToFind, Comparator() );
Это компилируется и выполняется правильно, все выглядит нормально, но здесь начинается интересная часть:
Посмотрите http://www.sgi.com/tech/stl/binary_search.html. Здесь сказано, что тип значения ForwardIterator того же типа, что и T. ". Довольно запутанное ограничение, и мое последнее решение нарушает его. Посмотрите, что говорит об этом стандарт С++:
25.3.3.4 binary_search
template<class ForwardIterator, class T>
bool binary_search(ForwardIterator first, ForwardIterator last,
const T& value);
template<class ForwardIterator, class T, class Compare>
bool binary_search(ForwardIterator first, ForwardIterator last,
const T& value, Compare comp);
Требуется: Тип T является LessThanComparable (20.1.2).
В явном виде ничего не сказано о типе ForwardIterator
. Но в определении LessThanComparable, приведенном в 20.1.2, говорится о сравнении двух элементов того же типа. И вот что я не понимаю. Действительно ли это означает, что тип исследуемого объекта и тип объектов контейнера должны совпадать, и мое решение нарушает это ограничение? Или это не относится к случаю, когда используется компаратор comp
и относится только к случаю, когда по умолчанию используется operator <
для сравнения? В первом случае я смущен тем, как использовать std::binary_search
для решения этой проблемы, не сталкиваясь с проблемами, упомянутыми выше.
Заранее благодарим за помощь и находим время, чтобы прочитать мой вопрос.
Примечание. Я понимаю, что запись двоичного поиска вручную не требует времени и решит проблему мгновенно, но чтобы избежать повторного создания колеса, я хочу использовать std:: binary_search. Также мне очень интересно узнать о существовании такого ограничения в соответствии со стандартом.
Ответы
Ответ 1
Если ваша цель - найти, есть ли Human
с заданным именем, то необходимо убедиться в следующем:
const std::string& get_name(const Human& h)
{
return h.name;
}
...
bool result = std::binary_search(
boost::make_transform_iterator(v.begin(), &get_name),
boost::make_transform_iterator(v.end(), &get_name),
name_to_check_against);
Ответ 2
[Завершить переписывание; игнорировать комментарии]
Формулировка была изменена с С++ 03 на С++ 0x. В последнем случае для T
больше не требуется быть меньше, чем сопоставимо, по-видимому, для облегчения этого ненужного ограничения.
Новый стандарт требует только, чтобы comp(e, value)
влечет !comp(value, e)
. До тех пор, пока ваш компаратор реализует оба направления, вы должны иметь возможность юридически искать string
как значение с помощью функтора компаратора, который реализует как асимметричные сравнения (т.е. Ваш "попытка 3" ).
Ответ 3
Я думаю, что здесь говорится в стандарте, что выражение fucntor(a, b)
должно быть допустимым строгим слабым порядком, независимо от того, решил ли алгоритм сделать что-то вроде functor(*begin, *(begin + 1))
. Поэтому, я думаю, ваш компаратор должен будет обеспечить перегрузку operator()(Human, Human)
, чтобы соответствовать.
Тем не менее, я думаю, что это одна из тех вещей, которые явно не разрешены стандартом, но для которых существует мало или вообще не существует реализаций, которые используют широту, предлагаемую стандартным.
Ответ 4
Я не вижу, чтобы в стандарте требовалось, чтобы типы значений, переданных функции сравнения (или оператору <
) на binary_search
, должны быть одинаковыми. Итак, формально я считаю, что отлично использовать компаратор, который работает с двумя значениями разных типов.