O que está por trás da exclusão do operador de atribuição lambda?

Dec 13 2020

Acabei de saber que um mutablelambda (par ) em C ++ não pode ser atribuído a menos que tenha capturas vazias (cf ClosureType::operator=).

Exemplo:

auto x = 0;
auto l0 = [copy = x]() mutable {};
auto l1 = []() mutable {};

static_assert(not std::is_copy_assignable_v<decltype(l0)>);
static_assert(std::is_copy_assignable_v<decltype(std::ref(x))>);
static_assert(std::is_copy_assignable_v<decltype(l1)>);

Estou perguntando a razão por trás dessa escolha: Por que o é operator=excluído? Particularmente em cenários, onde poderia ser padronizado, ou seja, o lambda é mutablee todas as capturas podem ser atribuídas por cópia (por exemplo, l0no exemplo acima)?

Estou ciente dessa questão relacionada para não mutablelambdas. Mas eu gostaria de entender a decisão em vez de contorná-la.

Respostas

3 JeffGarrett Dec 14 2020 at 23:59

Suspeito que não foi proposto. Lambdas entrou na linguagem muito mais fraco do que os objetos de função para os quais eles são açúcar e lentamente estão recuperando a funcionalidade. Com relação às funções-membro especiais, P0624 propôs adicionar atribuibilidade e construtibilidade padrão para lambdas sem captura. Apenas a construtibilidade padrão foi proposta em R0, porque isso é o que o autor precisava e, sem dúvida, é a deficiência mais óbvia, mas a atribuibilidade foi proposta em R1 com base no feedback do comitê.

A construtibilidade padrão para lambdas com capturas certamente é consistente com a linguagem:

auto x1 = [i = 1]() { return i; };
static_assert(not std::is_default_constructible_v<decltype(x1)>); // why??

struct { int i = 1; auto operator()() { return i; } } x2;
static_assert(std::is_default_constructible_v<decltype(x2)>);

A atribuição também é consistente e útil. Como apenas um exemplo que vem à mente, houve uma proposta em algum ponto para ter um análogo de std::default_deletepara alocadores, ou seja, um tipo que poderia ser usado como um parâmetro de modelo std::unique_ptrpara ponteiros alocados para alocadores. Você pode imaginar o uso de um lambda para capturar o alocador e usá-lo para essa finalidade:

auto allocator_delete(auto& allocator) {
    using traits = typename std::allocator_traits<std::decay_t<decltype(allocator)>>;
    return [alloc=std::ref(allocator)](typename traits::pointer p) { traits::deallocate(alloc, p, 1); };
}
template<class Alloc> using allocator_deleter_t = decltype(allocator_delete(std::declval<Alloc&>()));
static_assert(not std::is_move_assignable_v<std::unique_ptr<int, allocator_deleter_t<std::allocator<int>>>>);
// why??

Mas você não pode religar (mover-atribuir a) isso unique_ptr, porque o lambda exclui artificialmente a atribuição, embora seu estado de captura o permita. Reescreva isso como um tipo de objeto de função e unique_ptré atribuível, com operadores de atribuição gerados para o tipo de objeto de função.

Esse é apenas um exemplo, mas espero que ele esclareça que se você deseja ou não atribuir ao estado de captura (o std::ref(allocator)) não é o mesmo que o que o operador de chamada tem permissão para fazer no estado de captura. (A resposta da pergunta vinculada está errada.)