ti-enxame.com

Como as variáveis ​​estáticas nos objetos de função lambda funcionam?

As variáveis ​​estáticas usadas em um lambda são retidas nas chamadas da função em que o lambda é usado? Ou o objeto de função é "criado" novamente a cada chamada de função?

Exemplo inútil:

#include <iostream>
#include <vector>
#include <algorithm>

using std::cout;

void some_function()
{
    std::vector<int> v = {0,1,2,3,4,5};
    std::for_each( v.begin(), v.end(),
         [](const int &i)
         {
             static int calls_to_cout = 0;
             cout << "cout has been called " << calls_to_cout << " times.\n"
                  << "\tCurrent int: " << i << "\n";
             ++calls_to_cout;
         } );
}

int main()
{
    some_function();
    some_function();
}

Qual é a saída correta para este programa? Depende do fato de o lambda capturar variáveis ​​locais ou não? (certamente mudará a implementação subjacente do objeto de função, para que possa ter uma influência). É uma inconsistência comportamental permitida?

Não estou procurando: "Meu compilador gera ...", esse é um recurso muito novo para confiar nas implementações atuais IMHO. Eu sei que pedir cotações padrão parece ser popular desde que o mundo descobriu que isso existe, mas ainda assim, eu gostaria de uma fonte decente.

51
rubenvb

tl; versão dr na parte inferior.


§5.1.2 [expr.prim.lambda]

p1 expressão lambda:
lambda-introdutor lambda-declaratoropt declaração composta

p3 O tipo da lambda-expression (que também é o tipo do objeto de fechamento) é um tipo de classe não-union exclusivo e sem nome - chamado de tipo de fechamento - cujas propriedades estão descritas abaixo. Este tipo de classe não é um agregado (8.5.1). O tipo de fechamento é declarado no menor escopo de bloco, escopo de classe ou espaço de nome que contém a correspondente expressão lambda. ( Minha observação: as funções têm um escopo de bloco.)

p5 O tipo de fechamento para uma expressão lambda possui um operador público de chamada de função inline [...]

p7 A expressão lambda 's declaração composta produz o corpo da função (8.4) do operador de chamada de função [...]

Como a instrução composta é tomada diretamente como o corpo do operador da chamada de função e o tipo de fechamento é definido no menor escopo (mais interno), é o mesmo que escrever o seguinte:

void some_function()
{
    struct /*unnamed unique*/{
      inline void operator()(int const& i) const{
        static int calls_to_cout = 0;
        cout << "cout has been called " << calls_to_cout << " times.\n"
             << "\tCurrent int: " << i << "\n";
        ++calls_to_cout;

      }
    } lambda;
    std::vector<int> v = {0,1,2,3,4,5};
    std::for_each( v.begin(), v.end(), lambda);
}

Como C++ legal, é permitido que as funções tenham static variáveis ​​locais.

§3.7.1 [basic.stc.static]

p1 Todas as variáveis ​​que não possuem duração de armazenamento dinâmico, não possuem duração de armazenamento de encadeamento e não são locais têm duração de armazenamento estático. O armazenamento para essas entidades deve durar a duração do programa.

p3 A palavra-chave static pode ser usada para declarar uma variável local com duração de armazenamento estático. [...]

§6.7 [stmt.dcl] p4
(Isso trata da inicialização de variáveis ​​com duração de armazenamento estático em um escopo de bloco.)

[...] Caso contrário, essa variável é inicializada na primeira vez em que o controle passa por sua declaração; [...]


Reiterar:

  • O tipo de uma expressão lambda é criado no escopo mais interno.
  • É não criado novamente para cada chamada de função (isso não faria sentido, uma vez que o corpo da função envolvente seria como meu exemplo acima).
  • Ele obedece (quase) a todas as regras de classes/estruturas normais (apenas algumas coisas sobre this são diferentes), uma vez que é um tipo de classe que não é da união.

Agora que garantimos que para cada chamada de função, o tipo de fechamento é o mesmo, podemos dizer com segurança que a variável local estática também é a mesma; é inicializado na primeira vez em que o operador de chamada de função é chamado e permanece até o final do programa.

45
Xeo

A variável estática deve se comportar exatamente como em um corpo de função. No entanto, há poucas razões para usar um, pois um objeto lambda pode ter variáveis ​​de membro.

Na sequência, calls_to_cout é capturado pelo valor, que fornece ao lambda uma variável de membro com o mesmo nome, inicializada com o valor atual de calls_to_cout. Essa variável membro mantém seu valor entre as chamadas, mas é local para o objeto lambda, portanto, qualquer cópia do lambda terá sua própria variável de membro calls_to_cout em vez de compartilhar uma variável estática. Isso é muito mais seguro e melhor.

(e como os lambdas são const por padrão e esse lambda modifica calls_to_cout deve ser declarado como mutável.)

void some_function()
{
    vector<int> v = {0,1,2,3,4,5};
    int calls_to_cout = 0;
    for_each(v.begin(), v.end(),[calls_to_cout](const int &i) mutable
    {
        cout << "cout has been called " << calls_to_cout << " times.\n"
          << "\tCurrent int: " << i << "\n";
        ++calls_to_cout;
    });
}

Se você do deseja que uma única variável seja compartilhada entre instâncias do lambda, será melhor usar capturas. Basta capturar algum tipo de referência à variável. Por exemplo, aqui está uma função que retorna um par de funções que compartilham uma referência a uma única variável e cada função executa sua própria operação nessa variável compartilhada quando chamada.

std::Tuple<std::function<int()>,std::function<void()>>
make_incr_reset_pair() {
    std::shared_ptr<int> i = std::make_shared<int>(0);
    return std::make_Tuple(
      [=]() { return ++*i; },
      [=]() { *i = 0; });
}

int main() {
    std::function<int()> increment;
    std::function<void()> reset;
    std::tie(increment,reset) = make_incr_reset_pair();

    std::cout << increment() << '\n';
    std::cout << increment() << '\n';
    std::cout << increment() << '\n';
    reset();
    std::cout << increment() << '\n';
13
bames53

Uma estática pode ser construída na captura:

auto v = vector<int>(99);
generate(v.begin(), v.end(), [x = int(1)] () mutable { return x++; });

A lata lambda feita por outra lambda

auto inc = [y=int(1)] () mutable { 
    ++y; // has to be separate, it doesn't like ++y inside the []
    return [y, x = int(1)] () mutable { return y+x++; }; 
};
generate(v.begin(), v.end(), inc());

Aqui, y também pode ser capturado por referência, enquanto inc dura mais tempo.

6
QuentinUK

Eu não tenho uma cópia do padrão final e o rascunho não parece abordar o problema explicitamente (consulte a seção 5.1.2, iniciando na página 87 do PDF). Mas diz que uma expressão lambda é avaliada para um único objeto do tipo de fechamento , que pode ser chamado repetidamente. Sendo assim, acredito que o padrão exige que variáveis ​​estáticas sejam inicializadas uma e apenas uma vez, como se você tivesse escrito a classe, operator(), e a captura de variáveis ​​manualmente.

Mas como você diz, esse é um novo recurso; pelo menos por enquanto você está preso ao que quer que sua implementação faça, independentemente do que o padrão diz. É melhor estilo capturar explicitamente uma variável no escopo anexo de qualquer maneira.

2
David Seiler

A resposta curta: variáveis ​​estáticas declaradas dentro de um lambda funcionam da mesma forma que variáveis ​​estáticas de função no escopo anexo que foram capturadas automaticamente (por referência).

Nesse caso, mesmo que o objeto lambda seja retornado duas vezes, os valores persistem:

auto make_sum()
{
    static int sum = 0;
    static int count = 0;

    //Wrong, since these do not have static duration, they are implicitly captured
    //return [&sum, &count](const int&i){
    return [](const int&i){
        sum += i;
        ++count;

        cout << "sum: "<< sum << " count: " << count << endl;
    };
}

int main(int argc, const char * argv[]) {
    vector<int> v = {0,1,1,2,3,5,8,13};

    for_each(v.begin(), v.end(), make_sum());

    for_each(v.begin(), v.end(), make_sum());

    return 0;
}

vs:

auto make_sum()
{
    return [](const int&i){
        //Now they are inside the lambda
        static int sum = 0;
        static int count = 0;

        sum += i;
        ++count;

        cout << "sum: "<< sum << " count: " << count << endl;
    };
}

int main(int argc, const char * argv[]) {
    vector<int> v = {0,1,1,2,3,5,8,13};

    for_each(v.begin(), v.end(), make_sum());

    for_each(v.begin(), v.end(), make_sum());

    return 0;
}

Ambos dão a mesma saída:

sum: 0 count: 1
sum: 1 count: 2
sum: 2 count: 3
sum: 4 count: 4
sum: 7 count: 5
sum: 12 count: 6
sum: 20 count: 7
sum: 33 count: 8
sum: 33 count: 9
sum: 34 count: 10
sum: 35 count: 11
sum: 37 count: 12
sum: 40 count: 13
sum: 45 count: 14
sum: 53 count: 15
sum: 66 count: 16
0
stands2reason