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 и указатели»В общем случае 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 против constexpr
Заголовок раздела «const против constexpr»Значением 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-методы
Заголовок раздела «const-методы»const можно применить к методу — это просто распространяется на указатель this*.
Синтаксис такой:
class Demo{ int state;
private: int readState() const { return this->state; }};
// Что по смыслу означает примерно следующее:int readState(const Demo* const this){ return this->state;}const_cast
Заголовок раздела «const_cast»Это полезно, когда вы знаете, что операция не будет изменять объект, хотя он не объявлен 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
Заголовок раздела «std::unique_ptr»std::unique_ptr — шаблонный тип, представляющий динамически выделенную память,
которая следует RAII.
std::shared_ptr
Заголовок раздела «std::shared_ptr»auto и decltype
Заголовок раздела «auto и decltype»Раздел про полиморфизм при обсуждении лямбд отчасти требует auto,
поэтому я решил объяснить это здесь.
decltype
Заголовок раздела «decltype»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"; // не компилируется