La risposta accettata da Cort Ammon è buona, ma penso che ci sia un altro punto importante da sottolineare sull'implementazione.
Supponiamo che io abbia due diverse unità di traduzione, "one.cpp" e "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);
I due overload di foo
utilizzano lo stesso identificatore ( foo
) ma hanno nomi alterati diversi. (Nell'ABI Itanium utilizzato sui sistemi POSIX, i nomi alterati sono _Z3foo1A
e, in questo caso particolare,. _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) {}
Il compilatore C ++ deve assicurarsi che il nome void foo(A1)
alterato di "two.cpp" sia lo stesso del nome extern void foo(A2)
alterato di "one.cpp", in modo che possiamo collegare insieme i due file oggetto. Questo è il significato fisico di due tipi che sono "lo stesso tipo": si tratta essenzialmente di compatibilità ABI tra file oggetto compilati separatamente.
Il compilatore C ++ non è necessario per garantire che B1
e B2
siano "dello stesso tipo". (In effetti, è necessario assicurarsi che siano di tipi diversi; ma non è così importante in questo momento.)
Quale meccanismo fisico utilizza il compilatore per garantire che A1
e A2
siano "dello stesso tipo"?
Si insinua semplicemente attraverso i typedef e quindi esamina il nome completo del tipo. È un tipo di classe denominato A
. (Bene, ::A
dato che è nello spazio dei nomi globale.) Quindi è dello stesso tipo in entrambi i casi. È facile da capire. Ancora più importante, è facile da implementare . Per vedere se due tipi di classe sono dello stesso tipo, prendi i loro nomi e fai un strcmp
. Per manipolare un tipo di classe nel nome alterato di una funzione, scrivi il numero di caratteri nel suo nome, seguito da quei caratteri.
Quindi, i tipi con nome sono facili da manipolare.
Quale meccanismo fisico potrebbe utilizzare il compilatore per garantire che B1
e B2
siano "dello stesso tipo", in un mondo ipotetico in cui C ++ richiede che siano dello stesso tipo?
Bene, non potrebbe usare il nome del tipo, perché il tipo non ha un nome.
Forse potrebbe in qualche modo codificare il testo del corpo del lambda. Ma sarebbe un po 'imbarazzante, perché in realtà b
"one.cpp" è leggermente diverso da b
"two.cpp": "one.cpp" ha x+1
e "two.cpp" ha x + 1
. Così avremmo dovuto trovare una regola che dice che questa differenza sia gli spazi non lo fa materia, o che fa (che li rende diversi tipi dopo tutto), o che forse lo fa (forse la validità del programma è definito dall'implementazione , o forse è "mal formato nessuna diagnosi richiesta"). Comunque,A
Il modo più semplice per uscire dalla difficoltà è semplicemente dire che ogni espressione lambda produce valori di un tipo univoco. Quindi due tipi lambda definiti in unità di traduzione diverse non sono sicuramente lo stesso tipo . All'interno di una singola unità di traduzione, possiamo "nominare" i tipi lambda semplicemente contando dall'inizio del codice sorgente:
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; };
}
Ovviamente questi nomi hanno un significato solo all'interno di questa unità di traduzione. Questa TU $_0
è sempre di un tipo diverso da altre TU $_0
, anche se questa TU struct A
è sempre lo stesso tipo di altre TU struct A
.
A proposito, nota che la nostra idea di "codificare il testo del lambda" aveva un altro sottile problema: i lambda $_2
e $_3
consistono esattamente dello stesso testo , ma chiaramente non dovrebbero essere considerati dello stesso tipo!
A proposito, C ++ richiede che il compilatore sappia come manipolare il testo di un'espressione C ++ arbitraria , come in
template<class T> void foo(decltype(T())) {}
template void foo<int>(int);
Ma C ++ non (ancora) richiede al compilatore di sapere come manipolare un arbitrario C ++ dichiarazione . decltype([](){ ...arbitrary statements... })
è ancora mal formato anche in C ++ 20.
Notare anche che è facile dare un alias locale a un tipo senza nome usando typedef
/ using
. Ho la sensazione che la tua domanda possa essere nata dal tentativo di fare qualcosa che potrebbe essere risolto in questo modo.
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);
}
MODIFICATO PER AGGIUNGERE: Dalla lettura di alcuni dei tuoi commenti su altre risposte, sembra che ti stia chiedendo perché
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)>);
Questo perché i lambda senza cattura sono costruibili per impostazione predefinita. (In C ++ solo a partire da C ++ 20, ma è sempre stato concettualmente vero.)
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);
Se ci provassi default_construct_and_call<decltype(&add1)>
, t
sarebbe un puntatore a funzione inizializzato di default e probabilmente saresti segfault. Non è utile.