Прийнята відповідь Корта Аммона хороша, але я думаю, що є ще один важливий момент щодо реалізації.
Припустимо, у мене є дві різні одиниці перекладу, "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
), але мають різні спотворені імена. (В італійському 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) {}
Компілятор С ++ повинен переконатися, що ім'я зі спотворенням void foo(A1)
у "two.cpp" збігається з іменем extern void foo(A2)
у "one.cpp", щоб ми могли пов'язати два об'єктні файли разом. Це фізичне значення двох типів, які є "однаковими": це, по суті, ABI-сумісність між окремо скомпільованими об'єктними файлами.
Компілятор C ++ це НЕ потрібно , щоб гарантувати , що B1
і B2
є «тим же типом.» (Насправді потрібно переконатись, що вони різного типу; але це не так важливо зараз).
Який фізичний механізм робить використання компілятора для того , щоб A1
і A2
в «той же самий тип»?
Він просто прокопується через typedefs, а потім переглядає повну назву типу. Це тип класу з іменем A
. (Ну, ::A
оскільки це в глобальному просторі імен.) Отже, це однаковий тип в обох випадках. Це легко зрозуміти. Що ще важливіше, це легко здійснити . Щоб побачити, чи однакові два типи класів, ви берете їх імена та робите astrcmp
. Щоб перетворити тип класу на спотворене ім'я функції, ви вводите кількість символів у його назві, за якими слідують ці символи.
Отже, названі типи легко зруйнувати.
Який фізичний механізм може використання компілятора для того , щоб B1
і B2
є «тим же типом," в гіпотетичному світі , де C ++ вимагає від них бути тим же типом?
Ну, він не міг використовувати назву типу, оскільки тип не має імені.
Можливо, це могло б якось закодувати текст тіла лямбди. Але це було б якось незручно, оскільки насправді b
in "one.cpp" тонко відрізняється від b
"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
складаються з абсолютно однакового тексту , але їх явно не слід вважати однотипними !
До речі, С ++ вимагає, щоб компілятор знав, як маніпулювати текстом довільного виразу С ++ , як у
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)>);
Це тому, що беззахоплені лямбди можна конструювати за замовчуванням. (У С ++ лише станом на С ++ 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. Це, начебто, не корисно.