Ключевые слова 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()
      {
         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;


// x объявлен int: x_type есть int.
typedef decltype(x) x_type;



// auto выводит тип int: a_p и a типа int.
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;

  • const int& crx - lvalue 
// Тип (crx) есть const int&, и это 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;

  • 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() - 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() - 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;


  • (x*y) - prvalue

// Тип произведения int, и произведение
// есть prvalue. Поэтому, prod_xy_type есть int.
//
typedef decltype(x * y) prod_xy_type;
// auto также выводит тип int: a типа int.
//
auto a = x * y;
  • (cx*cy) - prvalue
// Тип произведения int (не const int!),
// и произведение 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: неоднозначный шаблонный параметр

Чтобы скомпилировать этот код необходимо явное приведение типа:
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)
{
      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
{
    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)>
{
return x < y ? x : y;
}

или пойти ещё дальше, отказавшись полностью от синтаксиса «trailing return type» и предоставить компилятору выводить возвращаемый тип из тела функции. В этом случае тип всегда выводится по значению, а не по ссылке[5]:

template<typename T, typename S>
auto fpmin(T x, S y)
{
   return x < y ? x : y;


Приведем ещё россыпь свойств decltype, и потом перейдём к новациям, появившихся в С++14/17.
  1. Важным свойством является то, что операнд decltype никогда не вычисляется, поэтому можно бесстрашно обращаться к элементам вне диапазона:
std::vector<int> vect;
assert(vect.empty());
typedef decltype(vect[666]) integer;
  1. Другое свойство decltype, которое стоит отметить, заключается в том, что когда decltype(expr) - это имя пользовательского типа (не ссылка или указатель, и не встроенный тип или тип функции), в этом случае decltype(expr) также является именем класса, Это означает, что вы можете напрямую обращаться к вложенным типам:
template<typename R>
class SomeFunctor
{
      public:
           typedef R result_type;
           result_type operator()()
          {
             return R();
          }
          SomeFunctor(){}
};

SomeFunctor<int> func;
typedef decltype(func)::result_type integer; // доступ к вложенному типу 
  1. Вы можете использовать decltype(expr) для специфицирования базового класса:
auto foo = [](){return 42;};

class DerivedFunctor : public decltype(foo)
{
      public:
          DerivedFunctor(): decltype(foo)(foo) {}
      //.......
};



Комментарии

Популярные сообщения из этого блога

Полезные новации С++17. Выражения свертки (Fold expressions)

Перегрузка и специализация шаблонных функций.

Поддержка декомпозиции при объявлении (structural bindings) для классов