Const cast c как использовать
Перейти к содержимому

Const cast c как использовать

  • автор:

объясните по const_cast

Скажите вот читаю про const_cast. написано const_cast позволяет лишить статуса const или volatile переменную . Но такое ощущение , что не совсем ведь так буквально: потому что рассматривать её как полноценную переменную я не могу :

 const int i = 0; const_cast(&i); i = 555; 

но могу вот так :

 const int i = 0; int* j = const_cast(&i); *j = 555; 

то есть , если объявлено const int i = 0;, то после const_cast i можно менять только при помощи указателя ?

Отслеживать
задан 27 сен 2020 в 13:21
Андрей Гуренков Андрей Гуренков
415 3 3 серебряных знака 10 10 бронзовых знаков

Оно изменяет тип выражения, но не изменяет тип исходного объекта. Тип объекта в С/С++ всегда неизменяемый. Соответственно операции, которые можно проводить с результатом любого каста должны учитывать настоящий тип объекта. Попытка модифицировать объект с const квалификатором, как в *j = 555; , является неопределенным поведением.

27 сен 2020 в 13:39
Модификация во втором примере приводит к UB en.cppreference.com/w/cpp/language/const_cast
27 сен 2020 в 13:39

так если «попытка модифицировать объект с const квалификатором, как в *j = 555;, » это неопределенное повдение как пишет user7860670 , то какой смысл от const_cast, если первый мой пример бракует компилятор, а второй это UB ? честно говоря я не могу понять зачем надо константу делать переменной, ну да ладно. как тогда адекватно менять значение у константы после применения const_cast ?

27 сен 2020 в 13:54

@АндрейГуренков смысл const_cast : если возникла необходимость в нём, то понять, что проблема в дизайне/API, и, при возможности, поменять их; либо, если нельзя поменять, использовать const_cast , удостоверившись, что не будет модификации, и рассказав об этом в комментарии

27 сен 2020 в 14:08

2 ответа 2

Сортировка: Сброс на вариант по умолчанию

Присваивать объекту, который был определён как const — это в любом случае UB (undefined behavior). const_cast ни коим образом не снимает константность с самого объекта, он только «возвращает» переданное ему значение с нужным типом (это выражение также может быть «присваиваемым» (l-value)).

На практике const_cast используется довольно редко. Обычно это относительно грязный хак, чтобы передать константный объект в функцию, которая заведомо его не меняет (но принимает неконстантную ссылку/указатель) или, наоборот, изменить объект заведомо созданные как неконстантный, но переданный по константной ссылке/указателю. В тривиальном варианте это выглядит как-то так:

void foo (const int &i) < const_cast(i) = 2; > void bar (int *i) < std::cout // . int i=0; const int ci=2; foo(i); bar(const_cast(&ci)); 

По непосредственному вопросу: с точки зрения синтаксиса можно привести константную переменную к неконстантной ссылке (само собой так делать не стоит):

const int i = 0; const_cast(i) = 2; // !! UB !! 

Const и оптимизации в C

Сегодня на /r/C_Programming задали вопрос о влиянии const в C на оптимизацию. Я много раз слышал варианты этого вопроса в течении последних двадцати лет. Лично я обвиняю во всём именование const .

Рассмотрим такую программу:

void foo(const int *); int bar(void) < int x = 0; int y = 0; for (int i = 0; i < 10; i++) < foo(&x); y += x; // this load not optimized out >return y; >

Функция foo принимает указатель на const, который обещает от имени автора foo что значение x не будет изменено. Может показаться, что компилятор может предположить, что x всегда равен нулю, а значит и y тоже.

Однако, если мы посмотрим на ассемблерный код, генерируемый несколькими разными компиляторами, то увидим, что x загружается при каждой итерации цикла. Вот что выдал gcc 4.9.2 с -O3, с моими комментариями:

bar: push rbp push rbx xor ebp, ebp ; y = 0 mov ebx, 0xa ; цикл по переменной i sub rsp, 0x18 ; allocate x mov dword [rsp+0xc], 0 ; x = 0 .L0: lea rdi, [rsp+0xc] ; вычисляем &x call foo add ebp, dword [rsp+0xc] ; y += x (не оптимизировано?) sub ebx, 1 jne .L0 add rsp, 0x18 ; deallocate x mov eax, ebp ; возвращаем y pop rbx pop rbp ret

clang 3.5 (с -fno-unroll-loops) выдал примерно то же самое, только ebp и ebx поменялись местами, и вычисление &x вынесено из цикла в r14 .

Неужели оба компилятора не способны воспользоваться этой полезной информацией? Разве если foo изменит x , это не будет undefined behavior? Как ни странно, ответ — "нет". В этой ситуации, это будет абсолютно верным определением foo .

void foo(const int *readonly_x) < int *x = (int *)readonly_x; // cast away const (*x)++; >

Важно помнить, что const — не значит константный. Возьмите себе на заметку, что это неправильное название. Это не инструмент для оптимизации. Он нужен чтобы информировать программиста — а не компилятор — как инструмент, помогающий поймать определенный класс ошибок во время компиляции. Мне нравится, когда его используют в API потому что он сообщает, как функция будет использовать свои аргументы, или как вызывающий должен обращаться с возвращенными указателями. Обычно он недостаточно строгий, чтобы изменить поведение компилятора.

Несмотря на то, что я сказал, иногда компилятор может воспользоваться const для оптимизации. В спецификация C99, в §6.7.3¶5, есть одно предложение об этом:

Если сделана попытка изменить объект объявленный с модификатором const через использование lvalue без модификатора const, поведение неопределенно.

Исходный x был без модификатора const, поэтому это правило не применилось. И нет никакого правила против приведения к не- const типу, чтобы изменить объект который сам по себе не const . Это значит, что вышеприведённое поведение foo это не undefined behavior для этого вызова. Обратите внимание, что неопределенность foo зависит от того, как она была вызвана.

Одним изменением в bar я могу сделать это правило применимым, позволяя оптимизатору поработать.

 const int x = 0;

Компилятор теперь может предположить, что изменение x в foo — это undefined behavior, и потому никогда не происходит. Как бы то ни было, в основном так оптимизатор C рассуждает о ваших программах. Компилятор может предположить, что x никогда не изменяется, позволяя ему оптимизировать и загрузку в каждой итерации, и y .

bar: push rbx mov ebx, 0xa ; переменная цикла i sub rsp, 0x10 ; allocate x mov dword [rsp+0xc], 0 ; x = 0 .L0: lea rdi, [rsp+0xc] ; вычисляем &x call foo sub ebx, 1 jne .L0 add rsp, 0x10 ; deallocate x xor eax, eax ; возвращаем 0 pop rbx ret

Загрузка исчезает, y исчезает, и функция всегда возвращает ноль.

Любопытно, что спецификация позволяет компилятору пойти еще дальше. Он может разместить x где-нибудь вне стека, даже в read-only памяти. Например, он может произвести такую трансформацию:

static int __x = 0; int bar(void)

Или на x86-64 (-fPIC, модель малой памяти), где получается избавиться от еще нескольких инструкций:

section .rodata x: dd 0 section .text bar: push rbx mov ebx, 0xa ; переменная цикла i .L0: lea rdi, [rel x] ; вычисляем &x call foo sub ebx, 1 jne .L0 xor eax, eax ; возвращаем 0 pop rbx ret

Ни clang, ни gcc не заходят так далеко, видимо потому, что это более опасно для плохо написанного кода.

Даже со специальным правилом о const rule, используйте const для себя и своих товарищей-программистов. Пусть оптимизатор сам для себя решает, что константно, а что нет.

Const cast c как использовать

Вот один из забавных примеров, в котором, в зависимости от компилятора и его оптимизатора, две переменные могут попасть на один адрес, чего по сути быть не может, и, конечно, это только иллюзия, а не действительность. В любом случае, в нижепоказанном коде идёт попытка снятия константности с изначально константного значения, что однозначно есть неопределённое поведение.

using namespace std ;
const int i = 100 ;
const int * ptr = &i;
int * pi = const_cast < int * >( &i); //Снятие const с переменной, на которую указывает pi
( * pi ) = 200 ; //Изменение переменной, которая обозначена const
cout < < ( void * ) &i << '\t' << i << '\n'; //На экране можно увидеть один адрес cout < < ( void * ) pi < < '\t' < < * pi < < '\n' ; //Но разные значения

В данной ситуации const_cast снимает константность при преобразовании типа. Поскольку переменная i была изначально константной, её адрес имел тип const int* . Благодаря const_cast с типа адреса была снята константность, и его стало возможно поместить в переменную, на которую указывает b , где эта переменная есть не const целое число . Значение i на самом деле изменяется. То, что на экран выводится её предыдущее значение (в некоторых, но не во всех компиляторах) — результат некорректной работы оптимизатора, который ошибочно предположил, что объявленная как const переменная i изменяться не будет, и подставил то значение, которое указано ещё при инициализации.

В любом случае — изменение переменной, которая инициализирована как const — это неопределённое поведение работы программы.

  • Иногда нужно изменять значение, которое изначально не квалифицировано как константное, но в какой-нибудь функции, в ходе выполнения цепочки функций, успело заболеть свойством константности.

Алёна C++

Бывают случаи, когда строгое придерживание константности неудобно. Объект может оставаться логически константным ("logically const"), но при этом его физическая константность ("physically const") может быть нарушена. Пример: в неком классе на основании данных класса по очень сложному и долгому алгоритму считается некая величина. Хорошо бы эту величину закэшировать.

class CFoo
int cachedValue;
bool bCached;
.
public:
int calculate() const
//долгое вычисление
cachedValue = . ; //ошибка
//нельзя делать присвоение данным класса в константной функции
>
.
>;

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

class CFoo
mutable int cachedValue;
mutable bool bCached;
.
public:
int calculate() const
if(bCached) return cachedValue;
//долгое вычисление
cachedValue = . ; //все в порядке
bCached = true;
>
.
>;

Подобное кэширование данных - это классический пример использования mutable .
mutable означает, что спецификатор const , примененный к классу, следует игнорировать. По стандарту только данные класса могут быть mutable .
Признак правильного использования mutable : если при доступе к данным через интерфейс класса все выглядит так, будто в классе ничего не менялось, то можно использовать mutable .
Еще один классический пример использования mutable - это синхронизация доступа к данным. Допустим, у нас есть класс, содержащий переменную, хранящую некоторое значение ( data ), и объект, отвечающий за синхронизацию доступа в многопоточных приложениях ( sync_obj ). Также есть две функции, отвечающие за доступ к данным: set_data и get_data . Функция get_data должна быть константной, она же не меняет данные класса, но как ей тогда залочить доступ к данным? Объявить sync_obj как mutable .

class mutable_test
int data;
mutable sync sync_obj;
public:

void set_data (int i)
sync_obj.lock ();
data = i;
sync_obj.unlock ();
>

int get_data () const
sync_obj.lock ();
int i = data;
sync_obj.unlock ();
return i;
>
>;

Если mutable вполне законное средство убрать константность, у него есть классические применения, то const_cast - это всегда некий хак. Используется обычно с библиотеками, которые не являются const -корректными. С помощью const_cast можно убрать только const , навешанный на объект, который изначально константным не является. Пример:

int i;
const int * pi = &i;
// *pi имеет тип const int,
// но pi указывает на int, который константным не является
int* j = const_cast (pi);

Если же попробовать убрать const с объекта, который на самом деле const , результатом будет undefined behaviour.

struct Foo < int val; >;

int main()
const Foo obj = < 1 >;
const_cast(&obj)->val = 3; // undefined behaviour
return obj.val;
>

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *