11.3. Средства времени выполнения и исключения

    11.3. Средства времени выполнения и исключения Язык Си по своей природе ориентирован на «чистую» компиляцию. Это означает, что в программном коде отсутствует исходная информация об объектах языка (переменных, функциях, классах), их именах и свойствах. Однако в Си++ этот принцип «немного нарушается». Иногда это делается «по делу», с целью реализовать необходимые механизмы технологии ООП (исключения, полиморфизм), иногда – для контроля программы над типами объектов, с которыми она в данный момент работает.

    С этой целью транслятор включает в программный код статические данные, касающиеся описаний классов (как минимум, их имена) и их взаимосвязей по системе наследования. Доступ к этим данным возможен через специальный класс, а сам механизм носит название RTTI – run time type identification (идентификация типов во время выполнения).

    Идентификация типов и их преобразование Прежде всего, программа может проверить, к какому классу относится объект. Понятно, если речь идет о прямом именовании объекта, то в этом нет необходимости, имя переменной однозначно соотносится с ее типом. Другое дело – ссылка или указатель. Преобразование типов указателей от производного класса к базовому и наоборот (см. 11.2) являются важным технологическим элементом наследования и полиморфизма (при передаче объектов по ссылке происходят аналогичные преобразования ссылочного типа).

    На каждый класс, используемый в программе, компилятор создает объект типа type_info. Операция typeid возвращает ссылку на тот объект, к которому относится выражение или тип, стоящие в скобках. Класс type_info имеет метод name, который возвращает константную строку с именем типа.

    C
    
    //-------------------------------------------------113-01.cpp
    
    // Идентификация типов и их преобразование
    
    #include <typeinfo.h>
    
    class A{ public:  virtual    void F(){ }};
    
    class B: public A{};
    
    class C: public A{};
    
    class D {};
    
    void main(){
    
                A aa; B bb;
    
                if (typeid(A)!=typeid(B)) puts("A!=B");
    
                puts(typeid(A).name());              // Выведет "class A"
    
                puts(typeid(B).name());               // Выведет "class B"
    
                puts(typeid(aa).name());            // Выведет "class A"
    
                puts(typeid(bb).name());              // Выведет "class B"
    
                A *p=&bb;                                 // Преобразование указателя к БК
    
                puts(typeid(p).name());              // Выведет "class A*"
    
                puts(typeid(*p).name()); // Выведет "class B"
    
                A *p1=dynamic_cast<A*>(&bb);  // Преобразование ПК к БК (B->A)
    
                B *p2=dynamic_cast<B*>(p1);    // Преобразование БК к ПК (A->B)
    
                C *p3=dynamic_cast<C*>(p1);    // Преобразование БК к ПК (A->С)
    
                D *p4=dynamic_cast<D*>(p1);    // Преобразование ??? (A->D)
    
                if (p3==NULL) puts("p3==NULL"); } // Выведет "p3==NULL"
    
                if (p4==NULL) puts("p4==NULL"); } // Выведет "p4==NULL"

    Операция вида dynamic_cast<D> отличается от обычного преобразования типа указателя, имеющей вид (D), тем, что она использует средства RTTI для проверки такого преобразования «в динамике», т.е. во время выполнения. Если таковое невозможно, то она возвращает NULL-указатель.

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

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

    • TEXT
         источник ошибки обычно находится в самой Си-программе: это программный код, который проверяет условие, при выполнении которого программа не может дальше выполняться корректно. Например, система динамического распределения памяти не может выделить участка требуемого размера;
    • TEXT
         источник ошибки находится обычно «глубоко внутри» цепочки вызовов функций (в ООП – цепочке вызовов методов). При этом реакция на ошибку (обработка) производится, как правило, «на более высоком уровне»;
    • TEXT
         вызываемая функция «нижнего уровня» обычно использует в возвращаемом  результате оригинальное значение (например, NULL-указатель) для обозначения факта возникновения ошибки;
    • TEXT
         вызывающая функция проверяет результат на наличие «особого» значения и пытается исправить ситуацию.

    рис. 112-1. Обработка ошибок традиционными средствами

    Даже в традиционной схеме видно, что источник и обработчик ошибки разнесены в программе. Вызывающая функция, в свою очередь, может попытаться исправить ошибку, а может и передать соответствующее значение «вышестоящему начальству», т.е. вверх по цепочке вызовов. В идеале хотелось бы, чтобы поток команд, связанный с обработкой ошибок, существовал синтаксически независимо от потока команд, связанного с возвратом обычных «правильных» значений. Такой механизм в Си++ носит название исключений.

    Синтаксически исключение состоит из тех частей, использующих служебные слова throw,try,catch:

    • TEXT
         генератор исключений, конструкция вида throw выражение; Она синтаксически и по механизму исполнения похожа на оператор return;
    • TEXT
         обработчик ошибок  вида catch(формальный параметр){…}. Своим синтаксическим видом он напоминает безымянную функцию с единственным формальным параметром. Действительно в теле обработчика может быть любой программный код и допустимо использование этого формального параметра, который может быть передан как по значению, так и по ссылке;
    • секции защищенного кода вида try {защищенный код} catch(){}…catch(){}, которая представляет собой блок, предваренный служебным словом try со следующим за ним одним или несколькими обработчиками.

    рис. 112-1. Обработка ошибок и помощью исключений

    Механизм обработки исключений можно охарактеризовать следующими словами: при генерации исключения моделируется последовательное выполнение оператора return в цепочке вызовов функций (или методов), пока программа не выйдет в программный код, защищенный соответствующим обработчиком. Рассмотрим этот процесс более подробно:

    • TEXT
         оператор throw в качестве результат возвращает значение выражения. Оно может быть каким угодно (константой, переменной, объектом), но для нас важен его тип. Т.е. исключения могут быть различными и иметь разные типы. Каждый тип исключения  имеет свои обработчики и обрабатывается независимо от других типов;
    • TEXT
         обработка исключения состоит в моделировании выполнения оператора return. При выходе из текущей функции (метода) выполняются все действия, связанные с разрушением локальной среды выполнения процесса: уничтожаются локальные переменные и формальные параметры, а если они являются объектами, то для них вызываются деструкторы;
    • TEXT
         выполнение последовательности выходов из функций (методов) производится до тех пор, пока программа не окажется в защищенной секции, имеющий обработчик исключений того же самого типа, что и возвращаемое значение. Например, исключение throw 5 требует обработчика catch(int n){…}, а исключение throw string(“1111”)- обработчика catch(string s){…};
    • TEXT
         при обнаружении обработчика необходимого типа происходит выполнение его тела. При этом значение, возвращаемое исключением, становится его формальным параметром.

    Последовательность выходов в поисках нужного обработчика называется «разматыванием стека». Если обработчик не найден, то исполнительная система завершает выполнение программы.

    Таким образом, программа, использующая исключения имеет два выхода из защищенной секции. При отсутствии ошибок (без генерации исключений) программа выполняет «естественным порядком», возврат происходит в ту же самую точку защищенного кода, где происходил вызов. Обработчики при отсутствии ошибок являются абсолютно прозрачными. При возникновении ошибки возврат идет «своим путем» в тело нужного обработчика, после чего программа покидает защищенный код.

    Передача данных между генератором и обработчиком исключений осуществляется через параметр, который может быть в том числе и объектом. В этом случае в операторе throw можно использовать безымянные объекты (см.10.1). Есть еще несколько упрощений синтаксиса. Если обработчик исключений не использует параметр, то в catch можно использовать только тип без имени, например catch(int). И наконец, обработчик «всех без исключения необслуженных исключений» имеет вид catch(…){}

    Замечание: столь замысловатые действия обработчика исключений требуют запоминания во время выполнения программы значительного числа подробностей, касающихся «истории» выполнения программы: локальных объектов, наличия обработчиков для исключений заданного типа и т.п.. Естественно, что все это сохраняется в стеке и обслуживается механизмами, аналогичными RTTI.

    А теперь посмотрим, как технологически использовать исключения. Обработка ошибки может включать в себя различные схемы, которые реализуются программистом внутри обработчика:

    • TEXT
         самый простой вариант – игнорирование ошибки получается автоматически при отсутствии в защищенном коде обработчика требуемого типа: исключение «передается наверх» по цепочке возвратов;
    • TEXT
         коррекция ошибки предполагает выполнение в программном коде обработчика тех действий, которые формируют «исправленное» значение результата защищенного кода;
    • TEXT
         повторное выполнение операции с другими параметрами предполагает, что вся конструкция try-catch заключается в цикл. Обработчик устанавливает в нем условие продолжения (повторения), а также новые значения, а сам защищенный код при успешном выполнении операции производит завершение цикла (явно через break или через тот же признак).

    Еще одну иллюстрацию к принципу динамического «разматывания стека» можно получить из рекурсивного программирования (см. 7.3). При обнаружении первого подходящего решения в процессе рекурсивного перебора вариантов может быть элементарно сгенерировано исключение. Результат поиска может быть передан в виде параметра исключения:

    //------------------------------------------------------------------------

    struct REZ{ // Класс объекта – результата

    TEXT
            REZ(…){…}       // Конструктор объекта

    };

    void F(…){ // Рекурсивная функция поиска первого подходящего

    TEXT
            if (…) return;      // Условия невозможности продолжения поиска
    
            if (…) throw REZ(…);
    
                                    // Найден первый подходящий - исключение
    
            for (…)F(…);      // Продолжение поиска – рекурсивные вызовы
    
            }

    void main(){

    TEXT
        try {                      // Первоначальный вызов рекурсивной функции

    F(…); // в секции защищенного кода

    } catch(REZ x){ …вывод результата x… }

    }

    Замечание: в «тотальных» системах ООП исключения являются единственным механизмом, при помощи которого стандартные библиотечные классы сообщают об ошибках, при работе программы с их объектами. В Си++ «тяжелое наследие» традиционного программирования не позволяет этого сделать. Поэтому, например, стандартный ввод-вывод, математика и т.п. обходятся без них.

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

    //-----------------------------------------------------113-02.cpp

    //-----------------------------------------------------

    // Общий код для тестов 1,2

    class A{

    int val;

    public: A(int n=0) { val = n; }

    TEXT
            A operator/(A &two){
    
                        if (two.val==0) throw *this;
    
                        A t=*this;
    
            t.val/=two.val;
    
                        return t; }
    
            A operator-(A &two){
    
                        if (val < two.val) throw val - two.val;

    A t=*this;

    TEXT
            t.val-=two.val;
    
            return t; }};

    //---------------------------------------------------1

    void main(){

    A a(10),b(0),c(12),e,f,g;

    int m=0;

    try { e=a/b;

    TEXT
            f=a-c;
    
            }

    catch (int n) { m=n; }

    catch (A n) { g=n; }}

    //---------------------------------------------------2

    void main(){

    A a(10),b(0),c(12),e,f,g;

    int m=0;

    try { f=a-c;

    TEXT
            e=a/b;
    
            }

    catch (int n) { m=n; }

    catch (A n) { g=n; }}

    //-----------------------------------------------------

    // Общий код для тестов 3,4

    class A{

    TEXT
      int val;
    
      int top;

    public:

    TEXT
      A(int n=10) { top = n; val=0; }
    
      A operator=(int v){
    
            if (v >= top) throw v;
    
            val=v;
    
            return *this; }
    
      A operator+(A &two){
    
            if (val + two.val >=top) throw *this;
    
            A t=*this;
    
            t.val+=two.val;
    
            return t; }};

    //---------------------------------------------------3

    void main(){

    A a(10),b(15),c(20);

    int m=0;

    try { a=8;

    TEXT
            b=18;
    
            c=25;
    
            a=a+c;
    
            b=a+c;
    
            }

    catch (int n) { c=10; }

    catch (A n) { c=1; }}

    //---------------------------------------------------4

    void main(){

    A a(10),b(15),c(20);

    int m=0;

    try { a=8;

    TEXT
            b=12;
    
            c=15;
    
            a=a+c;
    
            b=a+c;

    }

    catch (int n) { c=10; }

    catch (A n) { c=n; }}

    //-----------------------------------------------------

    // Общий код для тестов 5,6

    class string{

    char *str;

    void load(char *s) { str=strdup(s); }

    void add(char s) { str=(char)realloc(str,strlen(str)+strlen(s)+1); strcat(str,s); }

    int find(char s) { char p=strstr(str,s);

    TEXT
           if (p==NULL) throw str;
    
             return p-str; }

    int cmp(string &t) { return strcmp(str,t.str); }

    public:

    string() { load(""); }

    string(char *s) { load(s); }

    string(string &t){ load(t.str); }

    ~string() { delete []str; }

    char operator [](int n) {

    TEXT
                           if (n>=strlen(str)) throw *this;
    
                           return str[n]; }

    int operator [](char *s) { return find(s); }};

    //---------------------------------------------------5

    void main(){

    string s1("abcdefg"),s2("01234");

    char *p="***", c1='#',c2='#';

    int n1=0,n2=0;

    try { c1=s1[3];

    TEXT
            c2=s2[10];
    
            n1=s1["cde"];
    
            n2=s2["abc"];
    
            }

    catch (char *s) { p=s; }

    catch (string s){ c1=s[1]; }}

    //---------------------------------------------------6

    void main(){

    string s1("abcdefg"),s2("01234");

    char *p="***", c1='#',c2='#';

    int n1=0,n2=0;

    try {

    TEXT
         n1=s1["cde"];
    
         n2=s2["abc"];
    
         c1=s1[3];
    
         c2=s2[10];
    
         }

    catch (char *s) { p=s; }

    catch (string s){ c1=s[1]; }}