Ключевые слова auto и decltype – сходства, отличия и нюансы использования. Часть 3. decltype вывод типа сложного выражения.
В продолжение предыдущего.
2
ситуация.
К
ней относятся все выражения, не попадающие
в 1 ситуацию, то есть все выражения, НЕ
являющиеся простой, непараметризированной
переменной, параметром функции или
доступом к члену класса. Для упрощения
терминологии, давайте назовем выражения,
подпадающие под ситуацию 2 — сложными
выражениями,
и наоборот, подпадающие под 1 ситуацию
— простыми
выражениями.
Простейший путь произвести сложное
выражение — это взять простое и заключить
его в скобки, вот так (х). Так что же делает
decltype
со
сложными выражениями? Для точной
формулировки правила необходимо
разобраться сначала с терминами lvalue,
xvalue и
prvalue.
Термины
xvalue
и
prvalue
определяют
разбиение множества rvalue
на
два подмножества. Поэтому
сначала разбиремся с lvalue
и
rvalue.
Первоначальное
определение lvalue
и
rvalue
из
младенческих дней языка С может быть
сформулировано так: Lvalue
есть
выражение, которое может находится в
левой или правой стороне присваивания,
в то время как rvalue
есть
выражение, которое может находится
только на правой стороне присваивания.
Например:
int
a = 42;
int b = 43;
// a и b есть lvalue:
a = b; // ok
b = a; // ok
a = a * b; // ok
// a * b есть rvalue:
int c = a * b; // ok, rvalue справа от присваивания
a * b = 42; // ошибка, rvalue слева от присваивания
В
С++, это определение может быть полезно
как первый, интуитивный подход к lvalue
и rvalue. Однако, С++ с его пользовательскими
типами добавил некоторые тонкости,
касающиеся изменяемости и присваиваемости,
что сделало это определение неверным.
Не входя глубоко в детали, сформулируем
новое определение, которое позволит
нам легче перейти к rvalue
ссылкам:
Lvalue
есть
выражение которое относится к расположению
в памяти и позволяет нам взять адрес
этого расположения с помощью оператора
&.
Rvalue это
выражение которое не является lvalue.
Например:
//
lvalue:
//
int i = 42;
i = 43; // ok, i это lvalue
int* p = &i; // ok, i это lvalue
int& foo();
foo() = 42; // ok, foo() это lvalue
int* p1 = &foo(); // ok, foo() это lvalue
// rvalue:
//
int foobar();
int j = 0;
j = foobar(); // ok, foobar() это rvalue
int* p2 = &foobar(); // error, нельзя взять адрес rvalue
j = 42; // ok, 42 это rvalue
Теперь
вернемся к
xvalue и
prvalue.
- rvalue есть xvalue в одном из последующих вариантов:1. вызов функции, в котором возвращаемое функцией значение объявлено как rvalue-ссылка, например std::move(x);2. приведение к rvalue-ссылке, например static_cast<A&&>(a);3. доступ к члену xvalue, например (static_cast<A&&>(a)).m_x.
- Все остальные rvalue есть prvalue.
Теперь
мы готовы сформулировать правило : как
dectype
выводит
тип сложного выражения.
Пусть
expr
есть
выражение, которое не является простой,
непараметризированной переменной,
параметром функции или доступом к члену
класса. Пусть T
есть
тип expr.
- Если
expr
есть
lvalue, то decltype(expr)
есть
T&.
- Если
expr
есть
xvalue,
то decltype(expr)
есть
T&&.
- В
остальных случаях, если expr
есть
prvalue,
то decltype(expr)
есть
T.
Для
иллюстрации возьмем пример к ситуации
1 и усложним его, добавив скобки с целью
создания сложных выражений. decltype
со
сложными выражениями покажем коричневым
, decltype с
простыми выражениями
синим,
и
auto зеленым
цветом.
Заметьте,
что поведение auto
не зависит от того, заключено выражение
в скобки или нет.
struct
S
{
S()
S()
{
m_x
= 42;
}
int
m_x;
};
int x;
const int cx = 42;
const int& crx = x;
const S* p = new S();
- int x - lvalue.
//
(x) имеет
тип
int, и
decltype добавляет
ссылочность к
lvalue.
// Поэтому,
x_with_parens_type есть
int&.
typedef decltype((x)) x_with_parens_type;
typedef decltype((x)) x_with_parens_type;
// x объявлен int: x_type есть int.
typedef decltype(x) x_type;
//
auto выводит
тип
int: a_p и
a типа
int.
auto a_p = (x);
auto a = x;
auto a_p = (x);
auto a = x;
- const int cx - lvalue
//
Тип
(cx) есть
const int. Поскольку
(cx) есть
lvalue,
// decltype добавляет
к нему ссылку:
cx_with_parens_type есть
const
int&.typedef decltype((cx)) cx_with_parens_type;
//
cx объявлен
const int: cx_type есть
const int.
typedef decltype(cx) cx_type;
// auto удаляет квалификатор const: b_p и b типа int.
auto b_p = (cx);
auto b = cx;
//
Тип
(crx) есть
const int&, и
это
lvalue.typedef decltype(cx) cx_type;
// auto удаляет квалификатор const: b_p и b типа int.
auto b_p = (cx);
auto b = cx;
- const int& crx - lvalue
// decltype добавляет ссылку. По правилам склеивания ссылок
// это ничего не меняет. Поэтому,
// crx_with_parens_type есть const int&.
typedef decltype((crx)) crx_with_parens_type;
// crx объявлен const int&: crx_type есть const int&.
typedef decltype(crx) crx_type;
// auto удаляет ссылку и квалификатор const: c_p и c типа int.
auto c_p = (crx);
auto c = crx;
// S::m_x объявлен int. Поскольку p есть указатель const,
// тип (p->m_x) есть const int. Поскольку (p->m_x) есть
// lvalue, decltype Добавляет к нему ссылку. Поэтому,
// m_x_with_parens_type есть const int&.
typedef decltype((p->m_x)) m_x_with_parens_type;
// S::m_x объявлен int: m_x_type есть int.
typedef decltype(p->m_x) m_x_type;
// auto видит, что p->m_x константен, но удаляет квалификатор const
// Поэтому, d_p и d типа int.
auto d_p = (p->m_x);
auto d = p->m_x;
Теперь добавим несколько более сложных примеров — в том смысле, что они будут использовать операторы и вызовы функций.
// auto удаляет ссылку и квалификатор const: c_p и c типа int.
auto c_p = (crx);
auto c = crx;
- const int S::m_x - lvalue
// S::m_x объявлен int. Поскольку p есть указатель const,
// тип (p->m_x) есть const int. Поскольку (p->m_x) есть
// lvalue, decltype Добавляет к нему ссылку. Поэтому,
// m_x_with_parens_type есть const int&.
typedef decltype((p->m_x)) m_x_with_parens_type;
// S::m_x объявлен int: m_x_type есть int.
typedef decltype(p->m_x) m_x_type;
// auto видит, что p->m_x константен, но удаляет квалификатор const
// Поэтому, d_p и d типа int.
auto d_p = (p->m_x);
auto d = p->m_x;
Теперь добавим несколько более сложных примеров — в том смысле, что они будут использовать операторы и вызовы функций.
const
S foo();
const int& foobar();
std::vector<int> vect = {42, 43};
// foo() объявлена возвращающей const S. Тип of foo()
// есть const S. Поскольку foo() есть prvalue, decltype не
// добавляет ссылку. Поэтому, foo_type есть const S.
// Замечание:мы должны использовать пользовательский тип S здесь вместо int,
// поскольку C++ не позволяет нам возвращать встроенные типы как const.
// (Точнее позволяет, но const будет проигнорирована.)[3]
typedef decltype(foo()) foo_type;
// auto удаляет квалификатор const: a типа S.
//
auto a = foo() ;
const int& foobar();
std::vector<int> vect = {42, 43};
- foo() - prvalue типа const S
// foo() объявлена возвращающей const S. Тип of foo()
// есть const S. Поскольку foo() есть prvalue, decltype не
// добавляет ссылку. Поэтому, foo_type есть const S.
// Замечание:мы должны использовать пользовательский тип S здесь вместо int,
// поскольку C++ не позволяет нам возвращать встроенные типы как const.
// (Точнее позволяет, но const будет проигнорирована.)[3]
typedef decltype(foo()) foo_type;
// auto удаляет квалификатор const: a типа S.
//
auto a = foo() ;
- foolbar() - lvalue
//
Тип foobar() есть
const int&, и это lvalue.
// Поэтому, decltype добавляет ссылку.
// По правилу склеивания ссылок,
// это ничего не меняет. Поэтому,
// foobar_type типа const int&.
typedef decltype(foobar()) foobar_type;
// auto удаляет ссылку и квалификатор const: b типа int.
auto b = foobar();
// Тип vect.begin() есть std::vector<int>::iterator.
// Поскольку vect.begin() есть prvalue, ссылка
// не добавляется. Поэтому, iterator_type типа
// std::vector<int>::iterator.
//
typedef decltype(vect.begin()) iterator_type;
// auto также выводит тип std::vector<int>::iterator,
// поэтому iter имеет тип std::vector<int>::iterator.
auto iter = vect.begin();
// Оператор operator[] вектора std::vector<int> объявлен возвращающим тип
// int&. Поэтому, тип выражения vect[0] есть int&.
// Поскольку vect[0] есть lvalue, decltype добавляет ссылку.
// По правилам склеивания ссылок это не изменяет результата
// Поэтому, first_element имеет тип int&.
decltype(vect[0]) first_element = vect[0];
// second_element имеет тип int, поскольку auto удаляет ссылку.
auto second_element = vect[1];
// Тип произведения int, и произведение
// и произведение prvalue. Поэтому, prod_cxcy_type есть int.
typedef decltype(cx * cy) prod_cxcy_type;
// то же для auto: b типа int.
auto b = cx * cy;
// Тип выражения double, и выражение
// есть lvalue. Поэтому, добавляется ссылка, и
// cond_type есть double&.
typedef decltype(d1 < d2 ? d1 : d2) cond_type;
// Тип выражения double, поэтому c типа double.
auto c = d1 < d2 ? d1 : d2;
// Тип выражения double. Выражение
// есть prvalue, потому что для обеспечения продвижения
// x к double, создаётся временное значение.
// Поэтому, ссылка не добавляется, и
// cond_type_mixed есть double.
typedef decltype(x < d2 ? x : d2) cond_type_mixed;
// Тип выражения есть is double, отсюда d типа double.
auto d = x < d2 ? x : d2;
Теперь перейдем к более практическому примеру, в котором будет использоваться тернарный оператор, упомянутый ранее. Предположим, нам требуется написать функцию, возвращающую меньшее из двух чисел, причем числа могут быть как целыми, так и с плавающей точкой. Функция std::min нам не поможет, поскольку аргументы в ней должны быть одного типа:
SomeFunctor<int> func;
typedef decltype(func)::result_type integer; // доступ к вложенному типу
class DerivedFunctor : public decltype(foo)
{
public:
DerivedFunctor(): decltype(foo)(foo) {}
//.......
};
// Поэтому, decltype добавляет ссылку.
// По правилу склеивания ссылок,
// это ничего не меняет. Поэтому,
// foobar_type типа const int&.
typedef decltype(foobar()) foobar_type;
// auto удаляет ссылку и квалификатор const: b типа int.
auto b = foobar();
- vect.begin() - prvalue
// Тип vect.begin() есть std::vector<int>::iterator.
// Поскольку vect.begin() есть prvalue, ссылка
// не добавляется. Поэтому, iterator_type типа
// std::vector<int>::iterator.
//
typedef decltype(vect.begin()) iterator_type;
// auto также выводит тип std::vector<int>::iterator,
// поэтому iter имеет тип std::vector<int>::iterator.
auto iter = vect.begin();
- vect[i] - lvalue
// Оператор operator[] вектора std::vector<int> объявлен возвращающим тип
// int&. Поэтому, тип выражения vect[0] есть int&.
// Поскольку vect[0] есть lvalue, decltype добавляет ссылку.
// По правилам склеивания ссылок это не изменяет результата
// Поэтому, first_element имеет тип int&.
decltype(vect[0]) first_element = vect[0];
// second_element имеет тип int, поскольку auto удаляет ссылку.
auto second_element = vect[1];
В
последнем примере выше, первый элемент
вектора может быть изменен через ссылку
first_element, а
второй элемент не может быть изменен
через second_element,
поскольку это не ссылка. Это показывает,
как недостаточное понимание auto
и decltype
может привести к
проблемам, которые проявляются только
во время выполнения программы.
Теперь приведем несколько примеров с
бинарными и тернарными операторами.
int
x = 0;
int y = 0;
const int cx = 42;
const int cy = 43;
double d1 = 3.14;
double d2 = 2.72;
int y = 0;
const int cx = 42;
const int cy = 43;
double d1 = 3.14;
double d2 = 2.72;
- (x*y) - prvalue
// Тип произведения int, и произведение
//
есть prvalue. Поэтому, prod_xy_type есть
int.
//
typedef decltype(x * y) prod_xy_type;
//
typedef decltype(x * y) prod_xy_type;
//
auto также выводит тип int: a типа int.
//
auto a = x * y;
//
auto a = x * y;
- (cx*cy) - prvalue
// и произведение prvalue. Поэтому, prod_cxcy_type есть int.
typedef decltype(cx * cy) prod_cxcy_type;
// то же для auto: b типа int.
auto b = cx * cy;
- (d1 < d2 ? d1 : d2) - lvalue [4]
// Тип выражения double, и выражение
// есть lvalue. Поэтому, добавляется ссылка, и
// cond_type есть double&.
typedef decltype(d1 < d2 ? d1 : d2) cond_type;
// Тип выражения double, поэтому c типа double.
auto c = d1 < d2 ? d1 : d2;
- (x < d2 ? x : d2) - prvalue[4]
// Тип выражения double. Выражение
// есть prvalue, потому что для обеспечения продвижения
// x к double, создаётся временное значение.
// Поэтому, ссылка не добавляется, и
// cond_type_mixed есть double.
typedef decltype(x < d2 ? x : d2) cond_type_mixed;
// Тип выражения есть is double, отсюда d типа double.
auto d = x < d2 ? x : d2;
Теперь перейдем к более практическому примеру, в котором будет использоваться тернарный оператор, упомянутый ранее. Предположим, нам требуется написать функцию, возвращающую меньшее из двух чисел, причем числа могут быть как целыми, так и с плавающей точкой. Функция std::min нам не поможет, поскольку аргументы в ней должны быть одного типа:
int i = 42;
double d = 42.1;
auto a = std::min(i, d); // error: неоднозначный шаблонный параметр
double d = 42.1;
auto a = std::min(i, d); // error: неоднозначный шаблонный параметр
Чтобы скомпилировать
этот код необходимо явное приведение
типа:
auto a =
std::min(static_cast<double>(i), d);
Как же написать
такую шаблонную функцию fpmin()
, которая принимала
бы аргументы различных типов и возвращала
минимальное значение с сохранением его
типа?
Лобовое (и неудачное)
решение следующее:
template<typename T,
typename S>
auto fpmin(T x, S y) -> decltype(x < y ? x : y)
auto fpmin(T x, S y) -> decltype(x < y ? x : y)
{
return x < y ? x : y;
}
return x < y ? x : y;
}
Вот почему неудачное.
В соответствии с тем, что было сказано
ранее о тернарном операторе, тип
decltype(x < y ? x : y)
может быть, а может
и не быть ссылкой. Если типы параметров функции совпадают, то возвращаемый тип — ссылка. Если
имеет место смесь, например int
и double,
то происходит возврат
по значению.
Кроме того, в первом
случае функция возвращает ссылку на
локальную переменную (параметр в этом
случае), что тоже не хорошо. Лучшим вариантом является
удаление возможной ссылочности
возвращаемого значения с помощью
метафункции std::remove_reference:
template<typename T,
typename S>
auto fpmin(T x, S y) ->typename std::remove_reference<decltype(x < y ? x : y)>::type
auto fpmin(T x, S y) ->typename std::remove_reference<decltype(x < y ? x : y)>::type
{
return x < y ? x : y;
}
return x < y ? x : y;
}
С использованием стандарта С++14 синтаксис можно
упростить:
template<typename T,
typename S>
auto fpmin(T x, S y) ->std::remove_reference_t<decltype(x < y ? x : y)>
auto fpmin(T x, S y) ->std::remove_reference_t<decltype(x < y ? x : y)>
{
return x < y ? x : y;
}
return x < y ? x : y;
}
или пойти ещё
дальше, отказавшись полностью от
синтаксиса «trailing return type» и
предоставить компилятору выводить
возвращаемый тип из тела функции. В этом
случае тип всегда выводится по значению,
а не по ссылке[5]:
template<typename T,
typename S>
auto fpmin(T x, S y)
auto fpmin(T x, S y)
{
return x < y ? x : y;
}
return x < y ? x : y;
}
Приведем ещё россыпь
свойств decltype, и потом
перейдём к новациям, появившихся в С++14/17.
- Важным свойством является то, что операнд decltype никогда не вычисляется, поэтому можно бесстрашно обращаться к элементам вне диапазона:
std::vector<int>
vect;
assert(vect.empty());
typedef decltype(vect[666]) integer;
assert(vect.empty());
typedef decltype(vect[666]) integer;
- Другое свойство decltype, которое стоит отметить, заключается в том, что когда decltype(expr) - это имя пользовательского типа (не ссылка или указатель, и не встроенный тип или тип функции), в этом случае decltype(expr) также является именем класса, Это означает, что вы можете напрямую обращаться к вложенным типам:
template<typename R>
class SomeFunctor
class SomeFunctor
{
public:
typedef R result_type;
result_type operator()()
public:
typedef R result_type;
result_type operator()()
{
return R();
}
return R();
}
SomeFunctor(){}
};
SomeFunctor<int> func;
typedef decltype(func)::result_type integer; // доступ к вложенному типу
- Вы можете использовать decltype(expr) для специфицирования базового класса:
auto foo = [](){return 42;};
class DerivedFunctor : public decltype(foo)
{
public:
DerivedFunctor(): decltype(foo)(foo) {}
//.......
};
Комментарии
Отправить комментарий