в

Kazan Dev Alliance

Казанское Сообщество Разработчиков Программного Обеспечения

Персональный блог Александра Демченко

const_cast и устранение дублирования кода

const_cast

С точки зрения С++ если имеется некоторый тип T (без квалификаторов const или volatile), то тип const T (volatile T, const volatile T) является отдельным типом. Однако, во многих случаях можно использовать один вместо другого.

Например:

const T f()
{
return T(); // T не является const T
}

С другой стороны такая взаимозаменяемость не всегда возможна, например:

int t = 12;
const int& const_ref_t = t;
int& ref_t = const_ref_t; // ошибка, присваивание не возможно

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

Однако, конечно, программист может взять на себя ответственность, и явно осуществить небезопасное преобразование из константного объекта в неконстантный.

Например, понятно, что во втором примере const_ref_t можно без опасения преобразовать к int&, поскольку объект, на который ссылается const_ref_t реально не является константой. В С++, как известно, за снятие и установление константности (или volatile) на объект отвечает оператор приведения const_cast. const_cast выглядит как шаблонная функция, параметром шаблона которой является тип, к которому осуществляется преобразование, а обычным параметром - объект, который нужно преобразовать. Вот как можно было бы использовать const_cast во втором примере:

int t = 12;
const int& const_ref_t = t;
int& ref_t = const_cast<int&>(const_ref_t); //осуществляется явное преобразование
//из const T& к T&.

В результате такого преобразования, ref_t так же, как и const_ref_t ссылается на t. Поэтому ref_t++ влечет увеличение t на единицу.

Несколько модифицируем пример, объявив t константой.

const int t = 12;
const int& const_ref_t = t;
int& ref_t = const_cast<int&>(const_ref_t); //Снятие константности, ОК
ref_t++; //Попытка изменить константу - неопределенное поведение.

Приведение типов здесь пройдет успешно, но попытка модифицировать по полученной ссылке значение влечет неопределенное поведение, так как это по ссылке хранится дейтствительно константа.

 При этом поскольку поведение в указанной ситуации не определено, исполнив такой код, Вы не обязательно получите ошибку времени выполнения или какое-нибудь диагностическое сообщение. Я, например, откомпилировав этот код на компиляторе MS VS 2005 и запустив его, получил, что после ref_t++ значение константы не изменилось. При этом, конечно, поведение от запуска к запуску может меняться. В силу указанных причин такую ошибку сложно отловить в процессе отладки.

Использование const_cast для устранения дублирования кода.

 Представим, что мы решили написать простую обертку для встроенных массивов C++:

class Array
{
int* pArray;
int length;

public:
//Methods
};

Теперь нам хочется удобно обращаться к элементам массива - читать и изменять их. Поэтому мы решаем определить оператор []:

int& operator[](int index)
{
if ( (index < 0) || index >= length)
throw std::exception("Incorrect index");
return pArray;
}

Теперь, имея такой оператор мы можем читать и записывать элементы:

Array arr(12);
int val_3 = arr[3]; //Читаем   
arr[3] = 14; //Изменяем

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

const Array arr(12);
int val_3 = arr[3]; //Ошибка компиляции,
//вызов
неконстатного метода для константого объекта.

 

Для того, чтобы позволить читать элементы констатного массива, мы вынуждены создать константный аналог оператора []:
const int& operator[](int index) const
{
if ( (index < 0) || index >= length)
throw std::exception("Incorrect index");
return pArray;
}

Теперь все нормально. Если оператор [] вызывается для констатного объекта, то вызовется его константный вариант. При этом, попытка изменить элемент константного массива вызовет ошибку компиляции, так как будет попытка присвоить значение по константной ссылке.

const Array arr(12);
int val_3 = arr[3]; //ОК, вызывается operator[] const
arr[3] = 12; //Ошибка компиляции - вызывается operator[] const,
//но
по константной ссылке, которую он возвращает   нельзя менять объект

 Итак, окончательно имеем класс:

class Array
{
int* pArray;
int length;

public:
int& operator[](int index)
{
if ( (index < 0) || index >= length)
throw std::exception("Incorrect index");
return pArray[index];
}
const int& operator[](int index) const
{
if ( (index < 0) || index >= length)
throw std::exception("Incorrect index");
return pArray[index];
}
};

 Здесь мы конечно замечаем известный антипаттерн Copy and Paste.  Одну и ту же функциональность нам пришлось просто скопировать и вставить в константный аналога оператора []. Если здесь это не так много - всего 3 строчки, то, конечно, в реальных ситуациях, этот общий код может быть гораздо больше.

 Конечно, можно попробовать избавиться от Copy and Paste, например, просто вызвав из константного метода неконстантный. Так конечно сделать нельзя, поскольку неконстантный метод может изменять объект и компилятор это понимает. Однако, никакого вреда не будет, если мы из неконстантного метода вызовем констатный.

Код мог бы выглядеть примерно так:

int& operator[](int index)
{
const int& ref_elem = /* call operator [] const */
return const_cast<int&>(ref_elem); //Безопасно,
//так
как реально объект не является константой
}

Перед возвратом из функции нужно преобразовать полученную константную ссылку к неконстантной, поскольку мы планируем, что посредством неконстантного оператора [] пользователь может захотеть изменить элемент массива.

Вопрос здесь в том как вызвать именно константный метод. Прямого способа указать компилятору, что мы хотим вызвать именно этот метод, нет. Однако, он сам это сделает, если увидит, что вызов [] применен к константному объекту. В таком случае, самое время вспомнить о const_cast и "навесить" константность на this. Итоговый код будет таким:

int& operator[](int index)
{
return const_cast<int&>( const_cast<const Array*>(this)   -> operator[index] );
}
 
 
Дополнительные материалы:
Операторы преобразования С++
Константные функции-члены
Антипаттерны
Книга Скотта Мэйерса "Эффективное использование С++. 55 верных советов улучшить структуру и код
ваших программ". Правило 3 (Как избежать дублирования в константных и неконстантных функциях-членах)

Комментарии

Нет комментариев

Оставить комментарий

(required)  
(optional)
(required)  

© 2007 Kazan Developers Community and Post`s Authors