Перейти к содержимому

const, умные указатели и auto

const означает, что значение чего-либо нельзя изменить.

Можно объявлять const-переменные — их значение изменить не получится.

const int a = 5;
a = 10; // ошибка времени компиляции

Это относится к любым типам, включая пользовательские.

struct T
{
int value;
};
// ...
const T a{5};
a.value = 10; // нельзя, потому что a.value — часть памяти переменной `a`.
a = { 50 }; // нельзя менять и память целиком.

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

struct Demo
{
int value;
int expensiveToCopy[1000];
};
void stuff(const Demo& demo)
{
int value = demo.value; // читать разрешено.
demo.value = 5; // писать нельзя.
demo = { 6 }; // перезаписывать тоже нельзя.
}

В общем случае const применяется к указателям только на один уровень вглубь. Например, const int* или int const* означает, что нельзя изменить память, на которую указывает указатель, но сам указатель — можно, а int* const означает, что нельзя изменить сам адрес указателя, но память, на которую он указывает, — можно. Аналогично const int* const означает, что нельзя изменить ни то, ни другое.

Для указателей глубиной в несколько уровней const нужно указывать для каждого уровня.

void func(
int* p, // изменяемый адрес, изменяемый объект
const int* p1, // изменяемый адрес, неизменяемый объект
int* const p2, // неизменяемый адрес, изменяемый объект
const int* const p3 // неизменяемый адрес, неизменяемый объект
)
{
int local = 8;
*p = 10;
p = &local;
// *p1 = 10;
p1 = &local;
*p2 = 10;
// p2 = &local;
// *p3 = 10;
// p3 = &local;
}

Для указателей на объекты с указателями const применяется только на первом уровне. На память вложенного указателя он не распространится.

struct Demo
{
int someValue;
int* pointer;
};
void func(const Demo* demo)
{
int local = 8;
// demo->someValue = 10;
// demo->pointer = &local;
*demo->pointer = 10; // а это разрешено.
}

Значением const-переменной, использованной с известными во время компиляции константами примитивного типа, можно пользоваться в контексте времени компиляции. Например, в качестве размера статического массива или как параметр шаблона.

#include <array>
static inline const size_t arrayLength = 10;
int arr[arrayLength];
std::array<int, arrayLength> arr2;

Не получится, если использовать, скажем, структуру:

#include <array>
struct Test
{
size_t value;
};
// Это будет доступно только во время выполнения,
// потому что это не примитивный тип.
static inline const Test arrayLength = { 10 };
int arr[arrayLength.value]; // нельзя
std::array<int, arrayLength.value>; // нельзя

Но можно добиться, если заменить в этом примере const на constexpr. constexpr делает переменную константой времени компиляции.

constexpr также можно использовать с функциями, чтобы сообщить компилятору, что их можно вычислять во время компиляции. В C++11 и новее внутри constexpr-функции допустимы вызовы и обычных функций — при условии, что во время вычисления константы таких вызовов не происходит. Это очередная кроличья нора, так что здесь я остановлюсь.

const можно применить к методу — это просто распространяется на указатель this*. Синтаксис такой:

class Demo
{
int state;
private:
int readState() const
{
return this->state;
}
};
// Что по смыслу означает примерно следующее:
int readState(const Demo* const this)
{
return this->state;
}

Это полезно, когда вы знаете, что операция не будет изменять объект, хотя он не объявлен const. Правомерный сценарий — например, функция, дающая доступ к n-му элементу массива.

struct Buffer
{
int* elements;
size_t length;
};
int& getRefAtIndex(Buffer& buff, size_t index)
{
return buff.elements[index];
}
// Приходится реализовать тот же метод для const Buffer.
// Результат должен быть const, потому что мы хотим, чтобы константность Buffer
// была транзитивной для элементов буфера.
const int& getRefAtIndex(const Buffer& buff, size_t index)
{
// Заметьте, приходится предоставлять в точности ту же реализацию.
return buff.elements[index];
}
int main()
{
Buffer buff{new int[5], 5};
const Buffer buffConst{new int[5], 5};
// Вызывает неконстантную перегрузку
getRefAtIndex(buff, 0);
// Вызывает const-перегрузку
getRefAtIndex(buffConst, 0);
return 0;
}

Чтобы не реализовывать одно и то же тело функции дважды, можно сделать так, чтобы первая, неконстантная функция вызывала вторую, const-версию. Это означает, что параметр придется привести к const, а результат привести обратно к non-const. Как видно, реализация действительно корректна, поскольку этот const влияет только на тип возврата.

int& getRefAtIndex(Buffer& buff, size_t index)
{
// сначала приводим тип буфера
const Buffer& constBuff = buff;
// теперь вызываем const-функцию
const int& element = getRefAtIndex(constBuff, index);
// теперь убираем `const`
return const_cast<int&>(element);
}

Считается хорошей практикой, приводящей к более надежному коду, следовать const-корректности: то есть всегда применять const там, где это уместно.

Если вы понимаете RAII, то и умные указатели дадутся легко.

std::unique_ptr — шаблонный тип, представляющий динамически выделенную память, которая следует RAII.

Раздел про полиморфизм при обсуждении лямбд отчасти требует auto, поэтому я решил объяснить это здесь.

decltype — это оператор, который возвращает тип выражения:

struct T { float x; };
int main()
{
int a{1};
decltype(a) b{2}; // int b{2};
b += a;
T t{1.0f};
decltype(t) t1{2.0f}; // T t1{2.0f};
decltype(t.x) x{3.0f}; // float x{3.0f};
decltype(T::x) x1{4.0f}; // float x1{4.0f};
return 0;
}

Его нельзя использовать в момент объявления.

decltype(a) a = 5; // не компилируется
decltype(5) a = 5; // а вот это нормально

Выражение может быть и сложнее:

#include <algorithm>
decltype(std::max(5, 6) - static_cast<float>(5)) a = 5.0f; // float a = 5.0f;

auto — это ключевое слово, которое позволяет компилятору вывести тип переменной из типа выражения, которое ей присваивается.

auto x = y; эквивалентно decltype(y) x = y;.

🤓 Не совсем: auto выводит тип по правилам вывода параметров шаблонов — верхнеуровневые const и ссылки отбрасываются, а массивы и функции «распадаются» на указатели, — тогда как decltype(y) сохраняет объявленный тип y. На простых типах вроде int разницы нет.

int a1 = 5;
auto a2 = 5; // int a2{5};

auto можно использовать и как тип возврата функций. В следующем примере компилятор выведет, что тип возврата f — int:

auto f()
{
return 5;
}

Чтобы было предельно ясно: переменные, объявленные с типом auto, все равно статически типизированы. Присвоить им другой тип нельзя.

auto a = 5;
a = 6; // нормально
a = "Hello"; // не компилируется