1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
const int i = 0; //decltype(i) обозначает const int bool f(const MyClass& x) = false; //decltype(f) обозначает bool(const MyClass&) //decltype(x) обозначает MyClass& struct Point{ int x; //decltype(Point::x) обозначает int char ch; //decltype(Point::ch) обозначает char }; MyClass X; //decltype(X) обозначает MyClass if (f (w) ) {//...} //decltype(f(w)) обозначает bool //упрощённая версия вектора template <typename T> class vector{ public: //... Т& operator[] (std: : size_t index); }; vector<int> v; // decltype (v) обозначает vector<int> if (v[0] == 0) {//...} // decltype (v[0]) обозначает int& |
1 2 3 4 5 |
if (f (w) ) {//...} ; //Если да, если нет. // Это напоминает булево выражение, //результат: или истинна или ложь, //значит decltype((f (w) ) //обозначит bool |
Так как это механизм вывода типа, то можно использовать подстановку этого обозначения для обозначения типа переменных:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <iostream> #include <cstring> using std::cout; int main(){ int x; decltype (x) value; //Тип value тот же, что тип x (int) char S[10]; decltype(S) str; //Тип str тот же, что тип S (массив из 10 символов) value = 100; strcpy(str,"Hello"); cout << str << '\n' << value; } |
Вот такой вот механизм вывода типа.
Возвращаемый оператором operator[] контейнера тип зависит от самого контейнера. Чтобы было понятнее, разберите пример, где в одинаковых условиях происходят разные результаты:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include <iostream> #include <vector> using namespace std; template <typename T> void Test(const T& oldValue, const T& newValue, const char* message) { cout << "result for " << message << "\n\n"; vector<T> v; v.push_back(oldValue); cout << " before: v[0] = " << v[0] << '\n'; auto x = v[0]; x = newValue; cout << " after: v[0] = " << v[0] << '\n'; cout << "-------------------------------\n"; } int main() { vector<bool> vb; vector<int> vi; Test<bool>(true,false,"bool"); //true!=false, выведется 1,0 Test<int>(100,15,"int"); //100 != 15, выведется 100,100 } |
Эта нелогичность поведения как раз объясняется выводом типа с помощью auto , а такой вывод происходит из-за того, что в первом случае контейнер хранит bool, где при использовании auto для [] получается новый объект, а во втором контейнер хранит int, где при использовании auto для [] получается указатель на начало существующего объекта. Но эта тема не об этом. Эта тема о том, что использование decltype упрощает выражение этой зависимости. Кому-то может показаться, что вместо использования auto нужно использовать тип, выведенный шаблоном.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include <iostream> #include <vector> using namespace std; template <typename T> void Test(const T& oldValue, const T& newValue, const char* message) { cout << "result for " << message << "\n\n"; vector<T> v; v.push_back(oldValue); cout << " before: v[0] = " << v[0] << '\n'; T x = v[0]; //Было auto, стало T x = newValue; cout << " after: v[0] = " << v[0] << '\n'; cout << "-------------------------------\n"; } int main() { vector<bool> vb; vector<int> vi; Test<bool>(true,false,"bool"); //true!=false, выведется 1,1 Test<int>(100,15,"int"); //100 != 15, выведется 100,100 } |
Да, auto — это не выводимый шаблоном тип T, выводятся типы по-разному. Это работает и даже может быть работает как кто-то и задумал, но есть одно большое НО.
Константные! Ссылки! Согласно правилам языка, невозможно переинициализировать ссылку, она должна быть инициализирована сразу после создания. Может быть кто-то хотел конкретный тип, именно эту самую константную ссылку для внутренней локальной переменной. Тогда x=newValue; непозволительное выражение, компилятор не должен давать скомпилировать такую программу, где происходят попытки переинициализации ссылки. Здесь уже на сцену выходит decltype и, отталкивая auto в одну сторону, выводимый шаблоном тип T в другую, важно занимает позицию. Хотя и этот гордый decltype не всегда повлечёт прогнозируемое, но об этом позднее.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#include <iostream> #include <vector> using namespace std; template <typename T> void Test(const T& oldValue, const T& newValue, const char* message) { cout << "result for " << message << "\n\n"; vector<T> v; v.push_back(oldValue); cout << " before: v[0] = " << v[0] << '\n'; decltype(oldValue) x = v[0]; //Было auto, стало T, (потому что тип oldvalue есть T) x = v[0]; //Не компилируюсь. Переинициализация const bool& не допустима. x уже инициализирована как ссылка на v[0] cout << " after: v[0] = " << v[0] << '\n'; cout << "-------------------------------\n"; } int main() { vector<bool> vb; vector<int> vi; Test<bool>(true,false,"bool"); //true!=false, выведется 1,1 Test<int>(100,15,"int"); //100 != 15, выведется 100,100 } |
Вот такое имеется различие в выводах типов. Но это далеко не всё, что можно рассказать и имеет смысл знать. Вернёмся к тому, что в случае с [] тип может определяться как ссылочный, так и не ссылочный.
vector<bool> | [] | возвращает bool |
vector<int> | [] | возвращает int& |
Вот эта каверзность [] может кому-то сильно мешать. Кто-то может быть хочет написать функцию, из которой возвращать такое значение, которое будет обозначать ссылку на какой-то элемент вектора. Возвращать [].
Догадываюсь, что слова трудно написаны, но сейчас будет понятнее. Вот пример подобия написания такой функции:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <iostream> #include <vector> using namespace std; template <typename T, typename T2> auto auth_and_access(T& container, T2 index) { return container[index]; //Попытка возвращения } int main() { vector<int> v(10,0); auth_and_access(v,1) = 10; //v[1] = 10 for (auto &i:v) cout << i << '\n'; } |
Это такой вариант присваивания значения в контейнер по индексу, где по каким-то причинам программист решил, что так удобнее. Но этот код не компилируется.
В этом примере auto не имеет ничего общего с выводом типа auto , оно только-лишь обозначает, что будет использован завершающий возвращаемый тип.
В С++11 этот тип указывался после описания параметров с помощью ->
1 2 3 |
auto foo1(T& container, T2 index)->int{} // return возвращает что-то типа int auto foo2(T& container, T2 index)->T{} // return возвращает что-то типа T auto foo3(T& container, T2 index)->T2{} // return возвращает что-то типа T2 |
В С++14 указание типа стало можно опускать. Использование такого указания типа способствовало выбору возвращаемого типа из типа любого параметра функции.
В примере с попыткой возвращения [ ], который чуть выше, функция auth_and_access возвращает тот тип, который возвращает [] при применении к данному контейнеру. Вовнутрь функции подаётся вектор, хранящий int и некоторый номер индекса. Функция возвращает элемент вектора, взятый по этому поданному туда индексу, указанному в [], сам этот [] возвращает int& (ссылку на int).
Этот тип int, который отдаёт функция является rvalue (это значит, что во что-то можно присвоить то, что отдаёт функция, а в отдаваемое функцией присвоить ничего нельзя).
1 2 3 |
int value = foo(); //Воё что-то присвоить отдаваемое функцией можно foo() = value; //в отдаваемое функцией что-то присвоить нельзя //==rvalue |
В С++14 возможен такой синтаксис: decltype( auto ).
Возможно написание программы таким образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <iostream> #include <vector> using namespace std; template <typename T, typename T2> decltype(auto) auth_and_access(T& container, T2 index) { return container[index]; //Попытка возвращения } int main() { vector<int> v(10,0); auth_and_access(v,1) = 10; //v[1] = 10 for (auto &i:v) cout << i << '\n'; } |
В C++14 это работает.
Но этот код не лишён изъянов. Например, из любой функции с очень древних времён существования С++ можно возвращать структуру, в С++11 код мог бы выглядеть так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <iostream> using std::cout; /*КАКАЯ-ТО СТРУКТУРА*/ struct MyStruct{ int x,y; void print() const { cout << x << ' ' << y << '\n';} //Вывод данных структуры на экран }; MyStruct foo(){; return {100,200}; //Из функции возвращается объект структуры } int main() { MyStruct X = foo(); X.print(); } |
В случае же попытки выполнения такого кода в нашем варианте с decltype( auto ), ничего путного не выйдет.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <iostream> using std::cout; /*КАКАЯ-ТО СТРУКТУРА*/ struct MyStruct{ int x,y; void print() const { cout << x << ' ' << y << '\n';} //Вывод данных структуры на экран }; decltype(auto) foo(){; return {100,200}; //Ошибка } int main() { MyStruct X = foo(); X.print(); } |
Здесь, в принципе, ошибка простая, все эти {} со значениями внутри, они для компиляторов — списки инициализации в первую очередь. Такое препятствие имеет место.
В остальном, эта функция может быть как в левой части выражения для копирования какого-то значения в возвращаемое ею (как в примере), так и в правой части выражения для копирования значения из возвращаемого ею, в какой-то отдельный объект. Сложно словами.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <iostream> #include <vector> using namespace std; template <typename T, typename T2> decltype(auto) auth_and_access(T& container, T2 index) { return container[index]; //Попытка возвращения } int main() { vector<int> v(10,0); auth_and_access(v,1) = 10; //Скопировал 10 в возвращаемое функцией auto x = auth_and_access(v,1); //Скопировал 2 элемент из возвращаемого функцией в х for (auto &i:v) cout << i << '\n'; cout << "x == " << x << '\n'; //Можно и так auto y = auth_and_access(v,1) = 10; } |
Интересные возможности. Но это снова не всё. Такой приём имеет ещё один недостаток. Нельзя в функцию auth_and_access передать временный объект. Параметр указан как неконстантня lvalue ссылка (&).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> #include <vector> using namespace std; template <typename T, typename T2> decltype(auto) auth_and_access(T& container, T2 index) { return container[index]; //Попытка возвращения } int main() { auto x = auth_and_access( vector<int> ({1,2,3,4,5}),1); //Скопировал в x возвращаемый функцией элемент, x cout << "x == " << x << '\n'; } |
Здесь, в параметрах, отдаваемых вовнутрь функции, отдаётся временный вектор, который прекратит своё существование с прекращением существования породившего его вызова функции. Такие временные объекты в большинстве случаев создаются компилятором, а не программистом, но и программистом тоже. Любая символьная константа может представлять собой временный объект:
1 |
int x = 77; //эта 77 - временный объект |
Но тема не о временных объектах, такие временные объекты называются rvalue, или правоассоциативное (то, что с правой части и должно влиять на левую). Смысл в том, что этот объект имеет то же время жизни, которое имеет функция с момента вызова до возврата из неё значения. Из-за этого легко словить ошибку неопределённого поведения. Если создать ссылку на элемент, возвращаемый из такой функции, где в функцию передаётся временный объект, то вы получите по рукам и можете услышать клятое UB.
До С++11 было невозможно получить доступ для модификаций таких временных объектов, а после принятия стандарта С++11 стало возможно. Чтобы функция могла работать с таким временным объектом, и внутри функции можно было его модифицировать, нужно указать, что параметр, приходящий вовнутрь функции является временным объектом, делается это с помощью &&.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> #include <vector> using namespace std; template <typename T, typename T2> decltype(auto) auth_and_access(T&& container, T2 index) { return container[index]; //Попытка возвращения } int main() { auto x = auth_and_access(vector<int>{1,2,3,4,5},1); //Копируем в x второй элемент вектора (v[1]) cout << x; //Всё законно. Здесь просто скопировано значение cin.get(); } |
А вот если ссылаться на элемент ссылкой, то это уже — UB (неопределённое поведение)
1 2 3 |
auto &x = auth_and_access(vector<int>{1,2,3,4,5},1); //Ссылаемся на элемент вектора (v[1]), //но вектор в тот момент уже прекратил существование, UB cout << x; //Выведет, что угодно |
Будьте внимательны и помните об этом.
Для С++14 окончательный код имеет вот такой вид:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> #include <vector> using namespace std; template <typename T, typename T2> //T==container, T2==index decltype(auto) auth_and_access(T&& container, T2 index) { return forward<T>(container)[index]; //Условно приводим (container)[index] к ссылке rvalue, //для обеспечения точной пересылки } int main() { auto x = auth_and_access(vector<int>{1,2,3,4,5},1); //Копируем в x второй элемент вектора (v[1]) cout << x; //Обратите внимание, что x - просто значение, а не ссылка на умерщвлённый элемент вектора cin.get(); } |
Есть в decltype ещё кое что, что следует понимать, осознавать.
В коде это выглядит так:
1 2 3 4 5 |
int x = 10; decltype (x) z = 0; //вовнутрь decltype ушло имя переменной x, decltype вернёт int decltype ((x)) temp; //вовнутрь decltype ушло (x), это выражение для компилятора сложнее, чем просто x, //это имя, обрамлённое скобками //decltype вернёт ссылку: int& |
decltype почти всегда возвращает, что мы от него хотим, но почти — это не всегда. Если имя представляет собой больше выражение, чем имя, то ждём от decltype ссылку.
Чтобы было понятнее, более полный пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <iostream> using namespace std; int main() { int value = 10; decltype (value) zero = 0; //вовнутрь decltype ушло имя переменной value, decltype вернёт int decltype ((value)) temp = zero; //вовнутрь decltype ушло (value), это выражение для компилятора сложнее, чем просто value, //это имя, обрамлённое скобками //decltype вернёт ссылку: int&\ temp++; //Изменили temp cout << zero; //Но поменялось и zero cin.get(); } |
Этот пример показывает, что стоит быть особо внимательным к деталям. Ведь, если возвращать из функции имя, обёрнутое скобками (), то вернётся ссылка, а это звоночек к неопределённому поведению.
- decltype почти всегда даёт тип переменной или выражения без каких-либо изменений
- для lvalue выражений типа Т, отличных от имени, decltype гарантировано даёт Т&
- C++14 поддерживает синтаксическую конструкцию decltype( auto ), которая подобно auto , выводит тип из его инициализатора, но выполняет вывод типа с использованием правил decltype
Скотт Мейерс по этому поводу пишет так: “I have no idea why type deduction for auto and for templates is not identical. If you know, please tell me!”.
Спасибо вам огромное. Наконец-то нашёл, что давно искал.