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".
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 foo
usan el mismo identificador ( foo
) pero tienen diferentes nombres mutilados. (En el Itanium ABI utilizado en sistemas POSIX-ish, los nombres alterados son _Z3foo1A
y, en este caso particular,. _Z3fooN1bMUliE_E
)
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 B1
y B2
sean "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 A1
y A2
son "del mismo tipo"?
Simplemente busca en typedefs y luego mira el nombre completo del tipo. Es un tipo de clase llamado A
. (Bueno, ::A
ya 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 B1
y B2
son "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 b
en "one.cpp" es sutilmente diferente de b
en "two.cpp": "one.cpp" tiene x+1
y "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 sí (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 = [](){};
auto b = [](){};
auto f(int x) {
return [x](int y) { return x+y; };
}
auto g(float x) {
return [x](int y) { return x+y; };
}
Por supuesto, estos nombres solo tienen significado dentro de esta unidad de traducción. Esta TU $_0
es siempre un tipo diferente de otras TU $_0
, aunque esta TU struct A
es 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 $_2
y $_3
constan 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);
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; };
}
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)>
, t
sería un puntero de función inicializado por defecto y probablemente segfault. Eso es, como, no útil.