Considere este programa bastante inútil:
#include <iostream>
int main(int argc, char* argv[]) {
int a = 5;
auto it = [&](auto self) {
return [&](auto b) {
std::cout << (a + b) << std::endl;
return self(self);
};
};
it(it)(4)(6)(42)(77)(999);
}
Basicamente, estamos tentando fazer um lambda que retorne a si próprio.
- MSVC compila o programa e é executado
- O gcc compila o programa e segfaults
- clang rejeita o programa com uma mensagem:
error: function 'operator()<(lambda at lam.cpp:6:13)>' with deduced return type cannot be used before it is defined
Qual compilador está certo? Existe uma violação de restrição estática, UB ou nenhuma?
Atualize que esta leve modificação é aceita pelo clang:
auto it = [&](auto& self, auto b) {
std::cout << (a + b) << std::endl;
return [&](auto p) { return self(self,p); };
};
it(it,4)(6)(42)(77)(999);
Atualização 2 : Entendo como escrever um functor que retorne a si próprio, ou como usar o combinador Y, para conseguir isso. Esta é mais uma questão de advogado de linguagem.
Atualização 3 : a questão não é se é legal para um lambda retornar em geral, mas sobre a legalidade dessa maneira específica de fazer isso.
Pergunta relacionada: C ++ lambda retornando a si próprio .
auto& self
que elimina o problema de referência pendente.Respostas:
O programa está mal formado (clang está correto) por [dcl.spec.auto] / 9 :
Basicamente, a dedução do tipo de retorno do lambda interno depende de si mesma (a entidade que está sendo nomeada aqui é o operador de chamada) - portanto, você deve fornecer explicitamente um tipo de retorno. Nesse caso em particular, isso é impossível, porque você precisa do tipo de lambda interno, mas não pode nomeá-lo. Mas há outros casos em que tentar forçar lambdas recursivas como essa pode funcionar.
Mesmo sem isso, você tem uma referência pendente .
Deixe-me elaborar um pouco mais, depois de discutir com alguém muito mais inteligente (ou seja, TC) Há uma diferença importante entre o código original (um pouco reduzido) e a nova versão proposta (da mesma forma reduzida):
E é para isso que a expressão interior
self(self)
não é dependentef1
, masself(self, p)
é dependentef2
. Quando expressões não são dependentes, elas podem ser usadas ... avidamente ( [temp.res] / 8 , por exemplo, comostatic_assert(false)
é um erro grave , independentemente de o modelo em que se encontrar ser instanciado ou não).Pois
f1
, um compilador (como, digamos, clang) pode tentar instanciar isso ansiosamente. Você conhece o tipo deduzido do lambda externo quando chega ao;
ponto#2
acima (é o tipo do lambda interno), mas estamos tentando usá-lo mais cedo do que isso (pense nisso como no ponto#1
) - estamos tentando para usá-lo enquanto ainda estamos analisando o lambda interno, antes de sabermos qual é o tipo. Isso afeta o dcl.spec.auto/9.No entanto,
f2
não podemos tentar instanciar ansiosamente, porque é dependente. Só podemos instanciar no ponto de uso, momento em que sabemos tudo.Para realmente fazer algo assim, você precisa de um combinador-y . A implementação do artigo:
E o que você quer é:
fonte
Edit : Parece haver alguma controvérsia sobre se essa construção é estritamente válida de acordo com a especificação C ++. A opinião predominante parece ser que não é válida. Veja as outras respostas para uma discussão mais aprofundada. O restante desta resposta se aplica se a construção for válida; o código aprimorado abaixo funciona com o MSVC ++ e o gcc, e o OP publicou mais códigos modificados que também funcionam com o clang.
Esse é um comportamento indefinido, porque o lambda interno captura o parâmetro
self
por referência, masself
fica fora do escopo após areturn
linha 7. Assim, quando o lambda retornado é executado posteriormente, ele acessa uma referência a uma variável que saiu do escopo.A execução do programa
valgrind
ilustra isso:Em vez disso, você pode alterar o lambda externo para se auto-referenciar em vez de em valor, evitando várias cópias desnecessárias e resolvendo o problema:
Isso funciona:
fonte
self
uma referência?self
uma referência, este problema vai embora , mas Clang ainda o rejeita por outra razãoself
é capturado por referência!TL; DR;
clang está correto.
Parece que a seção do padrão que torna este mal formado é [dcl.spec.auto] p9 :
Trabalho original através
Se examinarmos a proposta A Proposta para adicionar o Y Combinator à biblioteca padrão, ela fornece uma solução funcional:
e diz explicitamente que seu exemplo não é possível:
e faz referência a uma discussão na qual Richard Smith alude ao erro que o clang está causando :
Barry me indicou a proposta de acompanhamento Lambdas recursivas, que explica por que isso não é possível e contorna a
dcl.spec.auto#9
restrição e também mostra métodos para conseguir isso hoje sem ela:fonte
self
isso não pareça uma entidade.Parece que o clang está certo. Considere um exemplo simplificado:
Vamos passar por isso como um compilador (um pouco):
it
éLambda1
com um operador de chamada de modelo.it(it);
aciona a instanciação do operador de chamadaauto
, portanto, devemos deduzi-lo.Lambda1
.self(self)
self(self)
é exatamente o que começamos!Como tal, o tipo não pode ser deduzido.
fonte
Lambda1::operator()
é simplesmenteLambda2
. Então, nessa expressão lambda interna , também é conhecido o tipo de retorno deself(self)
, uma chamada de . Possivelmente, as regras formais impedem essa dedução trivial, mas a lógica apresentada aqui não. A lógica aqui equivale apenas a uma afirmação. Se as regras formais atrapalham, isso é uma falha nas regras formais.Lambda1::operator()
Lambda2
Bem, seu código não funciona. Mas isso faz:
Código do teste:
Seu código é UB e mal formado, não é necessário diagnóstico. O que é engraçado; mas ambos podem ser corrigidos independentemente.
Primeiro, o UB:
esse é UB, porque o
self
valor externo é capturado por valor, as capturas internasself
por referência e o retorno é retornado após aouter
execução. Então segfaulting é definitivamente bom.O conserto:
O código permanece incorreto. Para ver isso, podemos expandir as lambdas:
isso instancia
__outer_lambda__::operator()<__outer_lambda__>
:Então, a seguir, temos que determinar o tipo de retorno de
__outer_lambda__::operator()
.Passamos por isso linha por linha. Primeiro criamos
__inner_lambda__
tipo:Agora, olhe lá - seu tipo de retorno é
self(self)
, ou__outer_lambda__(__outer_lambda__ const&)
. Mas estamos tentando deduzir o tipo de retorno de__outer_lambda__::operator()(__outer_lambda__)
.Você não tem permissão para fazer isso.
Embora, de fato, o tipo de retorno de
__outer_lambda__::operator()(__outer_lambda__)
não seja realmente dependente do tipo de retorno__inner_lambda__::operator()(int)
, o C ++ não se importa ao deduzir os tipos de retorno; simplesmente verifica o código linha por linha.E
self(self)
é usado antes de deduzi-lo. Programa mal formado.Podemos corrigir isso ocultando
self(self)
até mais tarde:e agora o código está correto e compila. Mas acho que isso é um pouco de hack; basta usar o ycombinator.
fonte
operator()
não pode, em geral, ser deduzido até que seja instanciado (sendo chamado com algum argumento de algum tipo). E assim, uma reescrita manual de máquina para código baseado em modelo funciona muito bem.É fácil reescrever o código em termos das classes que um compilador geraria, ou melhor, deveria gerar para as expressões lambda.
Quando isso é feito, fica claro que o principal problema é apenas a referência pendente e que um compilador que não aceita o código é um tanto desafiado no departamento lambda.
A reescrita mostra que não há dependências circulares.
Uma versão totalmente de modelo para refletir a maneira como o lambda interno no código original captura um item de tipo de modelo:
Eu acho que é esse modelo no mecanismo interno, que as regras formais são projetadas para proibir. Se eles proibirem a construção original.
fonte
template< class > class Inner;
o modelooperator()
é ... instanciado? Bem, palavra errada. Escrito? ... duranteOuter::operator()<Outer>
antes da dedução do tipo de retorno do operador externo. EInner<Outer>::operator()
tem um chamado paraOuter::operator()<Outer>
si. E isso não é permitido. Agora, a maioria dos compiladores não perceber oself(self)
porque esperar para deduzir o tipo de retornoOuter::Inner<Outer>::operator()<int>
para quandoint
é passado. Sensible. Mas sente falta do mal formado do código.Innner<T>::operator()<U>
seja instanciado. Afinal, o tipo de retorno pode depender doU
aqui. Não, mas em geral.