Принятый ответ Корта Аммона хорош, но я думаю, что есть еще один важный момент, касающийся реализуемости.
Предположим, у меня есть две разные единицы перевода: one.cpp и 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);
Две перегрузки foo
используют один и тот же идентификатор ( foo
), но имеют разные искаженные имена. (В Itanium ABI, используемом в системах типа POSIX, искаженные имена - _Z3foo1A
и, в данном конкретном случае,. _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) {}
Компилятор C ++ должен гарантировать, что искаженное имя void foo(A1)
в "two.cpp" совпадает с искаженным именем extern void foo(A2)
в "one.cpp", чтобы мы могли связать два объектных файла вместе. Это физический смысл того, что два типа являются «одним и тем же типом»: по сути, речь идет о совместимости с ABI между отдельно скомпилированными объектными файлами.
Компилятор C ++ не обязан гарантировать, что B1
и B2
являются «одного типа». (Фактически, необходимо убедиться, что это разные типы, но сейчас это не так важно.)
Какой физический механизм использует компилятор, чтобы гарантировать, что A1
и A2
являются «одного типа»?
Он просто копается в определениях типов, а затем смотрит на полное имя типа. Это тип класса с именем A
. (Ну, ::A
поскольку он находится в глобальном пространстве имен.) Таким образом, это один и тот же тип в обоих случаях. Это легко понять. Что еще более важно, это легко реализовать . Чтобы увидеть, являются ли два типа классов одним и тем же типом, вы берете их имена и выполняетеstrcmp
. Чтобы преобразовать тип класса в искаженное имя функции, вы пишете количество символов в его имени, за которым следуют эти символы.
Итак, именованные типы легко подделать.
Какой физический механизм мог бы использовать компилятор, чтобы гарантировать, что B1
и B2
являются «одного типа» в гипотетическом мире, где C ++ требует, чтобы они были одного типа?
Ну, он не мог использовать имя типа, так как тип не имеет имени.
Возможно, он мог как-то закодировать текст тела лямбды. Но это было бы немного неудобно, потому что на самом деле b
in "one.cpp" немного отличается от b
in "two.cpp": "one.cpp" имеет, x+1
а "two.cpp" имеет x + 1
. Таким образом, мы должны были бы придумать правило, которое гласит, что либо эта разница в пробелах не имеет значения, либо она имеет значение (в конце концов, делая их разными типами), либо, возможно, имеет значение (возможно, действительность программы определяется реализацией , или, может быть, это «плохо сформировано, диагностика не требуется»). Тем не мение,A
Самый простой выход из затруднения - просто сказать, что каждое лямбда-выражение производит значения уникального типа. Тогда два лямбда-типа, определенные в разных единицах перевода, определенно не являются одним и тем же типом . В рамках одной единицы трансляции мы можем «давать имена» лямбда-типам, просто считая от начала исходного кода:
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; };
}
Конечно, эти имена имеют значение только в пределах этой единицы перевода. Этот TU $_0
всегда отличается от некоторых других TU $_0
, даже если этот TU struct A
всегда того же типа, что и некоторые другие TU struct A
.
Кстати, обратите внимание , что наш «кодировать текст лямбды» идея было еще одна тонкой проблемы: лямбды $_2
и $_3
состоит из точно такого же текста , но они не должны четко рассматриваться тем же типа!
Между прочим, C ++ требует, чтобы компилятор знал, как искажать текст произвольного выражения C ++ , как в
template<class T> void foo(decltype(T())) {}
template void foo<int>(int);
Но C ++ (пока) не требует, чтобы компилятор знал, как искажать произвольный оператор C ++ . decltype([](){ ...arbitrary statements... })
все еще плохо сформирован даже в C ++ 20.
Также обратите внимание, что безымянному типу легко присвоить локальный псевдоним с помощью typedef
/ using
. У меня такое чувство, что ваш вопрос мог возникнуть из-за попытки сделать что-то, что можно было бы решить подобным образом.
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);
}
ИЗМЕНИТЬ ДОБАВИТЬ: Читая некоторые из ваших комментариев к другим ответам, похоже, вам интересно, почему
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 ++ только с C ++ 20, но концептуально это всегда было верно.)
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);
Если бы вы попробовали default_construct_and_call<decltype(&add1)>
, t
это будет указатель на функцию, инициализированный по умолчанию, и вы, вероятно, будете segfault. Это вроде бесполезно.