C ++ 11 std :: встановити функцію порівняння лямбда-сигналу


75

Я хочу створити 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(я знаю, у цьому випадку це не може бути тип лямбда, але ви знаєте, я взагалі запитую)?

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

РЕЗЮМЕ, ПІСЛЯ Я ОТРИМАВ БАГАТО ВЕЛИКИХ ВІДПОВІДЬ

Якщо швидкість є критичною, найкращим рішенням буде використання класу з operator()ака-функтором. Компілятору найпростіше оптимізувати та уникнути будь-яких побічних дій.

Для зручності обслуговування та кращого рішення загального призначення, використовуючи функції C ++ 11, використовуйте std::function. Це все ще швидко (лише трохи повільніше, ніж функтор, але може бути незначним), і ви можете використовувати будь-яку функцію - std::function, лямбда, будь-який об’єкт, що викликається.

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

Існує можливість визначити лямбда-функцію в іншому місці, але тоді ви нічого не отримуєте від функції порівняння, яка є лямбда-виразом, оскільки ви також можете зробити її класом operator() а розташування визначення все одно не буде конструкцією набору.

Є більше ідей, таких як використання делегування. Якщо ви хочете отримати більш ретельне пояснення всіх рішень, прочитайте відповіді :)


2
Я відчуваю запах передчасної оптимізації.

Чому не просто bool(*)(int, int)? Але може бути ефективніше створити явний клас, який може бути побудований за замовчуванням.
Kerrek SB

@Fanael, звідки ти знаєш, що, якщо у мене довгий набір об'єктів, відтворених графічним інтерфейсом, і мені дійсно потрібно було, щоб це було якомога швидше
cfa45ca55111016ee9269f0a52e771

@ fr33domlover: чи не std::functionв такому випадку вартість карликових перевищує вартість рендерингу?

@Fanael, якщо сортування виконується, поки відтворення немає, я все одно можу отримати більшу швидкість, зробивши сортування швидшим і надавши коду рендерингу більше часу на виконання. У будь-якому випадку, навіть якщо це передчасна оптимізація, питання все одно корисне: подивіться на відповіді та голоси за ...
cfa45ca55111016ee9269f0a52e771

Відповіді:


26

Так, a std::functionвводить майже неминучий непрямий вплив на ваш set. Хоча компілятор завжди може, теоретично, з'ясувати , що використання Вашого set«S std::functionвключає в себе виклик його на лямбда , який завжди точно такий же лямбда, який є одночасно твердим і дуже тендітні.

Тендітний, тому що перед тим, як компілятор зможе довести собі, що всі виклики, std::functionякі насправді є викликами до вашої лямбда-сигналу, він повинен довести, що жоден доступ до вашого std::setніколи не встановлює значення std::functionнічого, крім вашої лямбда-сигналу. Це означає, що він повинен відстежити всі можливі шляхи, щоб дістатися до вас std::setу всіх одиницях компіляції, і довести, що жоден з них цього не робить.

Це може бути можливо в деяких випадках, але відносно нешкідливі зміни можуть зламати його, навіть якщо ваш компілятор зумів це довести.

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

Так що так, на практиці я підозрюю, що це std::functionможе бути повільніше. З іншого боку, std::functionрішення простіше підтримувати, ніж make_setодне, і обмін часу програміста на продуктивність програми є досить мінливим.

make_setмає серйозний недолік, що будь-який такий setтип повинен бути виведений із заклику до make_set. Часто setзберігається стійкий стан, а не те, що ви створюєте в стеку, тоді виходить з поля зору.

Якщо ви створили статичну або глобальну лямбда-форму без стану auto MyComp = [](A const&, A const&)->bool { ... }, ви можете використовувати std::set<A, decltype(MyComp)>синтаксис для створення такого, setякий може зберігатися, але компілятору легко оптимізувати (оскільки всі екземпляри decltype(MyComp)є функторами без стану) та вбудованим. Я вказую на це, тому що ви дотримуєтеся символу seta 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алгоритмом.


Хороша порада щодо альтернативних контейнерів, але пам’ятайте, що функція хешування для деяких інших типів ( intвідмінно працює). Це може знищити продуктивність, якщо це зробити неправильно - або занадто повільно, або занадто багато зіткнень.
метал

Я думаю, що я пропустив цей момент з make_set, він не спрацює з лямбдами. Залишається лише рішення std :: function, яке передбачає опосередкованість, але в даний час у мене з цим проблем з продуктивністю немає. Також вектор дуже цікавий, вміст набору читається та відображається за допомогою графічного інтерфейсу, тому візуалізація відбувається набагато частіше, коли користувачі змінюють вміст (що трапляється рідко) ... можливо, сортування вектора по кожній зміні насправді буде швидшим, ніж пошук ключа
cfa45ca55111016ee9269f0a52e771

auto functor = [](...){...};Синтаксис має перевагу бути коротше , ніж struct functor { bool operator()(...)const{...}; };синтаксис, і той недолік , що вимагає functorпримірника, щоб викликати його (на відміну від по замовчуванням побудований функтор будь-якого для structвипадку).
Якк - Адам Неврамон

31

Навряд чи компілятор зможе вбудувати виклик 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.


Ви можете використовувати decltype лише після визначення лямбда, але тоді я втрачаю можливість визначати лямбда в самому конструкторі, тому я міг би також використовувати клас з operator (). Я хочу визначити функцію порівняння в конструкторі
cfa45ca55111016ee9269f0a52e771

Дивись вище. Ви можете зробити його статичним членом свого класу.
метал

Коментар до редагування (приклад коду): подивіться на приклад коду, він інший. Те, що ви пропонуєте, не працює, оскільки лямбда не може бути використана в неоціненому контексті + тип не може бути виведений правильно, оскільки він створюється компілятором для кожної окремої лямбди, яку ви створюєте. Також я знайшов відповідні SO запитання, вони кажуть саме це. В іншому випадку я б просто використав
лямбду

Правильно. Просто тестував це. Цей біт видалено.
метал

1
@ cfa45ca55111016ee9269f0a52e771 Тип лямбда залежить лише від типу повернення та типів аргументів. Ви можете використовувати будь-яку лямбду з однаковими поверненнями та аргументами. Ви можете використовувати невеликий зразок лямбди для використання decltypeна ньому: decltype ([] (bool A, bool B) {return bool (1)}) `.
Euri Pinhollow,

6

Лямбда без стану (тобто така, яка не захоплює) може перетворитися на покажчик функції, тому ваш тип може бути:

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

Інакше я піду на make_setрішення. Якщо ви не будете використовувати однорядкову функцію створення, оскільки вона нестандартна, ви не збираєтеся писати багато коду!


Цікаво, я не знав, що він передає покажчик на функцію ... Що стосується standatd / нестандартного, я мав на увазі, що якщо я покладаюся на те, що функція std :: буде краще реалізована в майбутньому, то майбутні версії компіляторів зроблять це так швидко, як лямбда, вбудований, без зміни коду
cfa45ca55111016ee9269f0a52e771

1
Розумію. Існує обмеження щодо ефективності std::functionотримання, але чи справді ви виміряли, щоб побачити, чи є проблема з продуктивністю, перш ніж турбуватися про це? std:functionможе бути досить проклятим: timj.testbit.eu/2013/01/25/cpp11-signal-system-performance
Джонатан

1
@ fr33domlover: Ваше припущення не обов'язково відповідає дійсності. Визначення " std::functionвимагає стирання типу на фактичній викличуваній сутності, яка утримується, і що в значній мірі вимагає опосередкованості. Навіть у випадку звичайного вказівника на функцію буде дорожче, ніж якщо ви створите функтор для цієї конкретної мети. Саме тому std::sortшвидше , ніж C - х qsort.
Девід Родрігес - дрибі

1

З мого досвіду, коли я бавився з профайлером, найкращим компромісом між продуктивністю та красою є використання власної реалізації делегатів, таких як:

/codereview/14730/impossibly-fast-delegate-in-c11

Як std::functionправило, це трохи занадто важко. Однак я не можу коментувати ваші конкретні обставини, оскільки я їх не знаю.


Виглядає як відмінне рішення загального призначення, але мені просто потрібен мій маленький кейс зі std :: set, я вважаю за краще просто використовувати make_set, який є "особливим випадком" загального класу делегатів. Але загалом це дуже цікаво, можливо, можна вирішити всі ці лямбда-проблеми
cfa45ca55111016ee9269f0a52e771

2
Такий делегат все ще має шар непрозорої опосередкованості (тобто еквівалент відміни покажчика) над функтором без стану. Можливість використовувати функтори без громадянства - одна з головних причин, що std::sortперевершує qsort.
Якк - Адам Неврамон

О, якщо в ньому є невбудована опосередкованість, я також можу використовувати std :: function і отримати однакову швидкість ...
cfa45ca55111016ee9269f0a52e771

1
Можливо, я невмілий gdb, але мені здавалося, що компілятор часто усував усі розмежування посилань, коли я використовував цей делегат із -O3.
user1095108

1

Якщо ви вирішили мати 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)як тип порівняння, оскільки лямбди без захоплення мають відповідну функцію перетворення. Це, звичайно, передбачає опосередкування через покажчик функції.


0

Різниця сильно залежить від оптимізації вашого компілятора. Якщо це оптимізує лямбда в std::functionеквівалентних умовах, якщо ні, ви вводите опосередкованість у першій, якої у вас не буде в другій.


Я використовую GCC, але хотів би знати, що взагалі роблять популярні компілятори. Можлива непрямість є причиною того, що я не просто вибираю рішення std :: function
cfa45ca55111016ee9269f0a52e771

2
stackoverflow.com/questions/8298780/… , це може бути цікаво.
фільм або

Хммм ... там сказано, що компілятори все ще не повністю розглядають прості випадки std :: function
cfa45ca55111016ee9269f0a52e771
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.