в

Kazan Dev Alliance

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

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

August 2008 - Posts

  • 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 (Как избежать дублирования в константных и неконстантных функциях-членах)
  • Questions from C++ examine

    В зимнем семестре мне довелось в КГУ читать спец. курс по С++. Предполагалось, что студенты ранее могли не изучать C или С++.

    В зимнем семестре удалось рассмотреть базовые конструкции языка и классы. Шаблоны были перенесены на весенний семестр. Также одна лекция была посвящена макросам.

    Программу зимнего семестра можно скачать тут.

     Экзамен делилился на 2 части. Первая и главная часть - практическая. Нужно было реализовать класс и протестировать его. Это задание было в двух вариантах. Первым вариантом было реализовать класс DynamicArray - массив, при необходимости расширяющий свои размеры (похожий на std::vector). Вторым вариантом - реализовать класс String (строка). Практическое задание было достаточно подробно описано. Было небольшое preview, в котором описывались проблемы, которые разрабатываемый класс должен решить, был приведен интерфейс класса и, наконец, были приведены некоторые инварианты, которым класс должен удовлетворять. Последнее, в частности, должно было снизить вероятность возникновения спорных вопросов, когда, к примеру, я считаю, что некоторая функциональность работает неверно, а студент считает, что верно.

    Практическое задание (C++) - вариант 1 (PDF скачать)

    Практическое задание (C++) - вариант 2 (PDF скачать)

     

    Второй маленькой частью экзамена были простенькие (где-то даже забавные) вопросы по С++, которые не требуют большого исследования. Размещаю эти вопросы и жду ответов в комментах ;-)

    1. Пусть задан некоторый встроенный тип Т. Всегда ли результат sizeof(T) зависит от реализации?

    2. Не используя инструкций выбора напишите функцию Round, которая принимает аргумент типа float и возвращает целое, являющееся результатом округления аргумента. Гарантируется, что аргумент
      неотрицателен.

      Правила округления обычные: если дробная часть меньше 0.5, то выбирается наибольшее целое, не превышающее аргумента. В противном случае -  наименьшее целое, которое не меньше аргумента.

    3. Приведите примеры (>1), когда отсутствие инициализатора в объявлении вызовет ошибку компиляции.

    4. Имеется следующий код:
         
          char* pString = "C++";
          char val = *(pString + 3);
          char* p = pString + 4;
          pString[0] = 'D';

      Определено ли поведение во второй, третьей и четвертой строках листинга. Если да, то каков результат их выполнения, а именно, что будет содержаться в val после выполнения второй строки, в p после
      выполнения третьей и в pString[0] после выполнения четвертой строки.

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

      Для данного типа T тип T* является "указателем на T". То есть переменная типа T* может хранить адрес объекта типа T. Из этого определения следует, что если в качестве типа T взять T*, то переменная типа T** является указателем на T* и может хранить адрес переменной типа T*.
      Рассмотрим это на примере:
         int main()
         {
             int x = 1;
             int* pX = &x;
             int** ppX = &pX;
             return 0;
         }

      Переменная ppX имеет тип "указатель на указатель на целое" и, следовательно, может хранить адрес объекта, являющегося указателем на целое.

      Выделение памяти под массив указателей на T (по аналогии) происходит с помощью операции new T*[], тип возвращаемого значения такой операции --- T**.
      Двумерный массив можно считать одномерным массивом, каждый элемент которого сам является массивом.

      Задача. Используя рассмотренные сведения, написать код выделения памяти под двумерный целочисленный массив из m строк и n столбцов (m и n неизвестны на момент компиляции).

      Написать код освобождения памяти для полученного массива.

    6. Дан следующий код:

         #include<iostream>
         using namespace std;
         enum Number
         {
            One = 1,
            Two,
            Three
         };

         int main()
         {
            Number a = ...;
            switch(a)
            {
               case One:
                 cout<<"One";
                 break;
               case Two:
                 cout<<"Two";
               case Three:
                 cout<<"Three";
                 break;
               default:
                 cout<<"Don't know";
                 break;
          }
          return 0;
        }
      Строка Numbers a = ...; - это псевдокод. а инициализируется некоторым значением. Что выведется на экран если a равно One, Two, Three? Переписать приведенный код с использованием инструкции выбора if вместо switch.

    7. Напишите функции PrefixIncrement и PostfixIncrement которые увеличивают на единицу значение целого числа, переданного в качестве параметра. Обеспечьте, чтобы первая функция имела поведение аналогичное префиксному инкременту, а вторая - постфиксному.

    8. Почему следующий код вызовет ошибку компиляции:
         
          int x = 1;
          int* p = &x++;

      Скомпилируется ли такой код:
         
          int x = 1;
          int* p = &(++x);

    9. Почему следующий код вызовет неопределенное поведение:

          int* pInt = new int(12);
          cout<<*pInt;
          delete[] pInt;

    10. Сколько раз выполнится следующий цикл

          for (int i = 10; i--; i > 10)
          {
              //Do something
          }
       
      Свой ответ объяснить.
      Подсказка. Не торопитесь с ответом.

    11. Имеется следующий код:

         class A
         {
             A(const A& copy)
             {
                 //Do something
             }
           public:
             A() {}
         };

        
      class B
         {
             A a1;
             A a2;
         };

      Все ли конструкторы, которые при необходимости генерируются компилятором неявно, могут быть им корректно созданы в случае с классом B.
      В частности, будет ли код компилироваться, если к указанному коду добавить main:


         int main()
         {
             B b;
             B c = b;
             return 0;
         }

    12. Пусть имеется некоторый пользовательский бинарный оператор, определенный в виде  глобальной функции. Как гарантировать, чтобы первый его операнд был модифицируемым(неконстантным) lvalue? (Гарантировать в том смысле, что если это не так, то возникнет ошибка компиляции). Как гарантировать, что его первый операнд
      может быть константным объектом? Как это гарантировать, если оператор определен в виде метода класса?

    13. Имеется следующий код:
         class A{ };

         class B : protected A { };

         class C : public B
         {
         public:
            void SomeMethod()
            {
               A* pA = this;
            }
         };

         int main()
         {
             B b;
             A* pA = &b;
             return 0;
         }

      Какие строки этого кода вызовут ошибку компиляции и почему?

      Изменилась ли бы ситуация и, если да, то как, если при наследовании B от A использовалось бы закрытое, открытое наследование?

    14. Имеется следующий код:
         class A
         {
         protected:
             int val;
         public:
             A(int val)
             {
                 this->val = val;
             }
         };

        
      class B : A
         {
         public:
             B()
             {
                 val = 3;
             }
         };

      Почему код не компилируется при таким образом определенном конструкторе класса B. Измените конструктор B так, чтобы код успешно компилировался.

      Будет ли код компилироваться и почему, если конструктор B определить следующим образом:

         class B : A
         {
         public:
             B() : val(3) { }
         };

     

    Скачать вопросы в PDF

© 2007 Kazan Developers Community and Post`s Authors