¿Por qué diseñar un lenguaje con tipos anónimos únicos?

90

Esto es algo que siempre me ha estado molestando como una característica de las expresiones lambda de C ++: el tipo de expresión lambda de C ++ es único y anónimo, simplemente no puedo escribirlo. Incluso si creo dos lambdas que son sintácticamente exactamente iguales, los tipos resultantes se definen para ser distintos. La consecuencia es que a) las lambdas solo se pueden pasar a las funciones de plantilla que permiten que el tiempo de compilación, el tipo inefable se pase junto con el objeto, yb) que las lambdas solo son útiles una vez que se borran el tipo mediante std::function<>.

Bien, pero así es como lo hace C ++, estaba listo para descartarlo como una característica molesta de ese lenguaje. Sin embargo, acabo de enterarme de que Rust aparentemente hace lo mismo: cada función o lambda de Rust tiene un tipo único y anónimo. Y ahora me pregunto: ¿Por qué?

Entonces, mi pregunta es la siguiente:
¿Cuál es la ventaja, desde el punto de vista del diseñador de idiomas, de introducir el concepto de un tipo único y anónimo en un idioma?

cmaster - reinstalar monica
fuente
6
como siempre, la mejor pregunta es por qué no.
Stargateur
31
"que las lambdas solo son útiles una vez que se borran el tipo mediante std :: function <>" - no, son directamente útiles sin std::function. Una lambda que se ha pasado a una función de plantilla se puede llamar directamente sin involucrar std::function. Luego, el compilador puede incorporar la lambda en la función de plantilla, lo que mejorará la eficiencia del tiempo de ejecución.
Erlkoenig
1
Supongo que facilita la implementación de lambda y hace que el lenguaje sea más fácil de entender. Si permitió que la misma expresión lambda se doblara en el mismo tipo, entonces necesitaría reglas especiales para manejar, { int i = 42; auto foo = [&i](){ return i; }; } { int i = 13; auto foo = [&i](){ return i; }; }ya que la variable a la que se refiere es diferente, aunque textualmente sean iguales. Si simplemente dice que todos son únicos, no tiene que preocuparse por tratar de averiguarlo.
NathanOliver
5
pero también puede darle un nombre a un tipo lambdas y hacer lo mismo con eso. lambdas_type = decltype( my_lambda);
idclev 463035818
3
Pero, ¿cuál debería ser un tipo de lambda genérico [](auto) {}? ¿Debería tener un tipo, para empezar?
Evg

Respuestas:

78

Muchos estándares (especialmente C ++) adoptan el enfoque de minimizar cuánto exigen a los compiladores. Francamente, ¡ya exigen suficiente! Si no tienen que especificar algo para que funcione, tienden a dejar la implementación definida.

Si las lambdas no fueran anónimas, tendríamos que definirlas. Esto tendría que decir mucho sobre cómo se capturan las variables. Considere el caso de una lambda [=](){...}. El tipo tendría que especificar qué tipos realmente fueron capturados por la lambda, lo que podría ser no trivial de determinar. Además, ¿qué pasa si el compilador optimiza con éxito una variable? Considerar:

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

Un compilador de optimización podría reconocer fácilmente que el único valor posible ique podría capturarse es 5 y reemplazarlo por auto f = []() { return 5; }. Sin embargo, si el tipo no es anónimo, esto podría cambiar el tipo o forzar al compilador a optimizar menos, almacenando iaunque en realidad no lo necesite. Esta es una bolsa completa de complejidad y matices que simplemente no es necesaria para lo que las lambdas debían hacer.

Y, en el caso de que realmente necesite un tipo no anónimo, siempre puede construir la clase de cierre usted mismo y trabajar con un functor en lugar de una función lambda. Por lo tanto, pueden hacer que lambdas maneje el caso del 99% y dejar que usted codifique su propia solución en el 1%.


El deduplicador señaló en los comentarios que no abordé la singularidad tanto como el anonimato. Estoy menos seguro de los beneficios de la singularidad, pero vale la pena señalar que el comportamiento de lo siguiente es claro si los tipos son únicos (la acción se instanciará dos veces).

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; });

Si los tipos no fueran únicos, tendríamos que especificar qué comportamiento debería ocurrir en este caso. Eso puede ser complicado. Algunas de las cuestiones que se plantearon sobre el tema del anonimato también levantan su fea cabeza en este caso por la singularidad.

Cort Ammon
fuente
Tenga en cuenta que no se trata realmente de ahorrar trabajo para un implementador del compilador, sino de ahorrar trabajo para el mantenedor de estándares. El compilador todavía tiene que responder a todas las preguntas anteriores para su implementación específica, pero no están especificadas en el estándar.
ComicSansMS
2
@ComicSansMS Unir estas cosas al implementar un compilador es mucho más fácil cuando no tienes que ajustar tu implementación al estándar de otra persona. Hablando por experiencia, a menudo es mucho más fácil para un mantenedor de estándares sobreespecificar la funcionalidad que tratar de encontrar la cantidad mínima para especificar mientras sigue obteniendo la funcionalidad deseada de su idioma. Como un excelente caso de estudio, observe cuánto trabajo dedicaron para evitar sobreespecificar memory_order_consume sin dejar de ser útil (en algunas arquitecturas)
Cort Ammon
1
Como todos los demás, usted defiende el anonimato de manera convincente . Pero, ¿es realmente tan buena idea obligarlo a ser único también?
Deduplicador
No es la complejidad del compilador lo que importa aquí, sino la complejidad del código generado. El punto no es simplificar el compilador, sino darle suficiente margen de maniobra para optimizar todos los casos y producir código natural para la plataforma de destino.
Jan Hudec
No puede capturar una variable estática.
Ruslan
70

Las lambdas no son solo funciones, son una función y un estado . Por lo tanto, tanto C ++ como Rust los implementan como un objeto con un operador de llamada ( operator()en C ++, los 3 Fn*rasgos en Rust).

Básicamente, [a] { return a + 1; }en C ++ se convierte en azúcar a algo como

struct __SomeName {
    int a;

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

luego usando una instancia de __SomeNamedonde se usa el lambda.

Mientras que en Rust, || a + 1en Rust desugar a 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 }
}

Esto significa que la mayoría de las lambdas deben tener diferentes tipos.

Ahora, hay algunas formas en que podríamos hacer eso:

  • Con tipos anónimos, que es lo que implementan ambos lenguajes. Otra consecuencia de eso es que todas las lambdas deben tener un tipo diferente. Pero para los diseñadores de lenguajes, esto tiene una clara ventaja: las lambdas se pueden describir simplemente utilizando otras partes más simples del lenguaje ya existentes. Son simplemente azúcar de sintaxis en torno a partes del lenguaje ya existentes.
  • Con alguna sintaxis especial para nombrar tipos de lambda: sin embargo, esto no es necesario ya que lambdas ya se pueden usar con plantillas en C ++ o con genéricos y los Fn*rasgos en Rust. Ninguno de los dos lenguajes te obliga a borrar lambdas para usarlos ( std::functionen C ++ o Box<Fn*>en Rust).

También tenga en cuenta que ambos lenguajes están de acuerdo en que las lambdas triviales que no capturan el contexto se pueden convertir en punteros de función.


Es bastante común describir características complejas de un idioma usando una característica más simple. Por ejemplo, tanto C ++ como Rust tienen ciclos de rango para, y ambos los describen como azúcar de sintaxis para otras características.

C ++ define

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

como 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

    }

} 

y Rust define

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

como 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 si bien parecen más complicados para un humano, son más simples para un diseñador de lenguaje o un compilador.

mcarton
fuente
15
@ cmaster-reinstatemonica Considere pasar un lambda como argumento de comparación para una función de clasificación. ¿Realmente le gustaría imponer una sobrecarga de llamadas a funciones virtuales aquí?
Daniel Langr
5
@ cmaster-reinstatemonica porque nada es virtual por defecto en C ++
Caleth
4
@cmaster: ¿te refieres a obligar a todos los usuarios de lambdas a pagar por dipatch dinámico, incluso cuando no lo necesiten?
StoryTeller - Unslander Monica
4
@ cmaster-reinstatemonica Lo mejor que obtendrá es la suscripción a virtual. Adivina qué, std::functionhace eso
Caleth
9
@ cmaster-reinstatemonica cualquier mecanismo en el que pueda volver a apuntar la función a llamar tendrá situaciones con sobrecarga de tiempo de ejecución. Esa no es la forma de C ++. Se std::function
inscribe
13

(Agregando a la respuesta de Caleth, pero demasiado largo para caber en un comentario).

La expresión lambda es simplemente azúcar sintáctica para una estructura anónima (un tipo de Voldemort, porque no puede decir su nombre).

Puede ver la similitud entre una estructura anónima y el anonimato de una lambda en este fragmento 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";
}

Si eso sigue siendo insatisfactorio para una lambda, tampoco debería serlo para una estructura anónima.

Algunos lenguajes permiten un tipo de escritura pato que es un poco más flexible, y aunque C ++ tiene plantillas que realmente no ayudan a crear un objeto a partir de una plantilla que tiene un campo miembro que puede reemplazar una lambda directamente en lugar de usar una std::functionenvoltura.

Eljay
fuente
3
Gracias, eso sí arroja un poco de luz sobre el razonamiento detrás de la forma en que se definen las lambdas en C ++ (tengo que recordar el término "tipo Voldemort" :-)). Sin embargo, la pregunta sigue siendo: ¿Cuál es la ventaja de esto a los ojos de un diseñador de idiomas?
cmaster - reinstalar a monica el
1
Incluso podría agregar int& operator()(){ return x; }a esas estructuras
Caleth
2
@ cmaster-reinstatemonica • Especulativamente ... el resto de C ++ se comporta de esa manera. Hacer que las lambdas usen algún tipo de escritura de pato con "forma de superficie" sería algo muy diferente al resto del lenguaje. Agregar ese tipo de facilidad en el lenguaje para lambdas probablemente se consideraría generalizado para todo el lenguaje, y eso sería un cambio potencialmente enorme. Omitir tal facilidad solo para lambdas encaja con la tipificación fuertemente-ish del resto de C ++.
Eljay
Técnicamente, un tipo de Voldemort sería auto foo(){ struct DarkLord {} tom_riddle; return tom_riddle; }, porque fuera de la foonada se puede usar el identificadorDarkLord
Caleth
@ cmaster-reinstatemonica eficiencia, la alternativa sería encuadrar y enviar dinámicamente cada lambda (asignarlo en el montón y borrar su tipo preciso). Ahora, como observa, el compilador podría deduplicar los tipos anónimos de lambdas, pero aún así no podría escribirlos y requeriría un trabajo significativo para una ganancia muy pequeña, por lo que las probabilidades no están realmente a favor.
Masklinn
10

¿Por qué diseñar un lenguaje con tipos anónimos únicos ?

Porque hay casos en los que los nombres son irrelevantes y no son útiles o incluso contraproducentes. En este caso, la capacidad de abstraer su existencia es útil porque reduce la contaminación de nombres y resuelve uno de los dos problemas difíciles de la informática (cómo nombrar las cosas). Por la misma razón, los objetos temporales son útiles.

lambda

La unicidad no es una cosa lambda especial, ni siquiera una cosa especial para tipos anónimos. También se aplica a tipos con nombre en el idioma. Considere lo siguiente:

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

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

void foo(A);

Tenga en cuenta que no puede pasar Ben foo, a pesar de que las clases son idénticos. Esta misma propiedad se aplica a los tipos sin nombre.

lambdas solo se pueden pasar a las funciones de plantilla que permiten que el tiempo de compilación, el tipo inefable se pase junto con el objeto ... borrado a través de std :: function <>.

Hay una tercera opción para un subconjunto de lambdas: las lambdas que no capturan se pueden convertir en punteros de función.


Tenga en cuenta que si las limitaciones de un tipo anónimo son un problema para un caso de uso, entonces la solución es simple: se puede usar un tipo con nombre en su lugar. Las lambdas no hacen nada que no se pueda hacer con una clase con nombre.

eerorika
fuente
10

La respuesta aceptada de Cort Ammon es buena, pero creo que hay un punto más importante que hacer sobre la implementabilidad.

Supongamos que tengo dos unidades de traducción diferentes, "one.cpp" y "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);

Las dos sobrecargas de foousan el mismo identificador ( foo) pero tienen diferentes nombres mutilados. (En el Itanium ABI utilizado en sistemas POSIX-ish, los nombres alterados son _Z3foo1Ay, en este 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) {}

El compilador de C ++ debe asegurarse de que el nombre mutilado de void foo(A1)en "two.cpp" sea el mismo que el nombre mutilado de extern void foo(A2)en "one.cpp", de modo que podamos vincular los dos archivos objeto juntos. Este es el significado físico de dos tipos que son "el mismo tipo": se trata esencialmente de compatibilidad ABI entre archivos de objeto compilados por separado.

El compilador de C ++ no es necesario para garantizar que B1y B2sean "del mismo tipo". (De hecho, es necesario asegurarse de que sean de diferentes tipos, pero eso no es tan importante en este momento).


¿Qué mecanismo físico utiliza el compilador para asegurarse de que A1y A2son "del mismo tipo"?

Simplemente busca en typedefs y luego mira el nombre completo del tipo. Es un tipo de clase llamado A. (Bueno, ::Aya que está en el espacio de nombres global). Así que es del mismo tipo en ambos casos. Eso es fácil de entender. Más importante aún, es fácil de implementar . Para ver si dos tipos de clases son del mismo tipo, toma sus nombres y haz un strcmp. Para convertir un tipo de clase en el nombre mutilado de una función, escribe el número de caracteres en su nombre, seguido de esos caracteres.

Por tanto, los tipos con nombre son fáciles de modificar.

¿Qué mecanismo físico podría usar el compilador para asegurarse de que B1y B2son "del mismo tipo", en un mundo hipotético donde C ++ requiere que sean del mismo tipo?

Bueno, no podría usar el nombre del tipo, porque el tipo no tiene nombre.

Quizás de alguna manera podría codificar el texto del cuerpo de la lambda. Pero eso sería un poco incómodo, porque en realidad ben "one.cpp" es sutilmente diferente de ben "two.cpp": "one.cpp" tiene x+1y "two.cpp" tiene x + 1. Entonces tendríamos que idear una regla que diga que esta diferencia de espacios en blanco no importa, o que (lo que los convierte en tipos diferentes después de todo), o que tal vez sí (tal vez la validez del programa esté definida por la implementación , o tal vez está "mal formado, no se requiere diagnóstico"). De todas formas,A

La forma más fácil de salir de la dificultad es simplemente decir que cada expresión lambda produce valores de un tipo único. Entonces, dos tipos lambda definidos en diferentes unidades de traducción definitivamente no son del mismo tipo . Dentro de una sola unidad de traducción, podemos "nombrar" los tipos lambda contando desde el principio del código fuente:

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
} 

Por supuesto, estos nombres solo tienen significado dentro de esta unidad de traducción. Esta TU $_0es siempre un tipo diferente de otras TU $_0, aunque esta TU struct Aes siempre del mismo tipo que algunas otras TU struct A.

Por cierto, observe que nuestra idea de "codificar el texto de la lambda" tenía otro problema sutil: las lambdas $_2y $_3constan exactamente del mismo texto , ¡pero claramente no deberían considerarse del mismo tipo!


Por cierto, C ++ requiere que el compilador sepa cómo manipular el texto de una expresión C ++ arbitraria , como en

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

Pero C ++ no requiere (todavía) que el compilador sepa cómo manipular una instrucción C ++ arbitraria . decltype([](){ ...arbitrary statements... })todavía está mal formado incluso en C ++ 20.


También observe que es fácil dar un alias local a un tipo sin nombre usando typedef/ using. Tengo la sensación de que su pregunta podría haber surgido al intentar hacer algo que podría resolverse de esta manera.

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 AGREGAR: Al leer algunos de sus comentarios sobre otras respuestas, parece que se pregunta 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)>);

Eso es porque las lambdas sin captura son construibles por defecto. (En C ++ solo a partir de C ++ 20, pero siempre ha sido conceptualmente cierto).

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);

Si lo intentara default_construct_and_call<decltype(&add1)>, tsería un puntero de función inicializado por defecto y probablemente segfault. Eso es, como, no útil.

Quuxplusone
fuente
" De hecho, es necesario asegurarse de que sean tipos diferentes; pero eso no es tan importante en este momento " . Me pregunto si hay una buena razón para forzar la unicidad si se define de manera equivalente.
Deduplicador
Personalmente, creo que el comportamiento completamente definido es (¿casi?) Siempre mejor que el comportamiento no especificado. "¿Son estos dos punteros de función iguales? Bueno, solo si estas dos instancias de plantilla son la misma función, lo cual es cierto solo si estos dos tipos lambda son del mismo tipo, lo cual es cierto solo si el compilador decidió fusionarlos". ¡Icky! (Pero observe que tenemos una situación exactamente análoga con la fusión de cadenas literales, y nadie está perturbado por esa situación. Por lo tanto, dudo que sea catastrófico permitir que el compilador
combine
Bueno, si dos funciones equivalentes (excepto como si) pueden ser idénticas también es una buena pregunta. El lenguaje en el estándar no es muy obvio para funciones libres y / o estáticas. Pero eso está fuera del alcance aquí.
Deduplicador
Por casualidad, este mismo mes se ha debatido en la lista de correo LLVM sobre la combinación de funciones. El codegen de Clang hará que funciones con cuerpos completamente vacíos se fusionen casi "por accidente": godbolt.org/z/obT55b Esto es técnicamente no conforme, y creo que probablemente parchearán LLVM para dejar de hacer esto. Pero sí, de acuerdo, fusionar direcciones de funciones también es una cosa.
Quuxplusone
Ese ejemplo tiene otros problemas, a saber, falta la declaración de devolución. ¿No es que ellos solos ya hacen que el código sea no conforme? Además, buscaré la discusión, pero ¿mostraron o asumieron que la fusión de funciones equivalentes no se ajusta al estándar, su comportamiento documentado, a gcc, o simplemente que algunos confían en que no suceda?
Deduplicador
9

Las lambdas de C ++ necesitan tipos distintos para operaciones distintas, ya que C ++ se enlaza estáticamente. Solo se pueden copiar / mover, por lo que la mayoría de las veces no necesita nombrar su tipo. Pero eso es todo un detalle de implementación.

No estoy seguro de si las lambdas de C # tienen un tipo, ya que son "expresiones de función anónimas", y se convierten inmediatamente en un tipo de delegado compatible o un tipo de árbol de expresión. Si es así, probablemente sea un tipo no pronunciable.

C ++ también tiene estructuras anónimas, donde cada definición conduce a un tipo único. Aquí el nombre no es impronunciable, simplemente no existe en lo que respecta al estándar.

C # tiene tipos de datos anónimos , que prohíbe cuidadosamente que escapen del ámbito en el que están definidos. La implementación también les da un nombre único e impronunciable.

Tener un tipo anónimo le indica al programador que no debe hurgar dentro de su implementación.

Aparte:

Usted puede dar un nombre a un tipo de lambda.

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

Si no tiene ninguna captura, puede usar un tipo de puntero de función

void (*pfoo)() = foo;
Caleth
fuente
1
El primer código de ejemplo todavía no permitirá un subsiguiente Foo_t = []{};, solo Foo_t = fooy nada más.
cmaster - reinstalar a monica el
1
@ cmaster-reinstatemonica eso se debe a que el tipo no se puede construir por defecto, no por el anonimato. Supongo que eso tiene tanto que ver con evitar tener un conjunto aún mayor de casos de esquina que debe recordar, como cualquier razón técnica.
Caleth
6

¿Por qué utilizar tipos anónimos?

Para los tipos que son generados automáticamente por el compilador, la opción es (1) cumplir con la solicitud de un usuario para el nombre del tipo, o (2) dejar que el compilador elija uno por su cuenta.

  1. En el primer caso, se espera que el usuario proporcione explícitamente un nombre cada vez que aparezca una construcción de este tipo (C ++ / Rust: siempre que se defina una lambda; Rust: siempre que se defina una función). Este es un detalle tedioso que el usuario debe proporcionar cada vez y, en la mayoría de los casos, nunca se vuelve a mencionar el nombre. Por lo tanto, tiene sentido dejar que el compilador descubra un nombre para él automáticamente y usar características existentes como decltypeinferencia de tipo o para hacer referencia al tipo en los pocos lugares donde se necesita.

  2. En el último caso, el compilador debe elegir un nombre único para el tipo, que probablemente sería un nombre oscuro e ilegible como __namespace1_module1_func1_AnonymousFunction042. El diseñador del lenguaje podría especificar con precisión cómo se construye este nombre con un detalle glorioso y delicado, pero esto expone innecesariamente al usuario un detalle de implementación en el que ningún usuario sensato podría confiar, ya que el nombre es sin duda frágil frente a refactores menores. Esto también restringe innecesariamente la evolución del lenguaje: futuras adiciones de características pueden hacer que el algoritmo de generación de nombres existente cambie, lo que lleva a problemas de compatibilidad con versiones anteriores. Por lo tanto, tiene sentido simplemente omitir este detalle y afirmar que el usuario no puede pronunciar el tipo generado automáticamente.

¿Por qué utilizar tipos únicos (distintos)?

Si un valor tiene un tipo único, entonces un compilador de optimización puede rastrear un tipo único en todos sus sitios de uso con fidelidad garantizada. Como corolario, el usuario puede estar seguro de los lugares donde el compilador conoce completamente la procedencia de este valor particular.

Como ejemplo, el momento en que el compilador ve:

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

/* ... intervening code */

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

el compilador tiene plena confianza en que gnecesariamente debe tener su origen f, sin siquiera saber la procedencia de g. Esto permitiría gdesvirtualizar la llamada . El usuario también lo sabría, ya que ha tenido mucho cuidado de preservar el tipo único de a ftravés del flujo de datos que condujo a g.

Necesariamente, esto limita lo que puede hacer el usuario f. El usuario no tiene la libertad de escribir:

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

ya que eso conduciría a la unificación (ilegal) de dos tipos distintos.

Para solucionar este problema, el usuario podría convertir el __UniqueFunc042al tipo no único &dyn Fn(),

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

La compensación hecha por este tipo de borrado es que los usos de &dyn Fn()complican el razonamiento del compilador. Dado:

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

el compilador tiene que examinar minuciosamente el /*expression */para determinar si se g2origina en fo alguna otra función (es), y las condiciones bajo las cuales se cumple esa procedencia. En muchas circunstancias, el compilador puede darse por vencido: quizás el ser humano podría decir que g2realmente proviene de fen todas las situaciones, pero la ruta de fa g2fue demasiado complicada para que el compilador la descifre, lo que resultó en una llamada virtual a g2con un rendimiento pesimista.

Esto se vuelve más evidente cuando dichos objetos se entregan a funciones genéricas (plantilla):

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

Si uno llama a h(f)where f: __UniqueFunc042, entonces hestá especializado en una instancia única:

h::<__UniqueFunc042>(f);

Esto permite al compilador generar código especializado para h, adaptado al argumento particular de f, y fes muy probable que el envío a sea estático, si no en línea.

En el escenario opuesto, donde se llama h(f)con f2: &Fn(), hse instancia como

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

que se comparte entre todas las funciones de tipo &Fn(). Desde dentro h, el compilador sabe muy poco acerca de una función opaca de tipo &Fn()y, por lo tanto, solo podría llamar de manera conservadora fcon un envío virtual. Para distribuir estáticamente, el compilador tendría que insertar la llamada h::<&Fn()>(f)en su sitio de llamada, lo que no está garantizado si hes demasiado complejo.

Rufflewind
fuente
La primera parte sobre la elección de nombres no es el punto: un tipo como void(*)(int, double)puede no tener un nombre, pero puedo escribirlo. Yo lo llamaría un tipo sin nombre, no un tipo anónimo. Y yo llamaría cosas crípticas como __namespace1_module1_func1_AnonymousFunction042alteración de nombres, que definitivamente no está dentro del alcance de esta pregunta. Esta pregunta trata sobre los tipos que están garantizados por el estándar como imposibles de escribir, en lugar de introducir una sintaxis de tipos que pueda expresar estos tipos de manera útil.
cmaster - reinstalar a monica
3

Primero, lambda sin captura se puede convertir en un puntero de función. Así que proporcionan alguna forma de genérico.

Ahora bien, ¿por qué las lambdas con captura no se pueden convertir en puntero? Debido a que la función debe acceder al estado de la lambda, este estado debería aparecer como un argumento de función.

Oliv
fuente
Bueno, las capturas deberían pasar a formar parte de la propia lambda, ¿no? Al igual que están encapsulados dentro de un std::function<>.
cmaster - reinstalar a monica
3

Para evitar colisiones de nombres con el código de usuario.

Incluso dos lambdas con la misma implementación tendrán tipos diferentes. Lo cual está bien porque también puedo tener diferentes tipos de objetos, incluso si su diseño de memoria es igual.

knivil
fuente
Un tipo como int (*)(Foo*, int, double)no corre ningún riesgo de colisión de nombres con el código de usuario.
cmaster - reinstalar a monica
Tu ejemplo no se generaliza muy bien. Si bien una expresión lambda es solo sintaxis, se evaluará en alguna estructura, especialmente con la cláusula de captura. Nombrarlo explícitamente podría provocar conflictos de nombres de estructuras ya existentes.
knivil
Nuevamente, esta pregunta es sobre diseño de lenguaje, no sobre C ++. Seguramente puedo definir un lenguaje donde el tipo de lambda es más parecido a un tipo de puntero de función que a un tipo de estructura de datos. La sintaxis del puntero de función en C ++ y la sintaxis del tipo de matriz dinámica en C demuestran que esto es posible. Y eso plantea la pregunta, ¿por qué las lambdas no utilizaron un enfoque similar?
cmaster - reinstalar a monica
1
No, no puede, debido al curry variable (captura). Necesita una función y datos para que funcione.
Blindy
@Blindy Oh, sí, puedo. Podría definir una lambda como un objeto que contiene dos punteros, uno para el objeto de captura y otro para el código. Un objeto lambda de este tipo sería fácil de pasar por valor. O podría hacer trucos con un código auxiliar al comienzo del objeto de captura que toma su propia dirección antes de saltar al código lambda real. Eso convertiría un puntero lambda en una sola dirección. Pero eso es innecesario, como ha demostrado la plataforma PPC: en PPC, un puntero de función es en realidad un par de punteros. Es por eso que no se puede convertir void(*)(void)hacia void*y hacia atrás en C / C ++ estándar.
cmaster - reinstalar a monica