понедельник, октября 03, 2005

mutable и const_cast

Бывают случаи, когда строгое придерживание константности неудобно. Объект может оставаться логически константным ("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<int *> (pi);
Если же попробовать убрать const с объекта, который на самом деле const, результатом будет undefined behaviour.
struct Foo { int val; };

int main()
{
const Foo obj = { 1 };
const_cast<Foo *>(&obj)->val = 3; // undefined behaviour
return obj.val;
}
Ссылки по теме:
comp.lang.c++.moderated "usefulness of const_cast"
comp.lang.c++.moderated "legitimate use of const_cast"
comp.lang.c++.moderated "mutable: why it's in the language"


Technorati tag:

8 коммент.:

Анонимный комментирует...

int calculate() const
{
static int bCached=0;
static int cachedValue=0;
if(bCached) return cachedValue;
//долгое вычисление
..............


?

Alena комментирует...

static int bCached=0;
.......


Это не будет работать корректно, если будет создано несколько экземпляров CFoo.
Но даже если я точно знаю, что будет всего один экземпляр (CFoo синглтон, например), я не буду использовать такое решение. Потому что оно чревато целым букетом проблем. Проблемы доступа к закэшированному значению из других функций. Очень вероятно, что кроме подсчета должна быть некая функция, которая будет сбрасывать cachedValue, когда оно устарело.
Данные класса получаются размазанными по коду, хотя ничто не мешает их хранить в классе. В итоге код тяжелее читать.
При наследовании придется определять переменные заново.

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

dalex комментирует...

По моему все проще простого с mutable: он попросту указывает что элемент класса (со спецификатором mutable) не влияет на "константность" класса. И не надо долгих примеров ;-)

FM комментирует...

Если mutable вполне законное средство убрать константность, у него есть классические применения, то const_cast - это всегда некий хак.

Не подскажете, как убрать const_cast в следующем случае? Я пробовал писать у указателей mutable, тогда появляется ошибка несовместимости по присваиванию указателя и указателя на const.

//
// Класс указателя с подсчётом ссылок на объект.
// Когда счётчик ссылок становится равен нулю, объект удаляется.
//
template<typename T>
class SimpleSmartPointer
{
private:

SimpleSmartPointer *_prev;
// предыдущий элемент кольцевого двунаправленного списка

SimpleSmartPointer *_next;
// следующий элемент кольцевого двунаправленного списка

T *_object;
// собственно указатель на предмет

...

//
// Заполнить указатель по образцу существующего
//
void FillPointerBy(SimpleSmartPointer<T> &copy) {
if (copy._object == NULL) {
_object = NULL;
_prev = NULL;
_next = NULL;
} else {
_object = copy._object;
_prev = &copy;
_next = copy._next;
_prev->_next = this;
_next->_prev = this;
}
}
public:
...
//
// Конструктор копирования - для первой инициализации объекта
// const тут совершенно не по делу, но без него ругается std::pair,
// используемая в std::map.
//
SimpleSmartPointer(const SimpleSmartPointer<T> &copy) {
FillPointerBy(const_cast<SimpleSmartPointer<T>&>(copy));
}
...
};

Алёна комментирует...

// Конструктор копирования - для первой инициализации объекта
// const тут совершенно не по делу, но без него ругается std::pair,
// используемая в std::map.
//


AFAIR, это классический вид конструктора копирования, с const. Модифицировать объект, с которого снимается копия, не есть хорошо.

FM, предлагаю продолжить переписку мылом, это будет удобнее.

Дмитрий комментирует...

Это как раз классический вид конструктора копирования _без_ const.

Так как при копировании мы меняем source то const там совершенно не нужен.

Не приведен код работы с std::pair, но вероятно просто некорректно определен тип контейнера.

Анонимный комментирует...

class A {
int i;
void foo() const {
A * temp = const_cast(this);
temp->i = 10;
}
}

это корректный код?

Анонимный комментирует...

Подаправим:
A * temp = const_cast(this);
и будет корректный код. Т.е. он будет работать корректно, но всё равно не рекомендуеться его так использовать.
ПС: можно было кастануть переменную i и получилось бы тоже самое.