Devo passar uma função std :: por const-reference?

141

Digamos que eu tenho uma função que recebe um std::function:

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

Em vez disso, devo passar xpela referência constante ?:

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

A resposta a esta pergunta muda dependendo do que a função faz com ela? Por exemplo, se é uma função ou construtor de membro da classe que armazena ou inicializa o std::functionem uma variável de membro.

Sven Adbring
fonte
1
Provavelmente não. Não sei ao certo, mas não esperaria sizeof(std::function)mais 2 * sizeof(size_t), que é o menor tamanho que você consideraria para uma referência const.
Mats Petersson
12
@ Mat: Eu não acho que o tamanho do std::functioninvólucro é tão importante quanto a complexidade de copiá-lo. Se cópias profundas estiverem envolvidas, pode ser muito mais caro do que sizeofsugere.
Ben Voigt
Você deve movea função?
Yakk - Adam Nevraumont
operator()()é constassim que uma referência const deve funcionar. Mas eu nunca usei std :: function.
Neel Basu
@ Yakk Acabei de passar um lambda diretamente para a função.
precisa saber é o seguinte

Respostas:

79

Se você deseja desempenho, passe por valor se estiver armazenando.

Suponha que você tenha uma função chamada "execute isso no thread da interface do usuário".

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

que executa algum código no thread "ui" e sinaliza futurequando terminar. (Útil em estruturas de interface do usuário em que o segmento da interface do usuário é onde você deve mexer com os elementos da interface do usuário)

Temos duas assinaturas que estamos considerando:

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

Agora, é provável que as usemos da seguinte maneira:

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

que criará um fechamento anônimo (um lambda), constrói um a std::functionpartir dele, passa-o para a run_in_ui_threadfunção e aguarda a conclusão da execução no encadeamento principal.

No caso (A), o std::functioné construído diretamente a partir do nosso lambda, que é então usado dentro do run_in_ui_thread. O lambda é moved dentro do std::function, portanto, qualquer estado móvel é eficientemente transportado para ele.

No segundo caso, um temporário std::functioné criado, o lambda é moved, e esse temporário std::functioné usado por referência dentro do run_in_ui_thread.

Até agora, tudo bem - os dois têm desempenho idêntico. Exceto run_in_ui_threadque ele fará uma cópia do seu argumento de função para enviar ao thread da interface do usuário para executar! (ele retornará antes de terminar, portanto, não pode apenas usar uma referência a ele). Para o caso (A), nós simplesmente moveo std::functionem seu armazenamento a longo prazo. No caso (B), somos forçados a copiar o arquivo std::function.

Essa loja torna a passagem por valor mais ideal. Se houver alguma possibilidade de você estar armazenando uma cópia do std::function, passe por valor. Caso contrário, qualquer uma das maneiras é aproximadamente equivalente: a única desvantagem do valor é se você estiver usando o mesmo volume std::functione tendo um sub método após o outro. Salvo isso, a moveserá tão eficiente quanto a const&.

Agora, existem outras diferenças entre as duas que mais se destacam se tivermos um estado persistente dentro da std::function.

Suponha que o std::functionobjeto seja armazenado com a operator() const, mas também possui alguns mutablemembros de dados que ele modifica (que rude!).

No std::function<> const&caso, os mutablemembros de dados modificados serão propagados para fora da chamada de função. No std::function<>caso, eles não vão.

Este é um caso de canto relativamente estranho.

Você quer tratar std::functioncomo faria com qualquer outro tipo móvel, possivelmente pesado e barato. Mover é barato, copiar pode ser caro.

Yakk - Adam Nevraumont
fonte
A vantagem semântica de "passar por valor se você o estiver armazenando", como você diz, é que, por contrato, a função não pode manter o endereço do argumento passado. Mas é realmente verdade que "exceto isso, uma mudança será tão eficiente quanto uma const &"? Eu sempre vejo o custo de uma operação de cópia mais o custo da operação de movimentação. Com a passagem, const&vejo apenas o custo da operação de cópia.
Ceztko # 8/17
2
@ceztko Nos casos (A) e (B), o temporário std::functioné criado a partir do lambda. Em (A), o temporário é elidido no argumento para run_in_ui_thread. Em (B) uma referência ao referido temporário é passada para run_in_ui_thread. std::functionDesde que seus s sejam criados a partir de lambdas como temporários, essa cláusula é válida. O parágrafo anterior trata do caso em que o problema std::functionpersiste. Se nós não estão armazenando, apenas a criação de um lambda, function const&e functiontem exatamente o mesmo teto.
Yakk - Adam Nevraumont 8/17/17
Ah entendi! Isso, obviamente, depende do que acontece fora de run_in_ui_thread(). Existe apenas uma assinatura para dizer "Passe por referência, mas não armazenarei o endereço"?
Ceztko 8/17
@ceztko Não, não existe.
Yakk - Adam Nevraumont 8/17/17
1
@ Yakk-AdamNevraumont se seria mais completo para cobrir outra opção para passar por rvalue ref:std::future<void> run_in_ui_thread( std::function<void()>&& )
Pavel P
33

Se você está preocupado com o desempenho e não está definindo uma função de membro virtual, provavelmente não deve usá std::function-lo.

Tornar o tipo de functor um parâmetro de modelo permite uma otimização maior do que std::function, inclusive a inclusão da lógica do functor. O efeito dessas otimizações provavelmente superará em muito as preocupações de copiar contra indireto sobre como passar std::function.

Mais rápido:

template<typename Functor>
void callFunction(Functor&& x)
{
    x();
}
Ben Voigt
fonte
1
Na verdade, não estou preocupado com o desempenho. Eu apenas pensei que usar referências const, onde elas deveriam ser usadas, é uma prática comum (strings e vetores vêm à mente).
precisa saber é o seguinte
13
@ Ben: Eu acho que a maneira mais moderna de implementar isso é usar hippies std::forward<Functor>(x)();, para preservar a categoria de valor do functor, já que é uma referência "universal". Não fará diferença em 99% dos casos.
precisa saber é o seguinte
1
@ Ben Voigt, então, no seu caso, eu chamaria a função com uma mudança? callFunction(std::move(myFunctor));
Arias_JC
2
@arias_JC: Se o parâmetro é um lambda, já é um rvalue. Se você tiver um valor l, poderá usá- std::movelo se não precisar mais dele de outra maneira ou passar diretamente se não desejar sair do objeto existente. As regras de recolhimento de referência garantem que callFunction<T&>()tenha um parâmetro do tipo T&, não T&&.
Ben Voigt
1
@BoltzmannBrain: Eu escolhi não fazer essa alteração porque é válida apenas para o caso mais simples, quando a função é chamada apenas uma vez. Minha resposta é para a pergunta "como devo passar um objeto de função?" e não limitado a uma função que nada faz além de invocar incondicionalmente esse functor exatamente uma vez.
Ben Voigt
25

Como de costume no C ++ 11, passar por value / reference / const-reference depende do que você faz com seu argumento. std::functionnão é diferente.

A passagem por valor permite mover o argumento para uma variável (geralmente uma variável de membro de uma classe):

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

    Object m_o;
};

Quando você sabe que sua função moverá seu argumento, esta é a melhor solução, assim seus usuários poderão controlar como eles chamam sua funçã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

Acredito que você já conhece a semântica das referências (não) const, por isso não abordarei o assunto. Se você precisar de mais explicações, basta perguntar e eu atualizarei.

syam
fonte