Por que o lambda do C ++ 11 requer a palavra-chave "mutável" para captura por valor, por padrão?

256

Breve exemplo:

#include <iostream>

int main()
{
    int n;
    [&](){n = 10;}();             // OK
    [=]() mutable {n = 20;}();    // OK
    // [=](){n = 10;}();          // Error: a by-value capture cannot be modified in a non-mutable lambda
    std::cout << n << "\n";       // "10"
}

A pergunta: por que precisamos do mutable palavra chave? É bem diferente da passagem tradicional de parâmetros para funções nomeadas. Qual é a lógica por trás?

Fiquei com a impressão de que todo o objetivo da captura por valor é permitir que o usuário altere o temporário - caso contrário, estou quase sempre melhor usando a captura por referência, não?

Alguma iluminação?

(Estou usando o MSVC2010, a propósito. AFAIK isso deve ser padrão)

kizzx2
fonte
101
Boa pergunta; embora eu esteja feliz que algo esteja finalmente constpor padrão!
xtofl 31/03
3
Não é uma resposta, mas acho que isso é sensato: se você considera algo por valor, não deve alterá-lo apenas para salvar uma cópia em uma variável local. Pelo menos você não cometerá o erro de alterar n substituindo = por &.
31511 stefaanv
8
@xtofl: Não tenho certeza se é bom, quando tudo o resto não é constpor padrão.
kizzx2
8
@ Tamás Szelei: Não para iniciar uma discussão, mas IMHO o conceito "fácil de aprender" não tem lugar na linguagem C ++, especialmente nos dias modernos. De qualquer forma: P
kizzx2 31/03
3
"o ponto inteiro da captura por valor é permitir que o usuário altere o temporário" - Não, o ponto inteiro é que o lambda pode permanecer válido após o tempo de vida de qualquer variável capturada. Se as lambdas do C ++ tivessem apenas captura por ref, elas seriam inutilizáveis ​​em muitos cenários.
Sebastian Redl

Respostas:

230

Requer mutableporque, por padrão, um objeto de função deve produzir o mesmo resultado toda vez que é chamado. Essa é a diferença entre uma função orientada a objeto e uma função que usa uma variável global de maneira eficaz.

Cachorro
fonte
7
Este é um bom argumento. Eu concordo totalmente. No C ++ 0x, porém, não vejo como o padrão ajuda a impor o acima. Considere que estou no lado receptor da lambda, por exemplo, estou void f(const std::function<int(int)> g). Como posso garantir que gé realmente referencialmente transparente ? gfornecedor pode ter usado de mutablequalquer maneira. Então eu não vou saber. Por outro lado, se o padrão for não- conste as pessoas tiverem que adicionar objetos em constvez de mutablefuncionar, o compilador pode realmente impor a const std::function<int(int)>peça e agora fpode assumir que gé const, não?
Kizzx2
8
@ kizzx2: No C ++, nada é imposto , apenas sugerido. Como de costume, se você fizer algo estúpido (requisito documentado para transparência referencial e depois passar a função transparente não referencial), obtém o que quer que seja.
Filhote
6
Essa resposta abriu meus olhos. Anteriormente, eu pensava que, neste caso, o lambda apenas modifica uma cópia para a "execução" atual.
Zsolt Szatmari
4
@ZsoltSzatmari Seu comentário abriu meus olhos! : -DI não entendi o verdadeiro significado desta resposta até ler seu comentário.
Jendas
5
Eu discordo da premissa básica desta resposta. C ++ não tem o conceito de "funções sempre devem retornar o mesmo valor" em qualquer outro lugar da linguagem. Como princípio de design, eu concordaria que é uma boa maneira de escrever uma função, mas não acho que ela retenha a água como a razão do comportamento padrão.
Ionoclast Brigham
103

Seu código é quase equivalente a isso:

#include <iostream>

class unnamed1
{
    int& n;
public:
    unnamed1(int& N) : n(N) {}

    /* OK. Your this is const but you don't modify the "n" reference,
    but the value pointed by it. You wouldn't be able to modify a reference
    anyway even if your operator() was mutable. When you assign a reference
    it will always point to the same var.
    */
    void operator()() const {n = 10;}
};

class unnamed2
{
    int n;
public:
    unnamed2(int N) : n(N) {}

    /* OK. Your this pointer is not const (since your operator() is "mutable" instead of const).
    So you can modify the "n" member. */
    void operator()() {n = 20;}
};

class unnamed3
{
    int n;
public:
    unnamed3(int N) : n(N) {}

    /* BAD. Your this is const so you can't modify the "n" member. */
    void operator()() const {n = 10;}
};

int main()
{
    int n;
    unnamed1 u1(n); u1();    // OK
    unnamed2 u2(n); u2();    // OK
    //unnamed3 u3(n); u3();  // Error
    std::cout << n << "\n";  // "10"
}

Portanto, você pode pensar em lambdas como gerando uma classe com operator () cujo padrão é const, a menos que você diga que é mutável.

Você também pode pensar em todas as variáveis ​​capturadas dentro de [] (explícita ou implicitamente) como membros dessa classe: cópias dos objetos para [=] ou referências aos objetos para [&]. Eles são inicializados quando você declara seu lambda como se houvesse um construtor oculto.

Daniel Munoz
fonte
5
Enquanto uma boa explicação de que um constou mutablelambda olharia como se implementado como tipos definidos pelo usuário equivalentes, a questão é (como no título e elaborado pelo OP nos comentários) por que const é o padrão, de modo que este não atender.
Underscore_d
36

Fiquei com a impressão de que todo o objetivo da captura por valor é permitir que o usuário altere o temporário - caso contrário, estou quase sempre melhor usando a captura por referência, não?

A questão é: é "quase"? Um caso de uso frequente parece retornar ou passar lambdas:

void registerCallback(std::function<void()> f) { /* ... */ }

void doSomething() {
  std::string name = receiveName();
  registerCallback([name]{ /* do something with name */ });
}

Eu acho que mutablenão é um caso de "quase". Considero "captura por valor" como "permitir que eu use seu valor após a morte da entidade capturada" em vez de "permitir que eu altere uma cópia dele". Mas talvez isso possa ser discutido.

Johannes Schaub - litb
fonte
2
Bom exemplo. Este é um caso de uso muito forte para o uso de captura por valor. Mas por que o padrão é const? Que finalidade isso alcança? mutableparece deslocado aqui, quando nãoconst é o padrão em "quase" (: P) em todo o resto do idioma.
kizzx2
8
@ kizzx2: desejo que eu constera o padrão, pelo menos as pessoas seriam forçadas a considerar const-correção: /
Matthieu M.
1
@ kizzx2 olhando para os papéis lambda, parece-me que eles o tornam padrão, para constque pudessem chamá-lo se o objeto lambda é ou não const. Por exemplo, eles poderiam passar para uma função usando a std::function<void()> const&. Para permitir que o lambda altere suas cópias capturadas, nos documentos iniciais, os membros dos dados do fechamento foram definidos mutableinternamente automaticamente. Agora você deve inserir manualmente mutablea expressão lambda. Ainda não encontrei uma lógica detalhada.
Johannes Schaub - litb 31/03
2
Veja open-std.org/JTC1/SC22/WG21/docs/papers/2008/n2651.pdf para obter mais detalhes.
Johannes Schaub - litb 31/03
5
Neste ponto, para mim, a resposta / lógica "real" parece ser "eles falharam em solucionar um detalhe de implementação": /
kizzx2
32

FWIW, Herb Sutter, um membro bem conhecido do comitê de padronização C ++, fornece uma resposta diferente para essa pergunta em questões de correção e usabilidade do Lambda :

Considere este exemplo do homem de palha, em que o programador captura uma variável local por valor e tenta modificar o valor capturado (que é uma variável membro do objeto lambda):

int val = 0;
auto x = [=](item e)            // look ma, [=] means explicit copy
            { use(e,++val); };  // error: count is const, need ‘mutable’
auto y = [val](item e)          // darnit, I really can’t get more explicit
            { use(e,++val); };  // same error: count is const, need ‘mutable’

Esse recurso parece ter sido adicionado à preocupação de que o usuário talvez não perceba que ele recebeu uma cópia e, em particular, como as lambdas são copiáveis, ele pode estar alterando a cópia de uma lambda diferente.

Seu artigo é sobre por que isso deve ser alterado no C ++ 14. É curto, bem escrito, vale a pena ler se você quiser saber "o que está em mente [do membro do comitê]" com relação a esse recurso em particular.

akim
fonte
16

Você precisa pensar qual é o tipo de fechamento da sua função Lambda. Toda vez que você declara uma expressão Lambda, o compilador cria um tipo de fechamento, que nada mais é do que uma declaração de classe sem nome com atributos ( ambiente em que a expressão Lambda foi declarada) e a chamada de função ::operator()implementada. Quando você captura uma variável usando copiar por valor , o compilador cria um novo constatributo no tipo de fechamento, para que você não possa alterá-la dentro da expressão Lambda porque é um atributo "somente leitura", é por isso que eles chamá-lo de " fechamento " ", porque, de alguma forma, você está fechando sua expressão Lambda, copiando as variáveis ​​do escopo superior para o escopo do Lambda.mutable, a entidade capturada se tornará umnon-constatributo do seu tipo de fechamento. É isso que faz com que as alterações feitas na variável mutável capturada pelo valor não sejam propagadas para o escopo superior, mas permaneçam dentro do Lambda com monitoração de estado. Sempre tente imaginar o tipo de fechamento resultante da sua expressão Lambda, que me ajudou muito, e espero que possa ajudá-lo também.

Tarântula
fonte
14

Veja este rascunho , em 5.1.2 [expr.prim.lambda], subcláusula 5:

O tipo de fechamento para uma expressão lambda possui um operador público de chamada de função em linha (13.5.4) cujos parâmetros e tipo de retorno são descritos pela cláusula parameter-statement-statement e trailingreturn- expression da expressão lambda, respectivamente. Este operador de chamada de função é declarado const (9.3.1) se, e somente se, a cláusula parâmetro-declaração-declaração da expressão lambda não for seguida por mutável.

Editar no comentário do litb: Talvez eles pensassem em captura por valor, para que mudanças externas nas variáveis ​​não sejam refletidas dentro do lambda? As referências funcionam nos dois sentidos, então essa é a minha explicação. Não sei se é bom.

Editar no comentário do kizzx2: A maioria das vezes em que um lambda deve ser usado é como um functor para algoritmos. O constness padrão permite que ele seja usado em um ambiente constante, assim como constfunções normais qualificadas podem ser usadas lá, mas as funções não constqualificadas não podem. Talvez eles tenham pensado em torná-lo mais intuitivo para esses casos, que sabem o que se passa em sua mente. :)

Xeo
fonte
É o padrão, mas por que eles escreveram dessa maneira?
kizzx2
@ kizzx2: Minha explicação está diretamente sob essa citação. :) Isso se refere um pouco ao que litb diz sobre a vida útil dos objetos capturados, mas também vai um pouco mais longe.
Xeo
@ Xeo: Ah, sim, eu perdi isso: P Também é outra boa explicação para um bom uso da captura por valor . Mas por que deveria ser constpor padrão? Eu já tenho uma nova cópia, parece estranho não me deixar alterá-la - especialmente não é algo principalmente errado com ela - eles só querem que eu adicione mutable.
kizzx2
Acredito que houve uma tentativa de criar uma nova sintaxe de declaração de função genral, parecida com uma lambda nomeada. Também deveria corrigir outros problemas, tornando tudo const por padrão. Nunca foram concluídas, mas as idéias foram reproduzidas na definição de lambda.
Bo Persson
2
@ kizzx2 - Se pudéssemos começar tudo de novo, provavelmente teríamos varcomo uma palavra-chave para permitir que mudanças e constantes sejam o padrão para todo o resto. Agora não temos, então temos que conviver com isso. OMI, C ++ 2011 saiu muito bem, considerando tudo.
Bo Persson
11

Fiquei com a impressão de que todo o objetivo da captura por valor é permitir que o usuário altere o temporário - caso contrário, estou quase sempre melhor usando a captura por referência, não?

nnão é temporário. n é um membro do objeto de função lambda que você cria com a expressão lambda. A expectativa padrão é que chamar seu lambda não modifique seu estado; portanto, é constante impedir que você modifique acidentalmente n.

Martin Ba
fonte
1
O objeto lambda inteiro é temporário, seus membros também têm vida útil temporária.
Ben Voigt
2
@ Ben: IIRC, eu estava me referindo à questão de que quando alguém diz "temporário", eu entendo que isso significa objeto temporário sem nome , que é o próprio lambda, mas seus membros não são. E também que, de "dentro" do lambda, realmente não importa se o lambda em si é temporário. Relendo a questão, parece que o OP só queria dizer "n dentro do lambda" quando ele dizia "temporário".
Martin Ba
6

Você precisa entender o que captura significa! está capturando não passando argumentos! vamos dar uma olhada em alguns exemplos de código:

int main()
{
    using namespace std;
    int x = 5;
    int y;
    auto lamb = [x]() {return x + 5; };

    y= lamb();
    cout << y<<","<< x << endl; //outputs 10,5
    x = 20;
    y = lamb();
    cout << y << "," << x << endl; //output 10,20

}

Como você pode ver, embora xtenha sido alterado para 20o lambda, ainda está retornando 10 ( xainda está 5dentro do lambda) Mudar xdentro do lambda significa alterar o próprio lambda a cada chamada (o lambda está mudando a cada chamada). Para reforçar a correção, o padrão introduziu a mutablepalavra - chave. Ao especificar um lambda como mutável, você está dizendo que cada chamada ao lambda pode causar uma alteração no próprio lambda. Vamos ver outro exemplo:

int main()
{
    using namespace std;
    int x = 5;
    int y;
    auto lamb = [x]() mutable {return x++ + 5; };

    y= lamb();
    cout << y<<","<< x << endl; //outputs 10,5
    x = 20;
    y = lamb();
    cout << y << "," << x << endl; //outputs 11,20

}

O exemplo acima mostra que, ao tornar o lambda mutável, a alteração xdentro do lambda " modifica " o lambda a cada chamada com um novo valor xque não tem nada a ver com o valor real da xfunção principal

Soulimane Mammar
fonte
4

Existe agora uma proposta para aliviar a necessidade de mutabledeclarações lambda: n3424

usta
fonte
Alguma informação sobre o que aconteceu? Pessoalmente, acho que é uma péssima ideia, pois a nova "captura de expressões arbitrárias" suaviza a maioria dos pontos problemáticos.
quer
1
@BenVoigt Sim, parece uma mudança por causa da mudança.
Roteamento de milhas
3
@BenVoigt Embora seja justo, espero que provavelmente haja muitos desenvolvedores de C ++ que não sabem que isso mutableé uma palavra-chave em C ++.
Rota das milhas
1

Para estender a resposta do Puppy, as funções lambda devem ser puramente funções . Isso significa que toda chamada que recebe um conjunto de entrada exclusivo sempre retorna a mesma saída. Vamos definir entrada como o conjunto de todos os argumentos mais todas as variáveis ​​capturadas quando o lambda é chamado.

Em funções puras, a saída depende apenas da entrada e não de algum estado interno. Portanto, qualquer função lambda, se pura, não precisa alterar seu estado e, portanto, é imutável.

Quando um lambda captura por referência, a gravação em variáveis ​​capturadas é uma pressão sobre o conceito de função pura, porque tudo o que uma função pura deve fazer é retornar uma saída, embora o lambda certamente não sofra mutação porque a gravação acontece com variáveis ​​externas. Mesmo nesse caso, um uso correto implica que, se o lambda for chamado com a mesma entrada novamente, a saída será a mesma todas as vezes, apesar desses efeitos colaterais nas variáveis ​​by-ref. Tais efeitos colaterais são apenas maneiras de retornar alguma entrada adicional (por exemplo, atualizar um contador) e podem ser reformulados para uma função pura, por exemplo, retornar uma tupla em vez de um único valor.

Attersson
fonte