Подтвердить что ты не робот

С++ 11 std:: set lambda compare function

Я хочу создать std::set с пользовательской функцией сравнения. Я мог бы определить его как класс с operator(), но я хотел получить возможность определять лямбда, где он используется, поэтому я решил определить функцию лямбда в списке инициализации конструктора класса, который имеет std::set как член. Но я не могу получить тип лямбды. Прежде чем продолжить, вот пример:

class Foo
{
private:
     std::set<int, /*???*/> numbers;
public:
     Foo () : numbers ([](int x, int y)
                       {
                           return x < y;
                       })
     {
     }
};

Я нашел два решения после поиска: один, используя std::function. Просто установите тип функции сравнения std::function<bool (int, int)> и передайте лямбду точно так, как я. Второе решение - написать функцию make_set, например std::make_pair.

РЕШЕНИЕ 1:

class Foo
{
private:
     std::set<int, std::function<bool (int, int)> numbers;
public:
     Foo () : numbers ([](int x, int y)
                       {
                           return x < y;
                       })
     {
     }
};

РЕШЕНИЕ 2:

template <class Key, class Compare>
std::set<Key, Compare> make_set (Compare compare)
{
     return std::set<Key, Compare> (compare);
}

Вопрос в том, есть ли у меня веская причина предпочесть одно решение другому? Я предпочитаю первый, потому что он использует стандартные функции (make_set - не стандартная функция), но мне интересно: использует ли std::function код (потенциально) медленнее? Я имею в виду, снижает ли вероятность того, что компилятор включит функцию сравнения, или он должен быть достаточно умным, чтобы вести себя точно так же, как если бы это был тип функции лямбда, а не std::function (я знаю, в этом случае он может " t быть лямбда-типом, но знаете, я спрашиваю вообще)?

(Я использую GCC, но я хотел бы знать, что делают популярные компиляторы вообще)

АННОТАЦИЯ, ПОСЛЕ У ДЕТЕЙ БОЛЬШИХ ОТВЕТОВ:

Если скорость критическая, лучшим решением является использование класса с operator() aka functor. Легче всего компилятор оптимизировать и избежать любых указаний.

Для удобства обслуживания и лучшего универсального решения, используя возможности С++ 11, используйте std::function. Он по-прежнему быстрый (немного медленнее, чем функтор, но он может быть незначительным), и вы можете использовать любую функцию - std::function, лямбда, любой вызываемый объект.

Также есть возможность использовать указатель на функцию, но если нет проблемы с производительностью, я думаю, что std::function лучше (если вы используете С++ 11).

Здесь есть возможность определить функцию лямбда где-то еще, но тогда вы ничего не получаете от функции сравнения, являющейся выражением лямбда, так как вы могли бы также сделать ее классом с operator(), а местоположение определения не было бы все равно построенная конструкция.

Есть больше идей, например, использование делегирования. Если вы хотите более подробное объяснение всех решений, прочитайте ответы:)

4b9b3361

Ответ 1

Да, a std::function вводит почти неизбежное косвенное отношение к вашему set. В то время как компилятор всегда может теоретически понять, что все использование вашего set std::function включает вызов его на лямбда, который всегда является одним и тем же лямбда, который является как жестким, так и крайне хрупким.

Хрупкий, потому что перед тем, как компилятор сможет доказать себе, что все вызовы на этот std::function на самом деле звонят на ваш лямбда, он должен доказать, что никакой доступ к вашему std::set никогда не устанавливает std::function на все, кроме вашей лямбды, Это означает, что он должен отслеживать все возможные маршруты, чтобы достичь вашего std::set во всех единицах компиляции и доказать, что ни один из них не делает этого.

Это может быть возможно в некоторых случаях, но относительно безобидные изменения могут сломать его, даже если ваш компилятор сумел это доказать.

С другой стороны, функтор с апатридом operator() может легко доказать поведение, а оптимизации, связанные с этим, - это повседневные вещи.

Итак, на практике я подозреваю, что std::function может быть медленнее. С другой стороны, решение std::function проще в обслуживании, чем make_set, а обмен времени программиста на производительность программы довольно взаимозаменяемо.

make_set имеет серьезный недостаток, заключающийся в том, что любой такой тип set должен быть выведен из вызова make_set. Часто set хранит постоянное состояние, а не то, что вы создаете в стеке, а затем выпадаете из области видимости.

Если вы создали статическую или глобальную безстоящую lambda auto MyComp = [](A const&, A const&)->bool { ... }, вы можете использовать синтаксис std::set<A, decltype(MyComp)> для создания set, который может сохраняться, но для компилятора легко оптимизировать (поскольку все экземпляры decltype(MyComp) являются функционалами без гражданства) и inline. Я указываю это, потому что вы придерживаетесь set в struct. (Или ваша поддержка компилятора

struct Foo {
  auto mySet = make_set<int>([](int l, int r){ return l<r; });
};

который я бы нашел удивительным!)

Наконец, если вы обеспокоены производительностью, считайте, что std::unordered_set выполняется намного быстрее (ценой невозможности итерации содержимого по порядку и необходимости писать/находить хороший хеш), и что отсортированный std::vector лучше, если у вас есть двухфазное "вставить все", а затем "повторно запросить содержимое". Просто сначала введите vector, затем sort unique erase, затем используйте бесплатный алгоритм equal_range.

Ответ 2

Маловероятно, что компилятор сможет встроить вызов std :: function, тогда как любой компилятор, который поддерживает лямбды, почти наверняка встроит версию функтора, в том числе, если этот функтор является лямбда-выражением, не скрытым std::function.

Вы можете использовать decltype чтобы получить тип лямбда-компаратора:

#include <set>
#include <iostream>
#include <iterator>
#include <algorithm>

int main()
{
   auto comp = [](int x, int y){ return x < y; };
   auto set  = std::set<int,decltype(comp)>( comp );

   set.insert(1);
   set.insert(10);
   set.insert(1); // Dupe!
   set.insert(2);

   std::copy( set.begin(), set.end(), std::ostream_iterator<int>(std::cout, "\n") );
}

Какие отпечатки:

1
2
10

Посмотрите, как он работает на Coliru.

Ответ 3

Безъядерный лямбда (т.е. один без захватов) может распадаться на указатель функции, поэтому ваш тип может быть:

std::set<int, bool (*)(int, int)> numbers;

В противном случае я бы выбрал решение make_set. Если вы не будете использовать функцию создания одной строки, потому что она нестандартная, вы не будете писать много кода!

Ответ 4

Из моего опыта, проведенного с профилировщиком, лучшим компромиссом между производительностью и красотой является использование пользовательской реализации делегата, например:

https://codereview.stackexchange.com/questions/14730/impossibly-fast-delegate-in-c11

Так как std::function обычно слишком тяжелый. Я не могу комментировать ваши конкретные обстоятельства, поскольку я их не знаю.

Ответ 5

Если вы решили, что set как член класса, инициализируя его компаратор во время конструктора, то по крайней мере один уровень косвенности неизбежен. Подумайте, что, насколько компилятор знает, вы можете добавить еще один конструктор:

 Foo () : numbers ([](int x, int y)
                   {
                       return x < y;
                   })
 {
 }

 Foo (char) : numbers ([](int x, int y)
                   {
                       return x > y;
                   })
 {
 }

Как только у вас есть объект типа Foo, тип set не несет информацию о том, какой конструктор инициализировал свой компаратор, поэтому для вызова правильной лямбды требуется косвенность к времени выполнения, выбранному лямбдой operator().

Поскольку вы используете безъядерные лямбды, вы можете использовать тип указателя функции bool (*)(int, int) как ваш тип компаратора, так как беззаботные лямбды имеют соответствующую функцию преобразования. Разумеется, это подразумевает косвенное использование указателя функции.

Ответ 6

Разница сильно зависит от ваших оптимизаций компилятора. Если он оптимизирует лямбда в std::function, то это эквивалентно, если вы не вводите косвенность в первом, что вы не будете иметь в последнем.