Por que criar uma linguagem com tipos anônimos exclusivos?


91

Isso é algo que sempre me incomodou como um recurso das expressões lambda C ++: o tipo de uma expressão lambda C ++ é único e anônimo, simplesmente não consigo anotá-lo. Mesmo se eu criar dois lambdas que são sintaticamente exatamente iguais, os tipos resultantes são definidos para serem distintos. A conseqüência é que a) lambdas só podem ser passados ​​para funções de template que permitem o tempo de compilação, tipo indizível a ser passado junto com o objeto, eb) que lambdas só são úteis uma vez que seu tipo seja apagado por meio std::function<>.

Ok, mas é assim que C ++ faz, eu estava pronto para descartá-lo como apenas um recurso enfadonho dessa linguagem. No entanto, acabei de aprender que Rust aparentemente faz o mesmo: cada função Rust ou lambda tem um tipo único e anônimo. E agora estou me perguntando: por quê?

Então, minha pergunta é a seguinte:
Qual é a vantagem, do ponto de vista do designer de linguagem, para introduzir o conceito de um tipo único e anônimo em uma linguagem?


6
como sempre, a melhor pergunta é por que não.
Stargateur

31
"que lambdas só são úteis uma vez que são apagados de tipo via std :: function <>" - não, eles são diretamente úteis sem std::function. Um lambda que foi passado para uma função de modelo pode ser chamado diretamente sem envolvimento std::function. O compilador pode então embutir o lambda na função de modelo, o que aumentará a eficiência do tempo de execução.
Erlkoenig

1
Meu palpite, torna a implementação de lambda's mais fácil e torna a linguagem mais fácil de entender. Se você permitiu que a mesma expressão lambda exata fosse dobrada no mesmo tipo, você precisaria de regras especiais para manipular, { int i = 42; auto foo = [&i](){ return i; }; } { int i = 13; auto foo = [&i](){ return i; }; }uma vez que a variável a que se refere é diferente, embora textualmente sejam iguais. Se você apenas disser que todos eles são únicos, não precisa se preocupar em tentar descobrir.
NathanOliver

5
mas você também pode dar um nome a um tipo de lambdas e fazer o mesmo com isso. lambdas_type = decltype( my_lambda);
maior_prime_is_463035818

3
Mas o que deve ser um tipo de lambda genérico [](auto) {}? Deve ter um tipo, para começar?
Evg

Respostas:


77

Muitos padrões (especialmente C ++) adotam a abordagem de minimizar o quanto eles exigem dos compiladores. Francamente, eles já exigem o suficiente! Se eles não tiverem que especificar algo para fazer funcionar, eles tendem a deixar a implementação definida.

Se os lambdas não fossem anônimos, teríamos que defini-los. Isso teria a dizer muito sobre como as variáveis ​​são capturadas. Considere o caso de um lambda [=](){...}. O tipo teria que especificar quais tipos realmente foram capturados pelo lambda, o que poderia ser não trivial de determinar. Além disso, e se o compilador otimizar com êxito uma variável? Considerar:

static const int i = 5;
auto f = [i]() { return i; }

Um compilador de otimização poderia reconhecer facilmente que o único valor possível de ique poderia ser capturado é 5 e substituí-lo por auto f = []() { return 5; }. No entanto, se o tipo não for anônimo, isso pode alterar o tipo ou forçar o compilador a otimizar menos, armazenando imesmo que ele realmente não precise disso. Isso é todo um saco de complexidade e nuances que simplesmente não são necessários para o que os lambdas pretendem fazer.

E, no caso de você realmente precisar de um tipo não anônimo, você sempre pode construir a classe de encerramento sozinho e trabalhar com um functor em vez de uma função lambda. Assim, eles podem fazer lambdas lidar com o caso de 99%, e deixar você codificar sua própria solução no 1%.


O desduplicador apontou em comentários que eu não trato da exclusividade tanto quanto do anonimato. Estou menos certo dos benefícios da exclusividade, mas é importante notar que o comportamento do seguinte é claro se os tipos forem únicos (a ação será instanciada duas vezes).

int counter()
{
    static int count = 0;
    return count++;
}

template <typename FuncT>
void action(const FuncT& func)
{
    static int ct = counter();
    func(ct);
}

...
for (int i = 0; i < 5; i++)
    action([](int j) { std::cout << j << std::endl; });

for (int i = 0; i < 5; i++)
    action([](int j) { std::cout << j << std::endl; });

Se os tipos não fossem únicos, teríamos que especificar qual comportamento deveria acontecer neste caso. Isso pode ser complicado. Algumas das questões levantadas no tópico do anonimato também levantam sua cabeça feia neste caso para exclusividade.


Observe que não se trata realmente de salvar trabalho para um implementador de compilador, mas salvar trabalho para o mantenedor dos padrões. O compilador ainda precisa responder a todas as questões acima para sua implementação específica, mas elas não estão especificadas no padrão.
ComicSansMS

2
@ComicSansMS Juntar essas coisas ao implementar um compilador é muito mais fácil quando você não precisa ajustar sua implementação ao padrão de outra pessoa. Falando por experiência própria, muitas vezes é muito mais fácil para um mantenedor de padrões especificar excessivamente a funcionalidade do que tentar encontrar a quantidade mínima a ser especificada enquanto obtém a funcionalidade desejada em seu idioma. Como um excelente estudo de caso, veja quanto trabalho eles gastaram evitando especificar excessivamente memory_order_consume enquanto ainda o tornavam útil (em algumas arquiteturas)
Cort Ammon

1
Como todo mundo, você defende o anônimo . Mas é realmente uma boa ideia forçá- lo a ser único também?
Deduplicator

Não é a complexidade do compilador que importa aqui, mas a complexidade do código gerado. O objetivo não é tornar o compilador mais simples, mas dar a ele espaço de manobra suficiente para otimizar todos os casos e produzir código natural para a plataforma de destino.
Jan Hudec

Você não pode capturar uma variável estática.
Ruslan

69

Lambdas não são apenas funções, eles são uma função e um estado . Portanto, tanto C ++ quanto Rust os implementam como um objeto com um operador de chamada ( operator()em C ++, as 3 Fn*características de Rust).

Basicamente, [a] { return a + 1; }em C ++ desugars para algo como

struct __SomeName {
    int a;

    int operator()() {
        return a + 1;
    }
};

em seguida, usando uma instância de __SomeNameonde o lambda é usado.

Enquanto em Rust, || a + 1em Rust irá desugar para algo como

{
    struct __SomeName {
        a: i32,
    }

    impl FnOnce<()> for __SomeName {
        type Output = i32;
        
        extern "rust-call" fn call_once(self, args: ()) -> Self::Output {
            self.a + 1
        }
    }

    // And FnMut and Fn when necessary

    __SomeName { a }
}

Isso significa que a maioria dos lambdas deve ter tipos diferentes .

Agora, existem algumas maneiras de fazer isso:

  • Com tipos anônimos, que é o que ambas as linguagens implementam. Outra consequência disso é que todos os lambdas devem ter um tipo diferente. Mas para designers de linguagem, isso tem uma vantagem clara: Lambdas podem ser descritos simplesmente usando outras partes mais simples já existentes da linguagem. Eles são apenas açúcar de sintaxe em torno de partes já existentes da linguagem.
  • Com alguma sintaxe especial para nomear tipos de lambda: No entanto, isso não é necessário, pois lambdas já podem ser usados ​​com modelos em C ++ ou com genéricos e os Fn*traços em Rust. Nenhuma das linguagens força você a apagar lambdas para usá-los ( std::functionem C ++ ou Box<Fn*>em Rust).

Observe também que ambas as linguagens concordam que lambdas triviais que não capturam contexto podem ser convertidos em ponteiros de função.


Descrever recursos complexos de uma linguagem usando recursos mais simples é bastante comum. Por exemplo, C ++ e Rust têm loops range-for, e ambos os descrevem como sintaxe de açúcar para outros recursos.

C ++ define

for (auto&& [first,second] : mymap) {
    // use first and second
}

como sendo equivalente a

{

    init-statement
    auto && __range = range_expression ;
    auto __begin = begin_expr ;
    auto __end = end_expr ;
    for ( ; __begin != __end; ++__begin) {

        range_declaration = *__begin;
        loop_statement

    }

} 

e Rust define

for <pat> in <head> { <body> }

como sendo equivalente a

let result = match ::std::iter::IntoIterator::into_iter(<head>) {
    mut iter => {
        loop {
            let <pat> = match ::std::iter::Iterator::next(&mut iter) {
                ::std::option::Option::Some(val) => val,
                ::std::option::Option::None => break
            };
            SemiExpr(<body>);
        }
    }
};

que, embora pareçam mais complicados para um humano, são mais simples para um designer de linguagem ou um compilador.


15
@ cmaster-reinstatemonica Considere passar um lambda como um argumento comparador para uma função de classificação. Você realmente deseja impor uma sobrecarga de chamadas de função virtual aqui?
Daniel Langr

5
@ cmaster-reinstatemonica porque nada é virtual por padrão em C ++
Caleth

4
@cmaster - Você quer dizer forçar todos os usuários de lambdas a pagar por dipatch dinâmico, mesmo quando não precisarão dele?
StoryTeller - Unslander Monica

4
@ cmaster-reinstatemonica O melhor que você terá é opt-in para virtual. Adivinha o quê, std::functionisso
Caleth

9
@ cmaster-reinstatemonica qualquer mecanismo onde você pode re-apontar a função a ser chamada terá situações com sobrecarga de tempo de execução. Esse não é o jeito do C ++. Você std::function
ativa

13

(Adicionando à resposta de Caleth, mas muito longo para caber em um comentário.)

A expressão lambda é apenas um açúcar sintático para uma estrutura anônima (um tipo de Voldemort, porque você não pode dizer seu nome).

Você pode ver a semelhança entre uma estrutura anônima e o anonimato de um lambda neste snippet de código:

#include <iostream>
#include <typeinfo>

using std::cout;

int main() {
    struct { int x; } foo{5};
    struct { int x; } bar{6};
    cout << foo.x << " " << bar.x << "\n";
    cout << typeid(foo).name() << "\n";
    cout << typeid(bar).name() << "\n";
    auto baz = [x = 7]() mutable -> int& { return x; };
    auto quux = [x = 8]() mutable -> int& { return x; };
    cout << baz() << " " << quux() << "\n";
    cout << typeid(baz).name() << "\n";
    cout << typeid(quux).name() << "\n";
}

Se isso ainda é insatisfatório para um lambda, também deve ser insatisfatório para uma estrutura anônima.

Algumas linguagens permitem um tipo de digitação de pato que é um pouco mais flexível e, embora C ++ tenha modelos que realmente não ajudam a fazer um objeto a partir de um modelo que tem um campo de membro que pode substituir um lambda diretamente em vez de usar um std::functionembrulho.


3
Obrigado, isso realmente ajuda a entender o raciocínio por trás da forma como os lambdas são definidos em C ++ (eu me lembrei do termo "tipo de Voldemort" :-)). No entanto, a questão permanece: Qual é a vantagem disso aos olhos de um designer de linguagem?
cmaster - restabelecer monica

1
Você pode até adicionar int& operator()(){ return x; }a essas estruturas
Caleth

2
@ cmaster-reinstatemonica • Especulativamente ... o resto do C ++ se comporta dessa maneira. Fazer lambdas usar algum tipo de "formato de superfície" de digitação de pato seria algo muito diferente do resto da linguagem. Adicionar esse tipo de facilidade na linguagem para lambdas provavelmente seria considerado generalizado para a linguagem inteira, e isso seria uma mudança potencialmente enorme. A omissão de tal facilidade apenas para lambdas se encaixa com a tipificação fortemente ish do resto do C ++.
Eljay

Tecnicamente, um tipo de Voldemort seria auto foo(){ struct DarkLord {} tom_riddle; return tom_riddle; }, porque fora do foonada pode usar o identificadorDarkLord
Caleth

@ cmaster-reinstatemonica eficiência, a alternativa seria encaixotar e despachar dinamicamente cada lambda (alocá-lo no heap e apagar seu tipo preciso). Agora, como você observou, o compilador poderia desduplicar os tipos anônimos de lambdas, mas você ainda não seria capaz de escrevê-los e isso exigiria um trabalho significativo para obter muito pouco ganho, portanto, as chances não são realmente favoráveis.
Masklinn

10

Por que criar uma linguagem com tipos anônimos exclusivos ?

Porque há casos em que os nomes são irrelevantes e inúteis ou mesmo contraproducentes. Nesse caso, a capacidade de abstrair sua existência é útil porque reduz a poluição de nomes e resolve um dos dois problemas difíceis na ciência da computação (como nomear as coisas). Pelo mesmo motivo, objetos temporários são úteis.

lambda

A exclusividade não é uma coisa lambda especial, ou mesmo algo especial para tipos anônimos. Ele também se aplica a tipos nomeados no idioma. Considere o seguinte:

struct A {
    void operator()(){};
};

struct B {
    void operator()(){};
};

void foo(A);

Note-se que não posso passar Bem foo, embora as classes são idênticos. Esta mesma propriedade se aplica a tipos não nomeados.

lambdas só podem ser passados ​​para funções de template que permitem o tempo de compilação, tipo indizível a ser passado junto com o objeto ... apagado via std :: function <>.

Há uma terceira opção para um subconjunto de lambdas: Lambdas que não capturam podem ser convertidos em ponteiros de função.


Observe que, se as limitações de um tipo anônimo forem um problema para um caso de uso, a solução é simples: um tipo nomeado pode ser usado em seu lugar. Lambdas não fazem nada que não possa ser feito com uma classe nomeada.


10

A resposta aceita de Cort Ammon é boa, mas acho que há mais um ponto importante a ser feito sobre a implementabilidade.

Suponha que eu tenha duas unidades de tradução diferentes, "one.cpp" e "two.cpp".

// one.cpp
struct A { int operator()(int x) const { return x+1; } };
auto b = [](int x) { return x+1; };
using A1 = A;
using B1 = decltype(b);

extern void foo(A1);
extern void foo(B1);

As duas sobrecargas de foousam o mesmo identificador ( foo), mas têm nomes mutilados diferentes. (No Itanium ABI usado em sistemas POSIX-ish, os nomes mutilados são _Z3foo1Ae, neste caso particular _Z3fooN1bMUliE_E,.)

// two.cpp
struct A { int operator()(int x) const { return x + 1; } };
auto b = [](int x) { return x + 1; };
using A2 = A;
using B2 = decltype(b);

void foo(A2) {}
void foo(B2) {}

O compilador C ++ deve garantir que o nome mutilado de void foo(A1)em "two.cpp" seja o mesmo que o nome mutilado de extern void foo(A2)em "one.cpp", para que possamos vincular os dois arquivos-objeto. Este é o significado físico de dois tipos sendo "o mesmo tipo": é essencialmente sobre compatibilidade ABI entre arquivos de objeto compilados separadamente.

O compilador C ++ não é necessário para garantir que B1e B2são "do mesmo tipo". (Na verdade, é necessário garantir que sejam de tipos diferentes; mas isso não é tão importante agora.)


Qual mecanismo físico o compilador usa para garantir que A1e A2sejam "do mesmo tipo"?

Ele simplesmente explora typedefs e, em seguida, examina o nome totalmente qualificado do tipo. É um tipo de classe chamado A. (Bem, ::Ajá que está no namespace global.) Portanto, é o mesmo tipo em ambos os casos. Isso é fácil de entender. Mais importante, é fácil de implementar . Para ver se dois tipos de classes são do mesmo tipo, pegue seus nomes e faça a strcmp. Para transformar um tipo de classe em um nome mutilado de função, você escreve o número de caracteres em seu nome, seguido por esses caracteres.

Portanto, os tipos nomeados são fáceis de destruir.

Que mecanismo físico o compilador pode usar para garantir que B1e B2sejam "do mesmo tipo", em um mundo hipotético onde C ++ exigia que fossem do mesmo tipo?

Bem, não poderia usar o nome do tipo, porque o tipo não tem um nome.

Talvez pudesse codificar de alguma forma o texto do corpo do lambda. Mas isso seria meio estranho, porque na verdade o bem "one.cpp" é sutilmente diferente do bem "two.cpp": "one.cpp" tem x+1e "two.cpp" tem x + 1. Então, nós teríamos que chegar a uma regra que diz que quer que esta diferença de espaço em branco não importa, ou que ele faz (tornando-os tipos diferentes depois de tudo), ou que talvez sim (talvez a validade do programa é a implementação-definido ou talvez seja "malformado, sem necessidade de diagnóstico"). De qualquer forma,A

A maneira mais fácil de sair da dificuldade é simplesmente dizer que cada expressão lambda produz valores de um tipo único. Então, dois tipos lambda definidos em unidades de tradução diferentes definitivamente não são o mesmo tipo . Em uma única unidade de tradução, podemos "nomear" tipos lambda apenas contando a partir do início do código-fonte:

auto a = [](){};  // a has type $_0
auto b = [](){};  // b has type $_1
auto f(int x) {
    return [x](int y) { return x+y; };  // f(1) and f(2) both have type $_2
} 
auto g(float x) {
    return [x](int y) { return x+y; };  // g(1) and g(2) both have type $_3
} 

É claro que esses nomes têm significado apenas nesta unidade de tradução. Este TU $_0é sempre um tipo diferente de algum outro TU $_0, embora este TU struct Aseja sempre do mesmo tipo que algum outro TU struct A.

A propósito, observe que nossa idéia de "codificar o texto do lambda" tinha outro problema sutil: lambdas $_2e $_3consistem exatamente no mesmo texto , mas eles claramente não devem ser considerados do mesmo tipo!


A propósito, C ++ exige que o compilador saiba como destruir o texto de uma expressão C ++ arbitrária , como em

template<class T> void foo(decltype(T())) {}
template void foo<int>(int);  // _Z3fooIiEvDTcvT__EE, not _Z3fooIiEvT_

Mas C ++ (ainda) não exige que o compilador saiba como destruir uma instrução C ++ arbitrária . decltype([](){ ...arbitrary statements... })ainda está mal formado mesmo em C ++ 20.


Observe também que é fácil fornecer um alias local para um tipo sem nome usando typedef/ using. Tenho a sensação de que sua pergunta pode ter surgido ao tentar fazer algo que poderia ser resolvido assim.

auto f(int x) {
    return [x](int y) { return x+y; };
}

// Give the type an alias, so I can refer to it within this translation unit
using AdderLambda = decltype(f(0));

int of_one(AdderLambda g) { return g(1); }

int main() {
    auto f1 = f(1);
    assert(of_one(f1) == 2);
    auto f42 = f(42);
    assert(of_one(f42) == 43);
}

EDITADO PARA ADICIONAR: lendo alguns de seus comentários em outras respostas, parece que você está se perguntando por quê

int add1(int x) { return x + 1; }
int add2(int x) { return x + 2; }
static_assert(std::is_same_v<decltype(add1), decltype(add2)>);
auto add3 = [](int x) { return x + 3; };
auto add4 = [](int x) { return x + 4; };
static_assert(not std::is_same_v<decltype(add3), decltype(add4)>);

Isso porque lambdas sem captura são construtíveis por padrão. (Em C ++ apenas a partir de C ++ 20, mas sempre foi conceitualmente verdadeiro.)

template<class T>
int default_construct_and_call(int x) {
    T t;
    return t(x);
}

assert(default_construct_and_call<decltype(add3)>(42) == 45);
assert(default_construct_and_call<decltype(add4)>(42) == 46);

Se você tentasse default_construct_and_call<decltype(&add1)>, tseria um ponteiro de função inicializado por padrão e provavelmente causaria um segfault. Isso não é útil.


Na verdade, é necessário garantir que sejam de tipos diferentes; mas isso não é tão importante agora. ” Eu me pergunto se há um bom motivo para forçar a exclusividade, se definido de forma equivalente.
Deduplicator

Pessoalmente, acho que um comportamento completamente definido é (quase?) Sempre melhor do que um comportamento não especificado. "Esses dois ponteiros de função são iguais? Bem, apenas se essas duas instanciações de modelo forem a mesma função, o que é verdadeiro apenas se esses dois tipos lambda forem do mesmo tipo, o que é verdadeiro apenas se o compilador decidir fundi-los." Icky! (Mas observe que temos uma situação exatamente análoga à fusão string-literal, e ninguém está perturbado com essa situação. Portanto, duvido que seja catastrófico permitir que o compilador mescle tipos idênticos.)
Quuxplusone

Bem, se duas funções equivalentes (exceto como se) podem ser idênticas também é uma boa questão. A linguagem no padrão não é muito óbvia para funções gratuitas e / ou estáticas. Mas isso está fora do escopo aqui.
Deduplicator

Por acaso, houve uma discussão neste mês na lista de discussão do LLVM sobre funções de fusão. O codegen do Clang fará com que funções com corpos completamente vazios sejam mescladas quase "por acidente": godbolt.org/z/obT55b Isso é tecnicamente incompatível e eu acho que eles provavelmente corrigirão o LLVM para parar de fazer isso. Mas sim, concordo, mesclar endereços de função também é uma coisa.
Quuxplusone

Esse exemplo tem outros problemas, nomeadamente declaração de retorno ausente. Eles já não tornam o código não conforme? Além disso, procurarei a discussão, mas eles mostraram ou presumiram que a mesclagem de funções equivalentes não está em conformidade com o padrão, seu comportamento documentado, com o gcc, ou apenas que alguns acreditam que isso não aconteça?
Deduplicator

9

Os lambdas do C ++ precisam de tipos distintos para operações distintas, pois o C ++ se vincula estaticamente. Eles só podem ser copiados / movidos, portanto, principalmente, você não precisa nomear seu tipo. Mas tudo isso é um detalhe de implementação.

Não tenho certeza se lambdas C # têm um tipo, pois são "expressões de função anônimas" e são imediatamente convertidas em um tipo de delegado compatível ou tipo de árvore de expressão. Se sim, provavelmente é um tipo impronunciável.

C ++ também possui estruturas anônimas, onde cada definição leva a um tipo único. Aqui o nome não é impronunciável, simplesmente não existe no que diz respeito ao padrão.

C # tem tipos de dados anônimos , que proíbe cuidadosamente de escapar do escopo em que foram definidos. A implementação também dá um nome único e impronunciável a eles.

Ter um tipo anônimo indica ao programador que ele não deve mexer em sua implementação.

A parte, de lado:

Você pode dar um nome ao tipo de um lambda.

auto foo = []{}; 
using Foo_t = decltype(foo);

Se você não tiver nenhuma captura, pode usar um tipo de ponteiro de função

void (*pfoo)() = foo;

1
O primeiro exemplo de código ainda não permitirá um subsequente Foo_t = []{};, apenas Foo_t = fooe nada mais.
cmaster - restabelecer monica

1
@ cmaster-reinstatemonica isso ocorre porque o tipo não pode ser construído por padrão, não por causa do anonimato. Meu palpite é que isso tem tanto a ver com evitar um conjunto ainda maior de casos esquivos que você precisa lembrar, quanto com qualquer razão técnica.
Caleth

6

Por que usar tipos anônimos?

Para os tipos gerados automaticamente pelo compilador, a escolha é (1) honrar a solicitação do usuário para o nome do tipo ou (2) deixar o compilador escolher um por conta própria.

  1. No primeiro caso, espera-se que o usuário forneça explicitamente um nome cada vez que tal construção aparecer (C ++ / Rust: sempre que um lambda é definido; Rust: sempre que uma função é definida). Esse é um detalhe tedioso para o usuário fornecer todas as vezes e, na maioria dos casos, o nome nunca é referido novamente. Portanto, faz sentido deixar o compilador descobrir um nome para ele automaticamente e usar recursos existentes, como decltypeinferência de tipo ou, para fazer referência ao tipo nos poucos lugares onde ele é necessário.

  2. No último caso, o compilador precisa escolher um nome exclusivo para o tipo, que provavelmente seria um nome obscuro e ilegível como __namespace1_module1_func1_AnonymousFunction042. O designer da linguagem poderia especificar precisamente como esse nome é construído em detalhes gloriosos e delicados, mas isso expõe desnecessariamente ao usuário um detalhe de implementação no qual nenhum usuário sensato poderia confiar, uma vez que o nome é sem dúvida frágil em face de refatoradores menores. Isso também restringe desnecessariamente a evolução da linguagem: futuras adições de recursos podem fazer com que o algoritmo de geração de nomes existente seja alterado, levando a problemas de compatibilidade com versões anteriores. Portanto, faz sentido simplesmente omitir esse detalhe e afirmar que o tipo gerado automaticamente não pode ser dito pelo usuário.

Por que usar tipos únicos (distintos)?

Se um valor tiver um tipo exclusivo, um compilador de otimização pode rastrear um tipo exclusivo em todos os seus sites de uso com fidelidade garantida. Como corolário, o usuário pode ter certeza dos locais onde a proveniência desse valor específico é totalmente conhecida pelo compilador.

Por exemplo, no momento em que o compilador vê:

let f: __UniqueFunc042 = || { ... };  // definition of __UniqueFunc042 (assume it has a nontrivial closure)

/* ... intervening code */

let g: __UniqueFunc042 = /* some expression */;
g();

o compilador tem plena confiança que gdeve necessariamente se originar de f, mesmo sem saber a proveniência de g. Isso permitiria que a chamada gfosse desvirtualizada. O usuário saberia disso também, pois o usuário tomou muito cuidado para preservar o tipo único de fatravés do fluxo de dados que o conduziu g.

Necessariamente, isso restringe o que o usuário pode fazer f. O usuário não tem liberdade para escrever:

let q = if some_condition { f } else { || {} };  // ERROR: type mismatch

pois isso levaria à unificação (ilegal) de dois tipos distintos.

Para contornar isso, o usuário pode fazer o upcast do __UniqueFunc042para o tipo não exclusivo &dyn Fn(),

let f2 = &f as &dyn Fn();  // upcast
let q2 = if some_condition { f2 } else { &|| {} };  // OK

A compensação feita por este tipo de eliminação é que os usos de &dyn Fn()complicam o raciocínio para o compilador. Dado:

let g2: &dyn Fn() = /*expression */;

o compilador deve examinar meticulosamente o /*expression */para determinar se se g2origina de fou alguma outra função (ões) e as condições sob as quais essa proveniência se mantém. Em muitas circunstâncias, o compilador pode desistir: talvez o humano possa dizer que g2realmente vem de fem todas as situações, mas o caminho de fpara g2era muito complicado para o compilador decifrar, resultando em uma chamada virtual para g2com desempenho pessimista.

Isso se torna mais evidente quando tais objetos são entregues a funções genéricas (modelo):

fn h<F: Fn()>(f: F);

Se alguém chama h(f)where f: __UniqueFunc042, então hé especializado em uma instância única:

h::<__UniqueFunc042>(f);

Isso permite que o compilador gere código especializado para h, adaptado para o argumento específico de f, e o envio para fé muito provável que seja estático, se não embutido.

No cenário oposto, onde se chama h(f)com f2: &Fn(), o hé instanciado como

h::<&Fn()>(f);

que é compartilhado entre todas as funções do tipo &Fn(). De dentro h, o compilador sabe muito pouco sobre uma função opaca do tipo &Fn()e, portanto, só pode chamar de forma conservadora fcom um envio virtual. Para despachar estaticamente, o compilador teria que embutir a chamada h::<&Fn()>(f)em seu site de chamada, o que não é garantido se hfor muito complexo.


A primeira parte sobre a escolha de nomes perde o ponto: um tipo como void(*)(int, double)pode não ter um nome, mas posso anotá-lo. Eu o chamaria de tipo sem nome, não de tipo anônimo. E eu chamaria coisas enigmáticas como __namespace1_module1_func1_AnonymousFunction042mutilação de nomes, o que definitivamente não está no escopo desta questão. Esta pergunta é sobre tipos que são garantidos pelo padrão como impossíveis de escrever, em oposição à introdução de uma sintaxe de tipo que pode expressar esses tipos de uma maneira útil.
cmaster - restabelecer monica

3

Primeiro, lambda sem captura são conversíveis em um ponteiro de função. Portanto, eles fornecem alguma forma de genericidade.

Agora, por que lambdas com captura não são conversíveis em ponteiro? Como a função deve acessar o estado do lambda, esse estado precisaria aparecer como um argumento da função.


Bem, as capturas devem se tornar parte do próprio lambda, não? Assim como eles estão encapsulados em um std::function<>.
cmaster - restabelecer monica

3

Para evitar conflitos de nome com o código do usuário.

Mesmo dois lambdas com a mesma implementação terão tipos diferentes. O que não tem problema, porque também posso ter tipos diferentes de objetos, mesmo que o layout da memória seja igual.


Um tipo como int (*)(Foo*, int, double)não corre o risco de colisão do nome com o código do usuário.
cmaster - restabelecer monica em

Seu exemplo não generaliza muito bem. Embora uma expressão lambda seja apenas uma sintaxe, ela será avaliada como alguma estrutura, especialmente com a cláusula de captura. Nomear explicitamente pode levar a conflitos de nomes de estruturas já existentes.
knivil

Novamente, esta questão é sobre design de linguagem, não sobre C ++. Certamente posso definir uma linguagem em que o tipo de lambda é mais semelhante a um tipo de ponteiro de função do que a um tipo de estrutura de dados. A sintaxe de ponteiro de função em C ++ e a sintaxe de tipo de array dinâmico em C provam que isso é possível. E isso levanta a questão: por que lambdas não usaram uma abordagem semelhante?
cmaster - restabelecer monica em

1
Não, você não pode, por causa do currying variável (captura). Você precisa de uma função e de dados para fazê-lo funcionar.
Blindy

@Blindy Oh, sim, eu posso. Eu poderia definir um lambda como um objeto contendo dois ponteiros, um para o objeto de captura e outro para o código. Esse objeto lambda seria fácil de passar por valor. Ou eu poderia fazer truques com um esboço de código no início do objeto de captura que leva seu próprio endereço antes de pular para o código lambda real. Isso transformaria um ponteiro lambda em um único endereço. Mas isso é desnecessário, como a plataforma PPC provou: no PPC, um ponteiro de função é na verdade um par de ponteiros. É por isso que você não pode converter void(*)(void)para void*C / C ++ padrão e vice-versa.
cmaster - restabelecer monica
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.