decltype(auto) łączy mechanizm dedukcji typu z decltype z wygodą składni auto. Podczas gdy auto stosuje zasady dedukcji argumentów szablonów, które przekształcają tablice na wskaźniki oraz usuwają najwyższe kwalifikatory cv i odniesienia, decltype(auto) zachowuje dokładny typ wyrażenia inicjalizującego. Konkretnie, jeśli wyrażenie to niezwrócone imię zmiennej, decltype daje zadeklarowany typ; jeśli jest to wyrażenie wartościowe rodzajowe w nawiasach, zwraca odwołanie lvalue. To pozwala funkcjom idealnie przekazywać swoje wartości zwracane bez konieczności eksponowania wyrażeń decltype lub martwienia się o złożoności związane z redukcją odniesień.
Musieliśmy wdrożyć uniwersalny wrapper dla dostępu do bazy danych, który warunkowo zwracałby albo odniesienie do cache'owanego rekordu, albo nowo skonstruowaną wartość domyślną. Krytycznym wymaganiem było zachowanie dokładnej semantyki zwracania typu — odniesienia muszą pozostać odniesieniami, aby uniknąć kopiowania dużych obiektów, podczas gdy wartości powinny być przenoszone lub kopiowane zgodnie z potrzebami.
Jedno z proponowanych rozwiązań wykorzystało explicite typ zwracany ze decltype i std::declval, specyfikując decltype(std::declval<Accessor>()(key)). Plusy: Dokumentuje to wyraźnie transformację typu i działa w C++11. Minusy: Składnia jest rozwlekła, wymaga idealnego przenoszenia argumentów do std::declval i staje się trudna do utrzymania przy obsłudze wielu przeciążeń lub logiki warunkowej.
Inne podejście wykorzystało proste auto jako typ zwracany, zakładając, że kompilator dedukuje odpowiedni typ. Plusy: Jest to zwięzłe i czytelne. Minusy: Auto stosuje zasady ograniczania typów, przekształcając Record& na Record i usuwając kwalifikatory const, co powoduje niepotrzebne głębokie kopie i narusza poprawność const, gdy wywołujący oczekuje wyłącznie do odczytu.
Wybraliśmy decltype(auto) jako typ zwracany, który stosuje zasady zachowania typów decltype do zwracanego wyrażenia. Ten wybór wyeliminował kod boilerplate, jednocześnie zapewniając, że odniesienia lvalue, kwalifikatory const i odniesienia rvalue są przekazywane poprawnie do wywołującego. Wynikiem był zerowy narzut na uniwersalny interfejs, który obsługuje zarówno zwroty wartości, jak i odniesień bez duplikacji kodu czy ukrytych konwersji, zmniejszając opóźnienia w wysoka częstotliwość wyszukiwań w pamięci podręcznej.
Dlaczego decltype((var)) daje typ odniesienia lvalue, podczas gdy decltype(var) daje zadeklarowany typ, i jak to wpływa na instrukcje zwracające decltype(auto)?
decltype stosuje dwa odmienne zasady: dla niezwróconego wyrażenia identyfikatora (takiego jak var) produkuje typ zadeklarowany dla tego bytu; dla jakiegokolwiek innego wyrażenia, w tym wyrażeń w nawiasach takich jak (var), zwraca typ tego wyrażenia, który jest typem odniesienia lvalue, jeśli wyrażenie jest lvalue. Gdy używasz decltype(auto), zwracając (var), tworzysz odniesienie do zmiennej lokalnej, co prowadzi do wieszających się odniesień po zakończeniu funkcji. Dlatego należy unikać niepotrzebnych nawiasów w instrukcjach zwracających, gdy używasz decltype(auto), ponieważ dodatkowe nawiasy zmieniają kategorię wyrażenia z wyrażenia identyfikatora na wyrażenie lvalue.
Jak decltype(auto) oddziałuje z xvalues (wygasającymi wartościami) w porównaniu do prvalues?
decltype(auto) zachowuje kategorie wartości zgodnie z semantyką decltype. Jeśli funkcja zwraca xvalue (np. std::move(obj)), decltype(auto) dedukuje typ jako odniesienie rvalue (T&&), podczas gdy auto dedukowałby typ jako T. Ta różnica jest kluczowa podczas implementacji fabrycznych funkcji idealnych do przenoszenia, które muszą zachować semantykę przenoszenia zwracanych tymczasowych wartości bez wymuszania kopii lub wymagania explicitnych adnotacji std::move w miejscu wywołania.
Co się dzieje, gdy decltype(auto) jest używane z listami inicjalizacyjnymi w klamrze i dlaczego różni się to od dedukcji auto?
Gdy jest inicjowane z listą-inicjalizacyjną, taką jak {1, 2, 3}, auto dedukuje do std::initializer_list<int>, ale decltype(auto) próbuje dedukować listę-inicjalizacyjną jako typ, co jest kontekstem niededukowanym dla decltype i skutkuje błędnym kodem. To uniemożliwia użycie decltype(auto) do zwracania list inicjalizacyjnych w klamrach bezpośrednio, w przeciwieństwie do auto, które może dedukować tymczasowy std::initializer_list. Ta subtelna różnica wynika z tego, że decltype zachowuje typ wyrażenia dokładnie, włączając konteksty niededukowane, w których wyrażenie nie jest zmienną lub wywołaniem funkcji.