Чи слід передавати функцію std :: через const-reference?


141

Скажімо, у мене є функція, яка займає std::function:

void callFunction(std::function<void()> x)
{
    x();
}

Чи слід xзамість цього пройти через const-reference ?:

void callFunction(const std::function<void()>& x)
{
    x();
}

Чи змінюється відповідь на це питання залежно від того, що функція робить з ним? Наприклад, якщо це функція або конструктор члена класу, який зберігає або ініціалізує std::functionзмінну члена.


1
Напевно, ні. Я точно не знаю, але я би сподівався, sizeof(std::function)що це не більше 2 * sizeof(size_t), що є найменшим розміром, який ви коли-небудь вважали б для посилання на const.
Матс Петерсон

12
@Mats: Я не думаю, що розмір std::functionобгортки настільки важливий, як і складність його копіювання. Якщо задіяні глибокі копії, це може бути набагато дорожчим, ніж sizeofпропонується.
Ben Voigt

Ви повинні moveфункціонувати в?
Якк - Адам Невраумон

operator()()це constтак посилання Const повинна працювати. Але я ніколи не використовував std :: function.
Ніл Басу

@Yakk Я просто передаю лямбда безпосередньо функції.
Sven Adbring

Відповіді:


79

Якщо ви хочете продуктивність, передавайте цінність, якщо ви зберігаєте її.

Припустимо, у вас є функція під назвою "запустити це в потоці інтерфейсу".

std::future<void> run_in_ui_thread( std::function<void()> )

який виконує деякий код у потоці "ui", а потім сигналізує про futureзавершення. (Корисно в структурах інтерфейсу користувача, де потік користувальницького інтерфейсу знаходиться там, де ви повинні поплутатися з елементами інтерфейсу)

Ми маємо два підписи, які ми розглядаємо:

std::future<void> run_in_ui_thread( std::function<void()> ) // (A)
std::future<void> run_in_ui_thread( std::function<void()> const& ) // (B)

Тепер ми, ймовірно, використовуватимемо наступне:

run_in_ui_thread( [=]{
  // code goes here
} ).wait();

який створить анонімне закриття (лямбда), побудує std::functionз нього, передасть його run_in_ui_threadфункції, а потім дочекайтеся її завершення в основному потоці.

У випадку (A), std::functionбезпосередньо побудований з нашої лямбда, який потім використовується в межах run_in_ui_thread. Лямбда moveперетворюється на std::function, тому будь-який рухливий стан ефективно переноситься в неї.

У другому випадку створюється тимчасовий std::function, лямбда moveвпадає в нього, тоді цей тимчасовий std::functionвикористовується за допомогою посилання в межах run_in_ui_thread.

Поки що так добре - вони обидва виступають однаково. За винятком того run_in_ui_thread, що збирається зробити копію свого аргументу функції, щоб надіслати потік ui для виконання! (він повернеться до того, як буде зроблено з ним, тому він не може просто використовувати посилання на нього). Для випадку (А), ми просто в його тривалого зберігання. У випадку (B), ми змушені копіювати .movestd::functionstd::function

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

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

Припустимо, що std::functionзберігає якийсь об’єкт з a operator() const, але він також має деякі mutableчлени даних, які він модифікує (як грубо!).

У std::function<> const&випадку, коли mutableмодифіковані члени даних поширюватимуться з виклику функції. У std::function<>випадку вони не стануть.

Це відносно дивний кутовий випадок.

Ви хочете поводитись так, std::functionяк до будь-якого іншого, можливо важкого, дешевого рухомого типу. Переміщення дешеве, копіювання може бути дорогим.


Семантична перевага "передавати за значенням, якщо ви його зберігаєте", як ви говорите, полягає в тому, що за контрактом функція не може зберігати адресу переданого аргументу. Але чи правда, що "забороняючи це, крок буде настільки ж ефективним, як і const &"? Я завжди бачу вартість операції з копіювання плюс вартість операції переміщення. Проходячи повз, const&я бачу лише вартість операції з копіювання.
ceztko

2
@ceztko У обох випадках (A) та (B) тимчасове std::functionстворюється з лямбда. У (A) тимчасове переходить у аргумент до run_in_ui_thread. У (B) посилання на вказану тимчасову передається run_in_ui_thread. До тих пір, поки ваші std::functions створюються з лямбда в якості тимчасових, цей пункт дотримується. У попередньому пункті йдеться про випадок, коли std::functionзберігається. Якщо ми не зберігаємо, просто створюємо з лямбда, function const&і functionмаємо точно такі ж накладні витрати.
Якк - Адам Невраумон

А, бачу! Звичайно, це залежить від того, що відбувається за межами run_in_ui_thread(). Чи є лише підпис, щоб сказати "Перейти через посилання, але я не буду зберігати адресу"?
ceztko

@ceztko Ні, немає.
Якк - Адам Невраумон

1
@ Yakk-AdamNevraumont, якщо буде більш повно, щоб охопити інший варіант, щоб пройти через rvalue ref:std::future<void> run_in_ui_thread( std::function<void()>&& )
Pavel P

33

Якщо ви турбуєтесь про продуктивність і не визначаєте функцію віртуального члена, то, швидше за все, ви не повинні використовувати це std::functionвзагалі.

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

Швидше:

template<typename Functor>
void callFunction(Functor&& x)
{
    x();
}

1
Я насправді не переживаю за продуктивність. Я просто думав, що використання const-посилань там, де їх слід використовувати, є звичайною практикою (на думку розумних рядків та векторів).
Sven Adbring

13
@Ben: Я думаю, що найсучасніший спосіб, що спричинить хіпі, це використовувати std::forward<Functor>(x)();, щоб зберегти ціннісну категорію функтора, оскільки це "універсальна" посилання. Але не збираюся змінювати в 99% випадків.
GManNickG

1
@Ben Voigt, так що для вашого випадку я б назвав функцію рухом? callFunction(std::move(myFunctor));
arias_JC

2
@arias_JC: Якщо параметр - лямбда, це вже оцінка. Якщо у вас є значення lvalue, ви можете або скористатися, std::moveякщо він вам більше не потрібен, або передати безпосередньо, якщо ви не хочете виїжджати з існуючого об'єкта. Правила згортання посилань гарантують, що callFunction<T&>()параметр типу T&не є T&&.
Ben Voigt

1
@BoltzmannBrain: Я вирішив не вносити цих змін, оскільки це справедливо лише для найпростішого випадку, коли функція викликається лише один раз. Моя відповідь - на питання "як я повинен передавати об'єкт функції?" і не обмежується функцією, яка нічого не робить, крім безумовного виклику цього функтора рівно один раз.
Бен Войгт

25

Як зазвичай у C ++ 11, проходження за значенням / посиланням / const-посиланням залежить від того, що ви робите зі своїм аргументом. std::functionнічим не відрізняється.

Перехід за значенням дозволяє перемістити аргумент у змінну (як правило, змінну-члена класу):

struct Foo {
    Foo(Object o) : m_o(std::move(o)) {}

    Object m_o;
};

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

Foo f1{Object()};               // move the temporary, followed by a move in the constructor
Foo f2{some_object};            // copy the object, followed by a move in the constructor
Foo f3{std::move(some_object)}; // move the object, followed by a move in the constructor

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

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