Zaakceptowana odpowiedź Corta Ammona jest dobra, ale myślę, że jest jeszcze jedna ważna kwestia, którą należy poruszyć w kwestii implementacji.
Załóżmy, że mam dwie różne jednostki tłumaczeniowe, „one.cpp” i „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);
Dwa przeciążenia foo
używają tego samego identyfikatora ( foo
), ale mają różne zniekształcone nazwy. (W Itanium ABI używanym w systemach POSIX-ish, zniekształcone nazwy to _Z3foo1A
iw tym konkretnym przypadku,. _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) {}
Kompilator C ++ musi zapewnić, że zniekształcona nazwa void foo(A1)
w „two.cpp” jest taka sama jak zniekształcona nazwa extern void foo(A2)
w „one.cpp”, abyśmy mogli połączyć ze sobą dwa pliki obiektowe. To jest znaczenie fizyczne dwóch typów „tego samego typu”: zasadniczo chodzi o zgodność ABI między oddzielnie skompilowanymi plikami obiektowymi.
Kompilator C ++ jest nie zobowiązani do zapewnienia, B1
i B2
są „tego samego typu.” (W rzeczywistości jest to wymagane, aby upewnić się, że są to różne typy, ale nie jest to teraz tak ważne).
Jakiego mechanizmu fizycznego używa kompilator, aby to zapewnić A1
iA2
to „ten sam typ”?
Po prostu zagłębia się w typedef, a następnie sprawdza w pełni kwalifikowaną nazwę typu. To nazwa typu klasy A
. (Dobrze,::A
ponieważ znajduje się w globalnej przestrzeni nazw.) Więc jest tego samego typu w obu przypadkach. Łatwo to zrozumieć. Co ważniejsze, jest łatwy do wdrożenia . Aby sprawdzić, czy dwa typy klas są tego samego typu, bierzemy ich nazwy i robimystrcmp
. Aby zmienić typ klasy w zniekształconą nazwę funkcji, należy wpisać liczbę znaków w jej nazwie, a następnie te znaki.
Tak więc nazwane typy są łatwe do modyfikowania.
Jakiego mechanizmu fizycznego może użyć kompilator, aby to zapewnićB1
i B2
to „ten sam typ” w hipotetycznym świecie, gdzie wymagane je C ++, aby być tego samego typu?
Cóż, nie mógł użyć nazwy typu, ponieważ typ nie ma nazwy.
Może mógłby jakoś zakodować tekst ciała lambdy. Ale to byłoby trochę niezręczne, ponieważ w rzeczywistości b
„one.cpp” jest subtelnie różne od b
„two.cpp”: „one.cpp” ma x+1
i „two.cpp” ma x + 1
. Musielibyśmy więc wymyślić regułę, która mówi albo że ta różnica białych znaków nie ma znaczenia, albo że ma (w końcu czyniąc je różnymi typami), albo że może tak (być może ważność programu jest zdefiniowana przez implementację , a może jest to „źle uformowane, nie wymaga diagnostyki”). Tak czy inaczej,A
Najłatwiejszym wyjściem z tej trudności jest po prostu stwierdzenie, że każde wyrażenie lambda daje wartości unikalnego typu. Zatem dwa typy lambda zdefiniowane w różnych jednostkach tłumaczeniowych na pewno nie są tego samego typu . W ramach jednej jednostki tłumaczeniowej możemy „nazwać” typy lambda, licząc od początku kodu źródłowego:
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; };
}
Oczywiście nazwy te mają znaczenie tylko w ramach tej jednostki tłumaczeniowej. Ta jednostka JC $_0
jest zawsze innego typu niż niektóre inne JT $_0
, nawet jeśli ta JC struct A
jest zawsze tego samego typu, co inne JC struct A
.
Nawiasem mówiąc, zwróć uwagę, że nasz pomysł „zakoduj tekst lambda” miał inny subtelny problem: lambdy $_2
i $_3
składają się z dokładnie tego samego tekstu , ale wyraźnie nie powinny być uważane za tego samego typu!
Nawiasem mówiąc, C ++ wymaga, aby kompilator wiedział, jak zmienić tekst dowolnego wyrażenia C ++ , jak w
template<class T> void foo(decltype(T())) {}
template void foo<int>(int);
Ale C ++ nie wymaga (jeszcze), aby kompilator wiedział, jak modyfikować dowolną instrukcję C ++ . decltype([](){ ...arbitrary statements... })
jest nadal źle sformułowany, nawet w C ++ 20.
Zauważ również, że łatwo jest nadać lokalny alias nienazwanemu typowi za pomocą typedef
/ using
. Mam wrażenie, że Twoje pytanie mogło powstać w wyniku próby zrobienia czegoś, co można by rozwiązać w ten sposób.
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);
}
ZMIENIONO DO DODANIA: Po przeczytaniu niektórych komentarzy na temat innych odpowiedzi wydaje się, że zastanawiasz się, dlaczego
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)>);
Dzieje się tak, ponieważ lambdy bez przechwytywania są domyślnie konstruowane. (W C ++ tylko od C ++ 20, ale koncepcyjnie zawsze było to prawdziwe).
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);
Gdybyś spróbował default_construct_and_call<decltype(&add1)>
, t
byłby domyślnie zainicjowanym wskaźnikiem funkcji i prawdopodobnie doszedłbyś do segfault. To jakby nieprzydatne.