Olá, Habrovsk. Em conexão com o início do recrutamento em um novo grupo no curso
"Desenvolvedor C ++" , estamos compartilhando com você a tradução da segunda parte do artigo "Lambdas: de C ++ 11 a C ++ 20". A primeira parte pode ser lida
aqui .

Na
primeira parte da série, vimos lambdas em termos de C ++ 03, C ++ 11 e C ++ 14. Neste artigo, descrevi as motivações por trás desse poderoso recurso C ++, uso básico, sintaxe e aprimoramentos em cada um dos padrões de linguagem. Mencionei também alguns casos limítrofes.
Agora é hora de passar para o C ++ 17 e dar uma olhada no futuro (muito perto!): C ++ 20.
EntradaUm lembrete rápido: a idéia para esta série surgiu após uma de nossas recentes reuniões do Grupo de Usuários C ++ em Cracóvia.
Tivemos uma sessão de programação ao vivo sobre a "história" das expressões lambda. A conversa foi conduzida pelo especialista em C ++ Thomas Kaminsky (
consulte o perfil do Thomas no Linkedin ). Aqui está o evento:
Lambdas: de C ++ 11 a C ++ 20 - Grupo de usuários C ++ Cracóvia .
Decidi pegar o código de Thomas (com sua permissão!) E escrever artigos baseados nele.Na primeira parte da série, falei sobre as expressões lambda da seguinte maneira:
- Sintaxe básica
- Tipo Lambda
- Operador de chamada
- Captura de variáveis (variáveis mutáveis, globais, estáticas, membros da classe e esse ponteiro, objetos somente para movimentação, armazenamento de constantes):
- Tipo de retorno
- IIFE - Expressão de Função Invocada Imediatamente
- Conversão para um ponteiro de função
- Tipo de retorno
- IIFE - Expressões invocadas imediatamente
- Converter em um ponteiro de função
- Melhorias no C ++ 14
- Saída do tipo de retorno
- Capturar com inicializador
- Capturar uma variável de membro
- Expressões lambda genéricas
A lista acima é apenas parte da história das expressões lambda!
Agora vamos ver o que mudou no C ++ 17 e o que obtemos no C ++ 20!
Melhorias no C ++ 17Norma (rascunho antes da publicação) da seção
N659 sobre lambdas:
[expr.prim.lambda] . O C ++ 17 trouxe duas melhorias significativas nas expressões lambda:
- constexpr lambda
- Capture * isto
O que essas inovações significam para nós? Vamos descobrir.
expressões lambda constexprComeçando com C ++ 17, o padrão define implicitamente
operator()
para um tipo lambda como
constexpr
, se possível:
Da expr.prim.lambda # 4 :
O operador de chamada de função é uma função constexpr se a declaração do parâmetro de condição da expressão lambda correspondente for seguida por constexpr, ou se satisfizer os requisitos para a função constexpr.
Por exemplo:
constexpr auto Square = [] (int n) { return n*n; }; // implicitly constexpr static_assert(Square(2) == 4);
Lembre-se de que no C ++ 17
constexpr
função deve seguir estas regras:
- não deve ser virtual;
- seu tipo de retorno deve ser um tipo literal;
- cada um dos tipos de seus parâmetros deve ser um tipo literal;
- seu corpo deve ser = delete, = default ou uma instrução composta que não contenha
- definições de asm
- goto expressões,
- tags
- tente bloquear ou
- a definição de uma variável não literal, variável estática ou variável de memória de fluxo contínuo para a qual a inicialização não é executada.
Que tal um exemplo mais prático?
template<typename Range, typename Func, typename T> constexpr T SimpleAccumulate(const Range& range, Func func, T init) { for (auto &&elem: range) { init += func(elem); } return init; } int main() { constexpr std::array arr{ 1, 2, 3 }; static_assert(SimpleAccumulate(arr, [](int i) { return i * i; }, 0) == 14); }
Você pode jogar com o código aqui:
@WandboxO código usa
constexpr
lambda e, em seguida, é passado para o algoritmo simples
SimpleAccumulate
. O algoritmo usa vários elementos do C ++ 17: as adições
constexpr
para
std::array
,
std::begin
e
std::end
(usadas em um loop
for
com um intervalo) agora também são
constexpr
, portanto, isso significa que todo o código pode ser executado em tempo de compilação.
Claro, isso não é tudo.
Você pode capturar variáveis (desde que também sejam
constexpr
):
constexpr int add(int const& t, int const& u) { return t + u; } int main() { constexpr int x = 0; constexpr auto lam = [x](int n) { return add(x, n); }; static_assert(lam(10) == 10); }
Mas há um caso interessante quando você não passa mais a variável capturada, por exemplo:
constexpr int x = 0; constexpr auto lam = [x](int n) { return n + x };
Nesse caso, em Clang, podemos receber o seguinte aviso:
warning: lambda capture 'x' is not required to be captured for this use
Provavelmente, isso se deve ao fato de que x pode ser alterado no local a cada uso (a menos que você o transfira ainda mais ou use o endereço desse nome).
Mas, por favor, diga-me se você conhece as regras oficiais para esse comportamento. Encontrei apenas (da
cppreference ) (mas não consigo encontrá-lo no rascunho ...)
(Nota do tradutor: como nossos leitores escrevem, provavelmente estou substituindo o valor de 'x' em todos os lugares em que é usado. É definitivamente impossível alterá-lo.)Uma expressão lambda pode ler o valor de uma variável sem capturá-la se a variável
* possui um número inteiro non-volatile
constante ou um tipo enumerado e foi inicializado com constexpr
ou
* é constexpr
e não possui membros mutáveis.Esteja preparado para o futuro:
No C ++ 20, teremos algoritmos padrão
constexpr
e, possivelmente, até alguns contêineres, portanto, lambdas
constexpr
serão muito úteis nesse contexto. Seu código será o mesmo para a versão em tempo de execução e para a versão
constexpr
(versão em tempo de compilação)!
Em poucas palavras:
constexpr
lambda permite que você seja consistente com a programação padrão e possivelmente tenha um código mais curto.
Agora vamos para o segundo recurso importante disponível no C ++ 17:
Captura de * istoCapture * istoVocê se lembra do nosso problema quando queríamos capturar um membro da classe? Por padrão, capturamos isso (como um ponteiro!) E, portanto, podemos ter problemas quando objetos temporários ficam fora do escopo ... Isso pode ser corrigido usando o método de captura com um inicializador (consulte a primeira parte da série). Mas agora, no C ++ 17, temos uma maneira diferente. Podemos embrulhar uma cópia * disso:
Você pode jogar com o código aqui:
@WandboxCapturar a variável de membro desejada usando capturar com o inicializador protege você de possíveis erros com valores temporários, mas não podemos fazer o mesmo quando queremos chamar um método como:
Por exemplo:
struct Baz { auto foo() { return [this] { print(); }; } void print() const { std::cout << s << '\n'; } std::string s; };
No C ++ 14, a única maneira de tornar o código mais seguro é capturá-
this
com um inicializador:
auto foo() { return [self=*this] { self.print(); }; } C ++ 17 : auto foo() { return [*this] { print(); }; }
Mais uma coisa:
Observe que se você escrever
[=]
em uma função de membro,
this
capturado implicitamente! Isso pode levar a erros no futuro ... e ficará obsoleto no C ++ 20.
Então, chegamos à próxima seção: o futuro.
O futuro com C ++ 20No C ++ 20, obtemos as seguintes funções:
- Permita
[=, this]
como uma captura lambda - P0409R2 e cancele a captura implícita disso via [=]
- P0806 - Extensão do pacote na
lambda init-capture: ... args = std::move (args)] () {}
- P0780 - captura estática,
thread_local
e lambda para ligações estruturadas - P1091 - padrão lambda (também com conceitos) - P0428R2
- Simplificando a captura implícita de Lambda - P0588R1
- Lambda construtiva e atribuível sem salvar o estado padrão - P0624R2
- Lambdas em um contexto não calculado - P0315R4
Na maioria dos casos, as funções recém-introduzidas “limpam” o uso do lambda e permitem alguns casos de uso avançados.
Por exemplo, com o
P1091, você pode capturar uma ligação estruturada.
Também temos esclarecimentos relacionados à captura disso. No C ++ 20, você receberá um aviso se capturar
[=]
em um método:
struct Baz { auto foo() { return [=] { std::cout << s << std::endl; }; } std::string s; }; GCC 9: warning: implicit capture of 'this' via '[=]' is deprecated in C++20
Se você realmente precisa capturar isso, escreva
[=, this]
.
Também há alterações relacionadas a casos de uso avançados, como contextos sem estado e lambdas sem estado que podem ser construídos por padrão.
Com as duas alterações, você pode escrever:
std::map<int, int, decltype([](int x, int y) { return x > y; })> map;
Leia os motivos desses recursos na primeira versão das frases:
P0315R0 e
P0624R0 .
Mas vamos olhar para um recurso interessante: modelos lambda.
Padrão LambdNo C ++ 14, obtivemos lambdas generalizadas, o que significa que os parâmetros declarados como auto são parâmetros de modelo.
Para lambda:
[](auto x) { x; }
O compilador gera uma instrução de chamada que corresponde ao seguinte método clichê:
template<typename T> void operator(T x) { x; }
Mas não havia como alterar esse parâmetro do modelo e usar os argumentos do modelo real. No C ++ 20, isso será possível.
Por exemplo, como podemos limitar nosso lambda para trabalhar apenas com vetores de algum tipo?
Podemos escrever uma lambda geral:
auto foo = []<typename T>(const auto& vec) { std::cout<< std::size(vec) << '\n'; std::cout<< vec.capacity() << '\n'; };
Mas se você chamá-lo com um parâmetro int (por exemplo,
foo(10);
), poderá receber algum erro difícil de ler:
prog.cc: In instantiation of 'main()::<lambda(const auto:1&)> [with auto:1 = int]': prog.cc:16:11: required from here prog.cc:11:30: error: no matching function for call to 'size(const int&)' 11 | std::cout<< std::size(vec) << '\n';
Em C ++ 20, podemos escrever:
auto foo = []<typename T>(std::vector<T> const& vec) { std::cout<< std::size(vec) << '\n'; std::cout<< vec.capacity() << '\n'; };
O lambda acima permite a declaração de chamada do modelo:
<typename T> void operator(std::vector<T> const& s) { ... }
O parâmetro do modelo segue a cláusula de captura
[]
.
Se você chamá-lo com
int (foo(10);)
, receberá uma mensagem melhor:
note: mismatched types 'const std::vector<T>' and 'int'
Você pode jogar com o código aqui:
@WandboxNo exemplo acima, o compilador pode nos alertar sobre inconsistências na interface lambda do que no código dentro do corpo.
Outro aspecto importante é que em um lambda universal você tem apenas uma variável, não o tipo de modelo. Portanto, se você deseja acessá-lo, deve usar decltype (x) (para uma expressão lambda com o argumento (auto x)). Isso torna algum código mais detalhado e complicado.
Por exemplo (usando o código de P0428):
auto f = [](auto const& x) { using T = std::decay_t<decltype(x)>; T copy = x; T::static_function(); using Iterator = typename T::iterator; }
Agora você pode escrever como:
auto f = []<typename T>(T const& x) { T::static_function(); T copy = x; using Iterator = typename T::iterator; }
Na seção acima, tivemos uma breve visão geral do C ++ 20, mas tenho outro caso de uso adicional para você. Essa técnica é ainda possível no C ++ 14. Então continue a ler.
Bônus - ELEVAÇÃO com lambdasAtualmente, temos um problema quando você tem sobrecargas de funções e deseja passá-las para algoritmos padrão (ou qualquer coisa que exija algum objeto chamado):
// two overloads: void foo(int) {} void foo(float) {} int main() { std::vector<int> vi; std::for_each(vi.begin(), vi.end(), foo); }
Obtemos o seguinte erro do GCC 9 (tronco):
error: no matching function for call to for_each(std::vector<int>::iterator, std::vector<int>::iterator, <unresolved overloaded function type>) std::for_each(vi.begin(), vi.end(), foo); ^^^^^
No entanto, existe um truque no qual podemos usar um lambda e depois chamar a função de sobrecarga desejada.
De forma básica, para tipos de valor simples, para nossas duas funções, podemos escrever o seguinte código:
std::for_each(vi.begin(), vi.end(), [](auto x) { return foo(x); });
E da forma mais geral, precisamos digitar um pouco mais:
Código bastante complicado ... certo? :)
Vamos tentar decifrá-lo:
Criamos um lambda genérico e depois transmitimos todos os argumentos que obtemos. Para determiná-lo corretamente, precisamos especificar noexcept e o tipo do valor de retorno. É por isso que precisamos duplicar o código de chamada - para obter os tipos corretos.
Essa macro LIFT funciona em qualquer compilador que suporte C ++ 14.
Você pode jogar com o código aqui:
@WandboxConclusãoNesta postagem, analisamos mudanças significativas no C ++ 17 e fornecemos uma visão geral dos novos recursos do C ++ 20.
Você pode perceber que, a cada iteração da linguagem, as expressões lambda se misturam com outros elementos do C ++. Por exemplo, antes do C ++ 17, não era possível usá-los no contexto de constexpr, mas agora é possível. Da mesma forma, com lambdas genéricas começando com C ++ 14 e sua evolução para C ++ 20 na forma de modelo lambdas. Estou faltando alguma coisa? Talvez você tenha algum exemplo emocionante? Por favor, deixe-me saber nos comentários!
ReferênciasC ++ 11 -
[expr.prim.lambda]C ++ 14 -
[expr.prim.lambda]C ++ 17 -
[expr.prim.lambda]Expressões lambda em C ++ | Documentos da MicrosoftSimon Brand -
Passando conjuntos de sobrecarga para funçõesJason Turner -
C ++ semanal - Ep 128 - Sintaxe de modelo do C ++ 20 para lambdasJason Turner -
C ++ semanalmente - Ep 41 - Suporte ao C ++ 17 do constexpr LambdaConvidamos a todos para o tradicional
seminário on-
line gratuito sobre o curso, que será realizado amanhã, 14 de junho.