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".
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 foo
usam o mesmo identificador ( foo
), mas têm nomes mutilados diferentes. (No Itanium ABI usado em sistemas POSIX-ish, os nomes mutilados são _Z3foo1A
e, neste 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) {}
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 B1
e B2
sã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 A1
e A2
sejam "do mesmo tipo"?
Ele simplesmente explora typedefs e, em seguida, examina o nome totalmente qualificado do tipo. É um tipo de classe chamado A
. (Bem, ::A
já 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 B1
e B2
sejam "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 b
em "one.cpp" é sutilmente diferente do b
em "two.cpp": "one.cpp" tem x+1
e "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 = [](){};
auto b = [](){};
auto f(int x) {
return [x](int y) { return x+y; };
}
auto g(float x) {
return [x](int y) { return x+y; };
}
É 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 A
seja 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 $_2
e $_3
consistem 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);
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; };
}
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)>
, t
seria um ponteiro de função inicializado por padrão e provavelmente causaria um segfault. Isso não é útil.