La réponse acceptée par Cort Ammon est bonne, mais je pense qu'il y a un autre point important à faire sur l'implémentation.
Supposons que j'ai deux unités de traduction différentes, "one.cpp" et "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);
Les deux surcharges foo
utilisent le même identifiant ( foo
) mais ont des noms mutilés différents. (Dans l'ABI Itanium utilisé sur les systèmes POSIX-ish, les noms mutilés sont _Z3foo1A
et, dans ce cas particulier,. _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) {}
Le compilateur C ++ doit s'assurer que le nom mutilé de void foo(A1)
dans "two.cpp" est le même que le nom mutilé de extern void foo(A2)
"one.cpp", afin que nous puissions lier les deux fichiers objets ensemble. C'est la signification physique de deux types étant "du même type": il s'agit essentiellement de compatibilité ABI entre des fichiers objets compilés séparément.
Le compilateur C ++ n'est pas obligé de s'assurer que B1
et B2
sont «du même type». (En fait, il est nécessaire de s'assurer qu'ils sont de types différents; mais ce n'est pas aussi important pour le moment.)
Quel mécanisme physique le compilateur utilise-t-il pour s'assurer que A1
et A2
sont "du même type"?
Il fouille simplement dans les typedefs, puis examine le nom complet du type. C'est un type de classe nommé A
. (Eh bien, ::A
puisque c'est dans l'espace de noms global.) Donc c'est le même type dans les deux cas. C'est facile à comprendre. Plus important encore, il est facile à mettre en œuvre . Pour voir si deux types de classes sont du même type, prenez leurs noms et effectuez un strcmp
. Pour transformer un type de classe en nom mutilé d'une fonction, vous écrivez le nombre de caractères dans son nom, suivi de ces caractères.
Ainsi, les types nommés sont faciles à modifier.
Quel mécanisme physique le compilateur pourrait -il utiliser pour s'assurer que B1
et B2
sont «du même type», dans un monde hypothétique où C ++ exigeait qu'ils soient du même type?
Eh bien, il ne pouvait pas utiliser le nom du type, parce que le type n'a pas avoir un nom.
Peut-être qu'il pourrait en quelque sorte encoder le texte du corps du lambda. Mais ce serait un peu gênant, car en fait le b
dans "one.cpp" est subtilement différent du b
dans "two.cpp": "one.cpp" a x+1
et "two.cpp" a x + 1
. Nous devrions donc trouver une règle qui dit soit que cette différence d'espace n'a pas d' importance, soit qu'elle le fait (ce qui en fait des types différents après tout), ou que peut - être que c'est le cas (peut-être que la validité du programme est définie par l'implémentation , ou peut-être que c'est "mal formé aucun diagnostic requis"). En tous cas,A
Le moyen le plus simple de sortir de la difficulté est simplement de dire que chaque expression lambda produit des valeurs d'un type unique. Ensuite, deux types lambda définis dans des unités de traduction différentes ne sont certainement pas du même type . Dans une seule unité de traduction, nous pouvons "nommer" les types lambda en comptant simplement à partir du début du code source:
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; };
}
Bien sûr, ces noms n'ont de sens que dans cette unité de traduction. Ce TU $_0
est toujours d'un type différent de certains autres TU $_0
, même si ce TU struct A
est toujours du même type que certains autres TU struct A
.
Soit dit en passant, notez que notre idée «encoder le texte du lambda» avait un autre problème subtil: les lambdas $_2
et se $_3
composent exactement du même texte , mais ils ne devraient clairement pas être considérés comme du même type!
À propos, C ++ nécessite que le compilateur sache comment modifier le texte d'une expression C ++ arbitraire , comme dans
template<class T> void foo(decltype(T())) {}
template void foo<int>(int);
Mais C ++ n'exige pas (encore) que le compilateur sache comment modifier une instruction C ++ arbitraire . decltype([](){ ...arbitrary statements... })
est encore mal formé même en C ++ 20.
Notez également qu'il est facile de donner un alias local à un type sans nom en utilisant typedef
/ using
. J'ai le sentiment que votre question a peut-être surgi en essayant de faire quelque chose qui pourrait être résolu de cette manière.
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);
}
MODIFIE POUR AJOUTER: En lisant certains de vos commentaires sur d'autres réponses, il semble que vous vous demandez pourquoi
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)>);
C'est parce que les lambdas sans captures sont constructibles par défaut. (En C ++ uniquement à partir de C ++ 20, mais cela a toujours été conceptuellement vrai.)
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 vous avez essayé default_construct_and_call<decltype(&add1)>
, ce t
serait un pointeur de fonction initialisé par défaut et vous auriez probablement une erreur de segmentation. Ce n'est pas utile.