Инициализация переменных. Ключевые слова auto и decltype

Практический курс по C/C++: https://stepik.org/course/193691

Продолжаем рассматривать основные отличия языка С++ от Си. Следующим шагом будет логично рассмотреть способы инициализации переменных в С++. Напомню, что в языке Си инициализация записывается с помощью оператора ‘=’ при объявлении переменной. Например:

int val = 0;  // инициализация
int pow[] = {1, 2, 4, 8};

Если же символ ‘=’ используется после объявления переменной:

double d;
d = 5.78;  // операция присваивания

то он уже определяет операцию присваивания.

Эти две операции нужно четко себе различать, так как они работают по-разному. Так вот, в языке С++, чтобы четко, синтаксически отделить одну операцию от другой, введен новый синтаксис и новые операции инициализации переменных. Помимо уже знакомого нам оператора ‘=’ инициализацию можно выполнять, по крайней мере, еще двумя распространенными способами:

    short sh(10);   // functional notation
    double d(-4.37);
 
    char ch{'b'};   // braced initialization
    long lv{};

При выводе значений этих переменных:

    cout << sh << " " << d << endl;
    cout << ch << " " << lv << endl;

увидим:

10 -4.37
b 0

То есть, переменным были присвоены значения, указанные в скобках. Если же в скобках ничего не прописано, то переменная инициализируется нулем.

Вообще, в языках Си и С++ начальная инициализация переменных – это рекомендуемая практика. Напомню, что все локальные переменные, размещаемые в стековом фрейме (их еще называют автоматическими), могут принимать неопределенное начальное значение. Конкретное поведение зависит от компилятора С++. Поэтому, чтобы не возникало непредвиденных ошибок из-за неопределенного начального значения переменной, ее следует сразу инициализировать. Именно для этого введены операторы круглых и фигурных скобок.

Но здесь возникает вопрос, чем отличаются эти два способа инициализации? На самом деле, почти ничем, но при использовании фигурных скобок дополнительно происходит проверка типа инициализируемых данных. Например, если у переменной lv в фигурных скобках прописать вещественное число:

long lv{5.43};

то компилятор выдаст ошибку из-за несовпадения типов переменной lv (long) и числа 5.43 (double). Причем, тип double нельзя без потерь привести к типу long. Но, если фигурные скобки заменить на круглые:

long lv(5.43);

то ошибки не будет и переменная lv примет значение 5, то есть, будет выполнено неявное приведение типа double к типу long.

Для полноты картины приведу еще несколько примеров инициализации переменных указанными способами:

    int sum {2 + 3 + 4 + 5};
    double p (1 * 2.3 * 4.5 - 1);
    bool n_fl(false), t_fl(true);

То есть, в инициализаторе можно прописывать любые допустимые конструкции языка С++.

Особенность модификатора const в С++

Я думаю, с инициализацией переменных, в целом, все понятно, поэтому перейдем к следующему отличию использования модификатора const. Как мы знаем из курса по Си с помощью ключевого слова const можно определять неизменяемые переменные, то есть, константы. Например, так:

const double pi {3.1415};

В С++ этот модификатор используется с теми же целями, но есть одно небольшое отличие при его использовании. Константная переменная должна быть инициализирована в момент ее объявления. То есть, запись, которая была допустима в языке Си:

const double pi;

в С++ приведет к ошибке при компиляции программы. Во всем остальном он работает так же, как и в языке Си. И это не удивительно, так как ключевое слово const перешло в Си из языка С++.

Вычисляемый тип (auto и decltype)

В языке С++, начиная со стандарта С++11, ключевое слово auto радикально изменило свое назначение. Если раньше в язык Си оно вводилось для явного обозначения автоматических переменных, то есть, тех, что располагаются в стековом фрейме, то теперь с его помощью можно объявлять переменные вычисляемого типа. О чем здесь речь? Давайте я покажу это на конкретном примере. С помощью ключевого слова auto мы можем объявлять переменные следующим образом:

    auto i = -100;      // тип int
    auto d = 76.98;     // тип double
    auto g = 0.55f;     // тип float
    auto h = 'f';       // тип char

Во всех случаях компилятор сначала вычисляет тип данных, которым инициализируется переменная, а затем, вместо auto подставляет этот тип. В итоге получаем в приведенном примере типы: int, double, float и char.

Или, могут быть более сложные выражения, например:

    auto i = 1 + 2 - 10;      // тип int
    auto d = 1 + i + 3.0;     // тип double
    auto g = 55u;     // тип unsigned int
    auto h = (short)10 + 100000UL;       // тип unsigned long

Здесь также вначале компилятор вычисляет тип инициализируемых данных и только потом вместо ключевого слова auto подставляет вычисленный тип.

Но, как всегда, здесь есть ряд важных нюансов. Давайте объявим следующие три переменных:

    int *ptr = nullptr;
    int k;
    int& lk = k;

и посмотрим, какой тип будет вычисляться компилятором с их использованием:

    auto t1 = k;        // int
    auto t2 = *ptr;     // int
    auto t3 = ptr;      // int *
    auto t4 = &ptr;     // int **
    auto t5 = lk;       // int

Пока использовались обычные переменные и указатели, все ожидаемо, но для ссылки получили обычный тип int, а не ссылку.

Далее, если перед типами прописать ключевое слово const:

    const int *ptr = nullptr;
    const int k = 0;
    const int& lk = k;

то  результат будет следующим:

    auto t1 = k;        // int
    auto t2 = *ptr;     // int
    auto t3 = ptr;      // const int *
    auto t4 = &ptr;     // const int **
    auto t5 = lk;       // int

В целом, ожидаемо. Компилятор старается найти наиболее общий тип при учете ссылок и модификаторов. Поэтому везде, где это допустимо, ключевое слово const отбрасывается при вычислении типа переменной. Очевидно, его нельзя отбросить для переменных t3 и t4, т. к. здесь объявляются указатели через константный указатель ptr. Во всех остальных случаях его можно не учитывать.

Однако если для нас важно сохранить полную идентичность типа переменной, например, ссылку оставить ссылкой, или не отбрасывать модификатор const, то при объявлении переменных можно воспользоваться еще одним новым ключевым словом decltype следующим образом:

    decltype(k) var1 = 1;   // const int
    decltype(ptr) var2;     // const int *
    decltype(lk) var3 = k;  // const int &

Конечно, здесь возникает вопрос, зачем это нужно? Разве не проще явно объявлять типы переменных, как это делалось до сих пор? На самом деле, есть ряд ситуаций, когда использование auto несколько облегчает написание программного кода. Часто это касается использования STL-коллекций со сложными шаблонными типами данных. Например, в программе можно объявить двусвязный список, содержащий числа, инициализировать его и перебрать обычным циклом for:

#include <iostream>
#include <list>
 
int main()
{
    std::list<short> dg = {-3, -2, 0, 2, 3};
 
    for(std::list<short>::iterator i = dg.begin(); i != dg.end(); ++i)
                   std::cout << *i << " ";
 
    return 0;
}

Смотрите, какой тип (std::list<short>::iterator) приходится прописывать у переменной i. Это не очень удобно. Как раз в подобных случаях проще прописать ключевое слово auto и компилятор сам автоматически подставит нужный тип переменной:

    for(auto i = dg.begin(); i != dg.end(); ++i)
                   std::cout << *i << " ";

И результат будет абсолютно такой же.

Конечно, злоупотреблять этим не стоит. Все же, явное обозначение чаще лучше неявного. И ключевое слово auto имеет ряд ограничений. Например, его нельзя (в ряде компиляторов) использовать при определении параметров функции:

void func(auto x)
{ }

Компилятор GCC выдает предупреждение, но другие могут приводить к ошибке. Поэтому вместо auto следует использовать или перегрузку функций, или определять шаблон функции.

Я не стану дальше углубляться в эту тему. Общей информации для понимания ключевых слов auto и decltype пока достаточно. В дальнейшем, когда в практике программирования вам действительно понадобится их использовать, значит, вы доросли до такого уровня, когда легко сможете более глубоко усвоить этот материал.

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме