CV-специфицированные значения



В предыдущей статье мы начали говорить о категориях выражений. Я привел примеры, в которых, на мой взгляд, достаточно легко определить принадлежность к той или иной категории. На их основе компилятор проверяет ограничения, оценивая правомерность написанного кода.



В С++ есть способы наложить дополнительные ограничения на действия над данными. Например, запретить пользователю изменять значения с помощью ключевого слова const. Вероятно, что это как-то должно повлиять на категорию выражения, не так ли?



Стандарт языка использует термин «cv-специфицированный» для описания типов с квалификаторами const и volatile. Пример:

// Запрещаем изменять значение

const int a = 1;



// Запрещаем кешировать значение в регистрах

volatile int b = 2;



// Комбинация двух предыдущих

const volatile int c = 3;




Про const вы, наверняка, уже знаете. Вот о квалификаторе volatile мы еще не говорили, от нас тут нужна хорошая подводка... В рамках этой темы достаточно знать, что volatile переменные всегда должны лежать в оперативной памяти (т.н. запрет на кеширование значений; запрет на оптимизацию).



Стоит подумать, для каких категорий выражений такие квалификаторы будут приносить пользу? Ограничить возможность изменять значение или запретить кеширование логично для lvalue:

// Returns const reference 

// to access for reading only

const std::string& foo() { return lvalue; }



// Accepts const reference

// to access for reading only

void bar(const std::string &lvalue)



// Spawns read-only value

const int magic = 3;




Несмотря на то, что переменной magic нельзя присвоить новое значение, она всё ещё принадлежит категории lvalue:

const int magic = 3; 



// lvalue rvalue

magic = 5;

// ~~^~~

// Error: assignment of

// read-only variable 'magic'




Нельзя сказать, что неизменяемый тип является rvalue. Нет, это просто другое свойство, которое накладывает ограничения на действия над данными. Однако, такие выражения могут быть использованы только как rvalue. Т.е. могут быть только прочитаны, скопированы. Это позволяет ослабить ограничения в таких ситуациях:

const int &d = 2; // Ok




Это может показаться странным, ведь d должна ссылаться на какое-то значение в памяти. Да и в остальных случаях это работает иначе:

int  a = 1; // Ok

int &b = a; // Ok

int &c = 2; // Error!




В отношении с все вполне логично и понятно — нельзя сослаться и изменять память, которая не выделена под неё. Почему же всё работает для d? Тут мы видим, что эти данные запрещено изменять и нет запрета на кеширование. Следовательно, при соблюдении этих ограничений дальше, выражение может быть использовано только как rvalue, т.е. без перезаписи значений в памяти. Компилятор либо подставит это значение по месту требования, либо создаст вспомогательную локальную копию. В общем случае, ни логика, ни работоспособность приложения не нарушится. Живой пример



Априори, в совокупности с volatile квалификатором такой трюк не прокатит из-за требований volatile:

const volatile int &f = 4; // Error!




Конечно, неприятный казус может случиться, если мы попытаемся обойти это ограничение — применим const_cast<int&>, т.е. осознанно выстрелим себе в ногу снимем ограничение на изменение данных. По сути, это прямое игнорирование ограничений, которые по каким-то причинам вводились в код проекта ранее. И вот желательно их выяснить и обойти иначе, а не использовать такие грязные трюки. Короче, это UB!



Наглядный пример, почему использование этого каста является дурным тоном в программировании на C++: https://compiler-explorer.com/z/qK1z3q89q. В общем, на языке переживших новогодние праздники: «главное не смешивать»



У меня есть офигенная кружка! Обожаю пить из неё кофе, пока пишу эти посты.



#cppcore #memory #algorithm