Decltype, declval, auto, nullptr
decltype
Иногда возвращаемый тип не хочется писать руками.
int f();
??? g() {
return f()
}
В C++11 появилась конструкция, которая позволяет по выражению узнать его тип:
int main() {
decltype(2 + 2) a = 42; // int a = 42;
}
decltype(f()) g() {
return f();
}
decltype сделан так, чтобы его было удобно использовать для возврата значений:
int foo();
int& bar();
int&& baz();
// decltype(foo()) int
// decltype(bar()) int&
// decltype(baz()) int&&
// decltype(expr)
// type для (prvalue)
// type& для (lvalue)
// type&& для (xvalue)
О decltype можно думать как о двух языковых конструкциях в одном синтаксисе
// decltype (expr) - работает, как написано выше
// decltype (var_name) - возвращает тип этой переменной
decltype определён таким образом, что он работает и для членов класса:
struct x {
int a;
};
int main() {
x::a; // COMPILE ERROR
decltype(x::a);
int a;
decltype(a) b = 42; // int b = 42
decltype((a)) c = a; // int& c = a
}
Последнее работает так, потому что (a) это выражение и оно имеет тип int&, а a - это имя переменной.
declval
Иногда хочется узнать тип чего-то, что зависит от шаблонных аргументов функции, но просто сделать это с помощью decltype не получится, так как тогда компилятор встречает обращение к параметру функции, когда ещё не дошёл до его объявления.
Для этого есть синтаксическая конструкция declval:
int foo(int);
float foo(float);
template <typename Arg0>
decltype(foo(arg0)) qux(Arg0&& arg0) { // COMPILE ERROR
return foo(std::forward<Arg0>(arg0));
}
template <typename Arg0>
decltype(foo(declval<Arg0>())) qux(Arg0&& arg0) {
return foo(std::forward<Arg0>(arg0));
}
Сигнатура у declval могла бы выглядеть как-то так:
template <typename T>
T declval();
Для declval не нужно тело функции, так как decltype не генерирует машинный код и считается на этапе компиляции.
В языке есть несколько мест с похожей логикой - например, sizeof. Такие места называются unevaluated contexts.
При использовании сигнатуры, как выше, могут возникать проблемы с неполными типами (просто не скомпилируется). Это происходит из-за того, что если функция возвращает структуру, то в точке, где вызывается эта функция, эта структура должна быть complete типом. Чтобы обойти это, делают возвращаемый тип rvalue-ссылкой:
template <typename T>
T&& declval();
Trailing return types
Чтобы не писать declval, сделали возможной следующую конструкцию:
template <typename Args>
auto qux(Args&& ...args) -> decltype(foo(std::forward<Args>(args)...)) {
return foo(std::forward<Args>(args)...);
}
auto
Можно заметить, что в return и decltype повторяется одно и то же выражение. Для этого добавили возможность писать decltype(auto):
int main() {
decltype(auto) b = 2 + 2; // int b = 2 + 2
}
template <typename Args>
decltype(auto) qux(Args&& ...args) {
return foo(std::forward<Args>(args)...);
}
Возникает вопрос, а зачем там вообще decltype, можно ли его заменить на просто auto? Для этого стоит сказать о том, как работает auto.
Правило вывода типов у auto почти полностью совпадают с тем, как выводятся шаблонные параметры. Поэтому auto отбрасывает ссылки и cv.
int& bar();
int main() {
auto c = bar(); // int c = bar()
auto& c = bar(); // int& c = bar()
}
Поэтому обычный auto в возвращаемом типе отбрасывает ссылки с cv, поэтому чаще всего нам нужно decltype(auto).
И ещё стоит сказать, что если у функции несколько return'ов, которые выводятся в разные типы, то использовать decltype и auto нельзя:
auto f(bool flag) { // COMPILE ERROR
if (flag) {
return 1;
} else {
return 1u;
}
}
nullptr
До C++11 для нулевого указателя использовалось либо 0, либо макрос NULL. Правило было такое - числовая константа, которая вычисляется в 0, может приводиться неявно в нулевой указатель.
Это привело бы к проблеме при использовании форвардинга, так как тип бы выводился в int.
В C++11 появился отдельный тип nullptr_t, который может приводиться к любому указателю. Определено это примерно так:
struct nullptr_t {
template <typename T>
opeartor T*() const {
return 0;
}
}
nullptr_t const nullptr;
Единственное отличие в том, что в C++11 nullptr это keyword, встроенный в язык, а не глобальная переменная. Но к типу всё ещё можно обратиться через decltype(nullptr), в стандартной библиотеке есть std::nullptr_t, который так и определён.