С++: Бросок производного класса по ссылке не работает, если ловить базовый класс

Я хочу исключить собственные исключения из базового класса Exception. Существует print виртуального метода, которая будет перезаписана подклассами. Я только поймаю тип Exception& и использую print чтобы получить конкретную ошибку. Проблема заключается в том, что как только я бросаю ссылку на подкласс, он транслируется так, как если бы это был базовый класс.

Вот пример:

#include <iostream>
using namespace std;

class Exception
{
    public:
        virtual void print()
        {
            cout << "Exception" << endl;
        }
};

class IllegalArgumentException : public Exception
{
    public:
        virtual void print()
        {
            cout << "IllegalArgumentException" << endl;
        }
};

int main(int argc, char **argv)
{
    try
    {
        IllegalArgumentException i;
        Exception& ref = i;

        cout << "ref.print: ";
        ref.print();

        throw ref;
    }
    catch(Exception& e)
    {
        cout << "catched: ";
        e.print();
    }
}

Результат этого примера:

ref.print: IllegalArgumentException
catched: Exception

Использование ссылки должно приводить к методу print из используемого производного класса. Внутри блока try ссылка использует его. Почему Exception& Except Exception& похоже на Exception& IllegalArgumentException и как я могу получить это поведение?

Следующий код, похоже, делает то, что он должен:

try
{
    IllegalArgumentException i;
    Exception* pointer = &i;

    throw pointer;
}
catch(Exception* e)
{
    cout << "pointer catched: ";
    e->print();
}

но не может ли указатель стать недействительным вне области действия блока try? Тогда было бы рискованно делать это, и если я распределю память в куче, чтобы обойти эту проблему, я не могу нести ответственность за удаление внутри блока catch. Итак, как бы вы решили проблему?

Ответ 1

throw неявно копии и, следовательно, срезы. Цитирование С++ 11, §15.1/3:

Выражение-выражение инициализирует временный объект, называемый объектом исключения, тип которого определяется удалением всех cv-квалификаторов верхнего уровня из статического типа операнда броска и настройки типа из "массива T " или "массив", функция возвращает T "в" указатель на T "или" указатель на функцию, возвращающую T ", соответственно. Временной является значением lvalue и используется для инициализации переменной, указанной в соответствующем обработчике. Если тип объекта исключения будет неполным типом или указатель на неполный другой тип, чем (возможно, резюме квалифицированное) void программу плохо сформировано. За исключением этих ограничений и ограничений на сопоставление типов, упомянутых в 15.3, операнд throw обрабатывается точно как аргумент функции в вызове или операнде оператора return.

Я видел несколько кодовых баз, которые работают вокруг этого, бросая указатели на исключения, а не на объекты напрямую, но лично я просто пересмотрю вашу "необходимость", чтобы сделать это в первую очередь.

Ответ 2

То, что вы видите, называется нарезкой.

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

Таким образом, C++ определен для выполнения нарезки на объекте. Скопирована только часть базового класса, а тип "понижен" и базовому классу.

Ответ 3

Когда вы бросаете объект, копия этого объекта делается в другое место, так что может возобновиться сканирование стека, и копия может быть передана обработчику исключений (либо по ссылке, чтобы никакие дополнительные копии не были сделаны, либо по значению, создающим вторую копию),

Вы можете проверить, что копия сделана, если вы сделаете исключение не копируемым. Вы больше не сможете бросать объекты такого типа.

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

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

Это должно печатать "catch: IllegalArgumentException".

try
{
    IllegalArgumentException i;

    throw i;
}
catch(Exception& e)
{
    cout << "caught: ";
    e.print();
}

Ответ 4

Вы можете использовать указатель вместо ссылки, чтобы избежать нарезки:

int main(int argc, char **argv)
{
    try
    {
        IllegalArgumentException* pIAE = new IllegalArgumentException();
        throw pIAE;
    }
    catch(IllegalArgumentException* i)
        {
            i->print();
    }
}

Захваченный указатель указывает на тот же объект, потому что конструктор копирования не называется неявно.