Должен ли я передать std :: function по 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), что является наименьшим размером, который вы когда-либо рассматривали бы для константной ссылки.
Матс Петерссон
12
@ Матс: я не думаю, что размер std::functionоболочки так же важен, как сложность ее копирования. Если используются глубокие копии, это может быть намного дороже, чем можно sizeofпредположить.
Бен Фойгт
Вы должны moveфункцию в?
Якк - Адам Невраумонт
operator()()это constтак ссылка Const должна работать. Но я никогда не использовал std :: function.
Нил Басу
@Yakk Я просто передаю лямбду в функцию.
Свен Адбринг

Ответы:

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что собирается сделать копию своего аргумента функции для отправки в поток пользовательского интерфейса для выполнения! (он вернется до того, как с ним будет покончено, поэтому он не может просто использовать ссылку на него). Для случая (А), мы просто в его длительного хранения. В случае (B) мы вынуждены копировать .movestd::functionstd::function

Этот магазин делает переход по стоимости более оптимальным. Если есть вероятность, что вы храните копию std::function, передайте по значению. В противном случае, любой из этих способов примерно эквивалентен: единственным недостатком по значению является то, что вы берете один std::functionи тот же громоздкий и используете его один вспомогательный метод за другим. За исключением этого, a moveбудет столь же эффективным, как и const&.

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

Предположим, что std::functionхранит некоторый объект с operator() const, но у него также есть некоторые mutableчлены данных, которые он изменяет (как грубо!).

В этом std::function<> const&случае mutableизмененные члены-данные будут распространяться из вызова функции. В std::function<>случае, они не будут.

Это довольно странный угловой случай.

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

Якк - Адам Невраумонт
источник
Семантическое преимущество «передачи по значению, если вы его храните», как вы говорите, заключается в том, что по контракту функция не может сохранить адрес переданного аргумента. Но правда ли, что «Если не считать этого, движение будет столь же эффективным, как и постоянное»? Я всегда вижу стоимость операции копирования плюс стоимость операции перемещения. При прохождении мимо 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, если будет более полным, чтобы охватить другой вариант, чтобы передать по значению ref:std::future<void> run_in_ui_thread( std::function<void()>&& )
Павел P
33

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

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

Быстрее:

template<typename Functor>
void callFunction(Functor&& x)
{
    x();
}
Бен Фойгт
источник
1
Я вообще не беспокоюсь о производительности. Я просто думал, что использование const-ссылок там, где их следует использовать, является обычной практикой (на ум приходят строки и векторы).
Свен Адбринг
13
@Ben: Я думаю, что самый современный способ реализации этого, дружественный к хиппи, заключается в использовании std::forward<Functor>(x)();для сохранения категории значений функтора, поскольку это «универсальная» ссылка. Хотя в 99% случаев это не поможет.
GManNickG
1
@ Бен Фойгт, так что для твоего случая, я бы вызвал функцию с ходом? callFunction(std::move(myFunctor));
arias_JC
2
@arias_JC: Если параметр является лямбда, это уже значение. Если у вас есть lvalue, вы можете использовать его, std::moveесли он вам больше не нужен, или перейти напрямую, если вы не хотите выходить из существующего объекта. Правила свертывания ссылок гарантируют, что callFunction<T&>()имеет параметр типа T&, а не T&&.
Бен Фойгт
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

Я полагаю, что вы уже знаете семантику (не) константных ссылок, поэтому я не буду вдаваться в подробности. Если вам нужно, чтобы я добавил больше объяснений по этому поводу, просто спросите, и я обновлю.

сям
источник