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
, который так и определён.