Принятый ответ Корта Аммона хорош, но я думаю, что есть еще один важный момент, касающийся реализуемости.
Предположим, у меня есть две разные единицы перевода: 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 ++ требует, чтобы они были одного типа?
Ну, он не мог использовать имя типа, так как тип не имеет имени.
Возможно, он мог как-то закодировать текст тела лямбды. Но это было бы немного неудобно, потому что на самом деле bin "one.cpp" немного отличается от bin "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. Это вроде бесполезно.