Программирование на основе классов и шаблонов

Дополнительные сведения об ООП

Аладин Дмитрий Владимирович

iu5edu.ru/wiki/cpp2

План

  1. Виртуальные базовые классы при множественном наследовании. Разрешение неоднозначности имен и механизм доминирования при множественном наследовании. Неоднозначные преобразования.
  2. Понятие полиморфизма. Раннее и позднее связывание. Параметрический полиморфизм. Полиморфизм через наследование. Виртуальные функции.
  3. Традиционные механизмы обработки ошибок. Механизм исключений. Свёртка стека. Гарантии безопасности исключений.
  4. Приведение типов и разновидности выражений для приведения типов.

Этот вопрос не может больше терпеть!

Да кто такой этот ваш ПОЛИМОРФИЗМ?

center

Выйдем в интернет с этим вопросом

В нескольких статьях я прочитал вот такое:

Перегрузка методов поддерживает свойство полиморфизма, поскольку именно таким способом в C# реализуется главный принцип полиморфизма: один интерфейс — множество методов.

Пообщавшись с коллегами по этому поводу, большинство высказалось что перегрузка методов (overload) не имеет ничего общего с полиморфизмом. Так ли это?

@Qwertiy на Stackoverflow

В воздухе витает слабый, едва различимый запах...

center

О полиморфизме Стивен Прата

Полиморфизм функций — это удобное добавление C++ к возможностям языка С. В то время как аргументы по умолчанию позволяют вызывать одну и ту же функцию с различным количеством аргументов, полиморфизм функций, также называемый перегрузкой функций, предоставляет возможность использовать несколько функций с одним и тем же именем. Слово полиморфизм означает способность иметь множество форм, следовательно, полиморфизм функций позволяет функции иметь множество форм. ...

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

Стивен Прата. Язык программирования C++

Дежурное напоминание

  • Читайте умные книжки.
  • Хорошая книга может изменить вашу жизнь.
  • Не все книжки умные и полезные.
  • Бывают вредные книжки.

О полиморфизме ответ с stackoverflow

В общем случае полиморфизм это возможность обращаться одинаковым образом с сущностями разных типов. Или, чуть более научно, существование общего интерфейса ... у сущностей различных типов. ... К этому полиморфизму перегрузка методов относится самым прямым образом. ...

В ООП же под полиморфизмом обычно понимают гораздо более узкое понятие: возможность вызывать один и тот же метод у любого объекта заданного класса/интерфейса или любого его подкласса/реализации. ... К этому полиморфизму перегрузка действительно не имеет отношения.

@user181100 на Stackoverflow

Ad hoc [æd ˈhɒk] - латинская фраза, буквально означающая "к этому".

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

Еще один ответ с stackoverflow

Перегрузка функций это ad hoc полиморфизм.

(*Ad hoc, лат, ситуационно, на данный случай)

[Сайт, который нарушает закон РФ] приводит три вида полиморфизма в ... - ad hoc, параметрический и подтипы. ...

@Abyx на Stackoverflow

Нужно быть осторожным!

Главная проблема цитат в интернете в том, что люди сразу верят в их подлинность.

В.И. Ленин. Собрание сочинений том 12 Страница 77

О полиморфизме Грэди Буч и К°

Полиморфизм – это концепция в теории типов, в которой имя может обозначать экземпляры многих различных классов при условии, что они связаны некоторым общим суперклассом. Таким образом, любой объект, обозначаемый этим именем, способен по-разному реагировать на некоторый общий набор операций. При полиморфизме операция может быть реализована классами в иерархии по-разному.

Грэди Буч и др. Объектно-ориентированный анализ и проектирование с примерами приложений

В этой же книге коллектив авторов упоминает, что Кристофер Стрейчи, один из первых кто говорил о концепции полиморфизма (курс лекций 1967 года), выделял:

  • Специальный полиморфизм (aka ad hoc полиморфизм) определяет общий интерфейс для произвольного набора индивидуально заданных типов. Сюда относится: перегрузка процедур и функций, перегрузка операторов.
  • Параметрический полиморфизм: не указывает конкретные типы и вместо этого использует абстрактные символы, которые могут заменить любой тип. Сюда относится: обобщённая функция, обобщённое программирование.

Дополнить эту типизацию, можно с помощью определения полиморфизма от Грэди Буча и соавторов:

  • Подтипирование (aka полиморфизм подтипов и полиморфизм включения): когда имя обозначает экземпляры множества различных классов, связанных некоторым общим суперклассом.

А что сюда относится мы рассмотрим чуть позже.

Stay tuned for more! (Оставайтесь с нами, чтобы узнать больше)

Что попало в интернет, останется там навсегда!

Основано на материале от microsoft, @ashtanyuk и хендбук Яндекса.

Адаптированный материал публикуется с сохранением условий распространения.

Множественное наследование

Вопросы о главном:

  • Зачем оно?
  • Без него никак?
  • Так оно все ломает! Может не надо?

А что у других?

  • C#: множественное наследование невозможно. Но в то же время есть механизм применение интерфейсов этот "недуг" компенсирует.
  • Go: вы о чем вообще?
  • Java: множественное наследование невозможно. Но в то же время поддерживает множественную реализация интерфейсов. // догадайтесь, у кого C# подцепил эти механизмы?
  • Delphi: множественное наследование невозможно. Проблемы множественного наследования решаются интерфейсами. // может все-таки C# у Delphi позаимствовал это?

Несколько базовых классов в C++

Пример объявление класса для CollectionOfBook, производного от Collection и Book:

class Collection {};
class Book {};
class CollectionOfBook : public Book, public Collection {
  // New members
};

Порядок, в котором указываются базовые классы, не имеет значения, за исключением некоторых случаев, когда вызываются конструкторы и деструкторы.

Порядок вызова конструкторов и деструкторов

  • конструкторы: инициализация выполняется в порядке, в соответствии с указанными классами в базовом списке.
  • деструкторы: вызываются в обратном порядке классов, указанных в базовом списке.

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

P.S. Не напоминает про порядок вызова конструктора и деструктора по иерархии при простом наследовании?

Виртуальные базовые классы

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

Без использования виртуальных базовых классов:

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

Лучше сразу делать хорошо!

Если базовый класс определен как виртуальный базовый класс:

  • он может несколько раз выступать как косвенный базовый класс без дублирования элементов данных. Единственная копия его элементов данных совместно используется всеми базовыми классами, которые используют его как виртуальный базовый класс.

При объявлении виртуального базового класса virtual ключевое слово отображается в базовых списках производных классов:

// взаимное расположение ключевых слов public и virtual
// несущественно
class ZooAnimal {};

class Bear : public virtual ZooAnimal {
  // ...
};
class Raccoon : virtual public ZooAnimal {
  // ...
};

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

Пример возникновения дублирования

class Device {
  //...
  Link* next();
};

class Computer : public Device {
  //...
};

class Monitor : public Device {
  //...
};

class Laptop : public Computer, public Monitor {
  //...
};

Конкретная реализация:

class Device {
 public:
  Device() { cout << "Device constructor called" << endl; }
};

class Computer : public Device {
 public:
  Computer() { cout << "Computer constructor called" << endl; }
};

class Monitor : public Device {
 public:
  Monitor() { cout << "Monitor constructor called" << endl; }
};

class Laptop : public Computer, public Monitor {};

Использование:

int main() {
  Laptop Laptop_instance;
  return 0;
}

Консольный вывод:

Device constructor called
Computer constructor called
Device constructor called
Monitor constructor called

Использование виртуального наследования

class Device {
 public:
  Device() { cout << "Device constructor called" << endl; }
  void turn_on() { cout << "Device is on." << endl; }
};

class Computer : virtual public Device {
 public:
  Computer() { cout << "Computer constructor called" << endl; }
};

class Monitor : virtual public Device {
 public:
  Monitor() { cout << "Monitor constructor called" << endl; }
};

class Laptop : public Computer, public Monitor {};

Использование:

int main() {
  Laptop Laptop_instance;
  Laptop_instance.turn_on();
  return 0;
}

Консольный вывод:

Device constructor called
Computer constructor called
Monitor constructor called
Device is on.

Ключевое слово virtual гарантирует, что включена только одна копия подобъекта Device.

Неоднозначность имен при множественном наследовании

class A {
 public:
  unsigned a;
  unsigned b();
};

class B {
 public:
  unsigned a();  // класс A также имеет член "a"
  int b();       //  и член "b".
  char c;
};

class C : public A, public B {};

Учитывая предыдущие объявления классов, неясно ссылается ли b на b из A или B:

C *pc = new C;

pc->b(); // Ошибка компиляции: "C::b" не является однозначным

Компилятор определяет неоднозначности, выполняя тесты в указанном порядке:

  • Если доступ к имени неоднозначен, создается сообщение об ошибке.
  • Если перегруженные функции являются однозначными, они разрешаются.
  • Если доступ к имени нарушает разрешение доступа к членам, создается сообщение об ошибке.

Разрешение неоднозначности имен

Если выражение приводит к неоднозначности в результате наследования, его можно разрешить вручную, указав вместо данного имени имя класса:

C *pc = new C;

pc->B::a();

Доминирование

class A {
 public:
  int a;
};

class B : public virtual A {
 public:
  int a();
};

class C : public virtual A {};

class D : public B, public C {
 public:
  D() { a(); }  // Не двусмысленно. B::a() доминирует над A::a.
};

Доминирующее имя

Имя доминирует над другим именем, если оно определено в обоих классах и один класс является производным от другого.

Доминирующее имя — это имя в производном классе; оно используется тогда, когда в противном случае возникла бы неоднозначность.

Другие проблемы множественного наследования

  • Явные и неявные преобразования указателей и ссылок в типы класса могут приводить к неоднозначности.
  • Если используются виртуальные базовые классы, доступ к функциям, объектам, типам и перечислениям можно получить по путям множественного наследования. Так как имеется только один экземпляр базового класса, нет неоднозначности при доступе к этим именам.

Подробности здесь.

... а что Полиморфизм?

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

Вспомним, что помимо этого есть полиморфизмом ситуационный и параметрический.

"Хрестоматийный" пример параметрического полиморфизма

class Cat {
  std::string name;

 public:
  Cat(const std::string& n) : name(n) {}
  const std::string& GetName() const { return name; }
  std::string Voice() const { return "Meow!"; }
};

class Dog {
  std::string name;

 public:
  Dog(const std::string& n) : name(n) {}
  const std::string& GetName() const { return name; }
  std::string Voice() const { return "Woof!"; }
};

Использование:

void Process(const Cat& creature) {
  std::cout << creature.GetName() << ": " << creature.Voice() << "\n";
}

void Process(const Dog& creature) {
  std::cout << creature.GetName() << ": " << creature.Voice() << "\n";
}

int main() {
  Cat c("Tom");
  Dog d("Buffa");
  Process(c);  // Tom: Meow!
  Process(d);  // Buffa: Woof!
}

В этом коде сразу заметно дублирование. Как сделать хорошо?

Уберём дублирование с помощью шаблонов:

template <typename Creature>
void Process(const Creature& creature) {
  std::cout << creature.GetName() << ": " << creature.Voice() << "\n";
}

Код функции main не изменится. В каком-то смысле это уже полиморфизм: у нас есть разные классы с однотипным интерфейсом, а также набор одноименных перегруженных функций для их обработки.

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

Пример полиморфизма через наследование

class Animal {
  std::string name;

 public:
  Animal(const std::string& n) : name(n) {}
  const std::string& GetName() const { return name; }
  std::string Voice() const { return "Generic creature voice"; }
};

class Cat : public Animal {
 public:
  Cat(const std::string& n) : Animal(n) {}
  std::string Voice() const { return "Meow!"; }
};

class Dog : public Animal {
 public:
  Dog(const std::string& n) : Animal(n) {}
  std::string Voice() const { return "Woof!"; }
};

Использование:

template <typename Creature>
void Process(const Creature& creature) {
  std::cout << creature.GetName() << ": " << creature.Voice() << "\n";
}

int main() {
  Cat c("Tom");
  Dog d("Buffa");
  Process(c);  // Tom: Meow!
  Process(d);  // Buffa: Woof!
}

Все работает. Но где использование наследования?

Воспользуемся ссылкой на базовый класс:

void Process(const Animal& creature) {
  std::cout << creature.GetName() << ": " 
  << creature.Voice() << "\n";
}

int main() {
  Cat c("Tom");
  Dog d("Buffa");
  Process(c);  // Tom: Generic creature voice
  Process(d);  // Buffa: Generic creature voice
}

Наша программа компилируется, но животные внезапно теряют свой голос.

Где-то мы уже это видели:

class A {
 public:
  void hello() { std::cout << get_name() << std::endl; };

 protected:
  const char* get_name() { return "class A"; }
};

class B : public A {
 protected:
  const char* get_name() { return "class B"; }
};

int main() {
  A a = A();
  a.hello(); // class A

  B b = B();
  b.hello(); // class A
}

И чинили мы это с помощью virtual и override:

class A {
 public:
  void hello() { std::cout << get_name() << std::endl; };

 protected:
  virtual const char* get_name() { return "class A"; }
};

class B : public A {
 protected:
  const char* get_name() override { return "class B"; }
};

int main() {
  A a = A();
  a.hello(); // class A 

  B b = B();
  b.hello(); // class B
}

Позднее и ранее связывание

  • В предыдущем примере выбор функции Voice осуществлялся на этапе компиляции программы. Компилятор выбирал её исходя из формального типа аргумента creature — внутри функции это был const Animal&. Этот стандартный для C++ подход называется ранним связыванием. Имеется в виду, что ещё до запуска программы адрес выбранной функции Animal::Voice в памяти был заранее привязан к этому месту кода.
  • Виртуальные функции класса позволяют осуществить позднее связывание. В этом случае выбор подходящей функции будет происходить уже во время выполнения программы.

Пример виртуальной функции и полиморфизма

class Animal {
  // ...
 public:
  virtual std::string Voice() const { return "Generic creature voice"; }
};

class Cat : public Animal {
  // ...
 public:
  std::string Voice() const override { return "Meow!"; }
};

class Dog : public Animal {
  // ...
 public:
  std::string Voice() const override { return "Woof!"; }
};

Абстрактный класс и полиморфизм

В С++, класс в котором существует хотя бы один чистый виртуальный метод (pure virtual) принято считать абстрактным. Если виртуальный метод не переопределен в дочернем классе, код не скомпилируется.

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

class Animal {
 public:
  // ...

  virtual std::string Voice() const = 0;
};

Полиморфизм и контейнеры

int main() {
  std::vector<Animal> zoo;

  zoo.push_back(Cat("Tom"));
  zoo.push_back(Dog("Buffa"));

  Process(zoo[0]);  // Tom: Generic creature voice!
  Process(zoo[1]);  // Buffa: Generic creature voice!
}

Снова видим результат работы базовой функции!

Мы снова видим результат работы базовой функции Animal::Voice. Это произошло потому, что в векторе хранятся копии переданных в push_back объектов, и эти копии имеют тип Animal. То же самое бы случилось, если функция Process принимала бы свой параметр по значению, а не по константной ссылке.

Чтобы виртуальные функции заработали, в вектор можно положить указатели на Animal.

int main() {
  std::vector<Animal*> zoo;

  // Создадим пока что объекты на стеке
  Cat c("Tom");
  Dog d("Buffa");

  // Кладём в вектор адреса этих объектов
  zoo.push_back(&c);
  zoo.push_back(&d);

  // Для разыменования нужна звёздочка
  Process(*zoo[0]);  // Tom: Meow!
  Process(*zoo[1]);  // Buffa: Woof!
}

Мы положили в вектор адреса автоматических объектов на стеке.
Эти объекты разрушаются при выходе из блока.

Попробуем создать объекты в динамической памяти:

int main() {
  std::vector<Animal*> zoo;

  zoo.push_back(new Cat("Tom"));
  zoo.push_back(new Dog("Buffa"));

  Process(*zoo[0]);  // Tom: Meow!
  Process(*zoo[1]);  // Buffa: Woof!
}

center

Если в базовом классе есть виртуальные функции, и с ним предполагается полиморфная работа с созданием объектов в динамической памяти, то деструктор базового класса необходимо объявить виртуальным:

class Animal {
 public:
  // ...
  virtual ~Animal() {}
};

// ...

int main() {
  // ...
  for (Animal* animal : zoo) {
    delete animal; 
    // ~Cat() вызовется для кошки
    // ~Dog() - для собаки
  }
}

Традиционная обработка ошибок

Техники обработки ошибок:

  • прекратить выполнение;
  • возвратить значение "ошибка";
  • возвратить допустимое значение и оставить программу в ненормальном состоянии;
  • вызвать функцию обработки ошибок.

Вариант 1. Прекратить выполнение программы

int main() {
  int top = 0;
  assert(top != 0); // Assertion failed: (top != 0).
  return 0;
}

Другой пример:

char Stack::pop() {
  assert(top != 0);
  return store[--top];
}

Вариант 2. Возвратить ошибку

char Stack::pop() { 
  return top ? store[--top] : 0; 
}

P.S. Чувствуется магия чисел?

Результат каждого вызова должен проверяться на ошибку.

Или так:

enum tRC { OK, BAD_SIZE, OVERFLOW, UNDERFLOW };

tRC Stack::pop(char* c) {
  if (!top) return UNDERFLOW;
  *c = store[--top];
  return OK;
}

char Stack::pop(tRC* rc) {
  if (top) {
    *rc = OK;
    return store[--top];
  }
  *rc = UNDERFLOW;
  return 0;
}

Вариант 3. Оставить программу в ненормальном состоянии

char Stack::pop() {
  if (top != 0) {
    return store[--top];
  }
  g_Error = UNDERFLOW;
  return 0;
}

int main() {
  Stack stack;
  char c = stack.pop();
  if (g_Error != OK) {
    // error occured
  } else {
    // OK
  }
}

Вариант 4. Вызвать функцию обработки ошибок

void* new(size_t size) {
  void* p;
  for (;;) {
    if (p = malloc(size)) {
      return p;
    }
    if (!find_memory_somewhere()) {
      return 0;
    }
  }
}

Исключения

Механизм исключений (exceptions):

  • Генерация сообщения об ошибке (throw).
  • Перехват этих сообщений (catch).

try блок для заключения одной или нескольких инструкций, которые могут вызвать исключение.

В программе может одновременно существовать только одно исключение.

Генерация исключений

В языке C++ оператор throw используется для сигнализирования о возникновении исключения или ошибки (аналогия тому, когда свистит арбитр):

throw -1; // генерация исключения типа int
throw ENUM_INVALID_INDEX; // генерация исключения 
                          // типа enum
throw "Can not take square root of negative number"; // генерация
                                                     // исключения типа 
                                                     // const char* (строка C-style)
throw dX; // генерация исключения типа
          // double (переменная типа double, 
          // которая была определена ранее)
throw MyException("Fatal Error"); // генерация исключения с использованием 
                                  // объекта класса MyException

Поиск исключений

В языке C++ мы используем ключевое слово try для определения блока стейтментов (так называемого "блока try"). Блок try действует как наблюдатель в поисках исключений, которые были выброшены каким-либо из операторов в этом же блоке try, например:

try {
    // Здесь мы пишем стейтменты, которые будут генерировать следующее исключение
    throw -1; // типичный стейтмент throw
}

Обработка исключений

Обработка исключений — это работа блока(ов) catch. Ключевое слово catch используется для определения блока кода (так называемого "блока catch"), который обрабатывает исключения определенного типа данных:

catch (int a) {
    // Обрабатываем исключение типа int
    std::cerr << "We caught an int exception with value" 
              << a << '\n';
}

При передаче объекта оператору throw блок catch получает копию этого объекта. И эта копия существует только в пределах блока catch.

Пример обработки исключения

MyData md;
try {
  md = GetNetworkResource();
} catch (const networkIOException& e) {
  // Код, который выполняется при возникновении исключения типа networkIOException
  // ...
  cerr << e.what(); // Регистрируйте сообщение об ошибке в объекте исключения
} catch (const myDataFormatException& e) {
  // Код, который обрабатывает другой тип исключения
  // ...
  cerr << e.what();
}

MyData GetNetworkResource() {
  // ...
  if (IOSuccess == false) throw networkIOException("Unable to connect");
  // ...
  if (readError) throw myDataFormatException("Format error");
  // ...
}

Еще один пример обработки исключений

int ReadAge() {
    std::cin.exceptions(std::istream::failbit);
    int age;
    std::cin >> age;
    if (age < 0 || age >= 128) {
        throw WrongAgeException(age);
    }
    return age;
}
int main() {
    try {
        age = ReadAge();
    } catch (const WrongAgeException& ex) {
        std::cerr << "Age is not correct: " << ex.age << "\n"; return 1;
    } catch (const std::istream::failure& ex) {
        std::cerr << "Failed to read age: " << ex.what() << "\n"; return 1;
    } catch (...) { //  позволяет обработать любое исключение
        std::cerr << "Some other exception\n"; return 1;
    }
    // ...

Исключения стандартной библиотеки

Функции и классы стандартной библиотеки в некоторых ситуациях генерируют исключения особых типов. Все такие типы выстроены в иерархию наследования от базового класса std::exception. Иерархия классов позволяет писать обработчик catch сразу на группу ошибок, которые представлены базовым классом: std::logic_error, std::runtime_error и т. д. Например:

  • Потоки ввода-вывода могут генерировать исключение std::ios_base::failure.
  • Функция at у контейнеров std::array, std::vector и std::deque генерирует исключение std::out_of_range при некорректном индексе.

Традиционная обработка ошибок в конструкторах

  • Возвратить объект в "неправильном" состоянии;
  • Присвоить значение глобальной переменной;
  • Использовать функцию инициализации;
  • Осуществлять инициализацию при первом вызове функции-члена.

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

Пример исключения в конструкторах

class A {
  int m_stack;

 public:
  A(int i) try : m_stack(i) {
    throw -1; 
  } catch (int a) {
    // обработка исключения
    std::cerr << "Wow!" << a << std::endl;
  }
};

int main() {
  A a(1);
}

Копирующий конструктор подобен другим конструкторам:

  • может генерировать исключения
  • при этом объект не создается

Копирующее присваивание перед генерацией исключения должно убедиться, что оба операнда находятся в корректном состоянии

Свёртка стека

Генерировать исключения в конструкторах — совершенно нормальная практика. Однако не следует допускать, чтобы исключения покидали пределы деструкторов.

Свёртка стека (stack unwinding): вызываются деструкторы для всех созданных объектов в самой функции и в блоке try, как если бы они вышли из своей области видимости.

Механизм свёртки стека гарантирует, что деструкторы для всех созданных автоматических объектов или полей класса в любом случае будут вызваны.

Пример свёртки стека

#include <exception>
#include <iostream>
 
void f() {
    std::cout << "Welcome to f()!\n";
    Logger x;
    // ...
    throw std::exception();  // в какой-то момент происходит исключение
}
 
int main() {
    try {
        Logger y; // Logger(): 1
        f();      // Welcome to f()!
                  // Logger(): 2
                  // ~Logger(): 2
                  // ~Logger(): 1
    } catch (const std::exception&) {
        std::cout << "Something happened...\n";
        return 1;
    }
}

Пример свёртки стека из-за исключения в конструкторе

#include <exception>
#include <iostream>
 
class C {
private:
    Logger x;
public:
    C() {
        std::cout << "C()\n";
        Logger y;
        // ...
        throw std::exception();
    }
    ~C() {
        std::cout << "~C()\n";
    }
};
int main() {
    try {
        C c;
    } catch (const std::exception&) {
        std::cout << "Something happened...\n";
    }
}

Вывод программы:

Logger(): 1  // конструктор поля x
C()
Logger(): 2  // конструктор локальной переменной y
~Logger(): 2  // свёртка стека: деструктор y
~Logger(): 1  // свёртка стека: деструктор поля x
Something happened...

Деструктор самого класса C не вызывается, так как объект в конструкторе не был создан.

Свёртка стека работает только с автоматическими объектами.

P.S. Примеры с исключениями динамических объектов см.
в Яндекс Хэнбуке.

Гарантии безопасности исключений

  • Гарантия отсутствия сбоев. Функции с такими гарантиями вообще не выбрасывают исключений.
  • Строгая гарантия безопасности. Исключение может возникнуть, но от этого объект нашего класса не поменяет состояние: количество элементов останется прежним, итераторы и ссылки не будут инвалидированы и т. д.
  • Базовая гарантия безопасности. При исключении состояние объекта может поменяться, но оно останется внутренне согласованным, то есть, инварианты будут соблюдаться.
  • Отсутствие гарантий. Это довольно опасная категория: при возникновении исключений могут нарушаться инварианты.

Функции класса, которые гарантируют отсутствие сбоев, следует помечать ключевым словом noexcept:

class C {
public:
    void f() noexcept {
        // ...
    }
};

С одной стороны, noexcept позволяет компилятору генерировать более эффективный код.

С другой — эффективно обрабатывать объекты таких классов в стандартных контейнерах.

Приведение типов

В языке С++ существует 4 разновидности выражений для приведения типов:

  • dynamic_cast
  • static_cast
  • reinterpret_cast
  • const_cast

P.S. reinterpret [riːɪnˈtɜːprɪt] - интерпретировать

dynamic_cast

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

void f(Employee* pEmp) {
  Programmer* pp = dynamic_cast<Programmer*>(pEmp);
  if (pp) {
    pp->team();
  }
}
void g(Employee& re) {
  try {
    Programmer& rp = dynamic_cast<Programmer&>(re);
  } catch (bad_cast) {
    //...
  }
}

static_cast

Используется для преобразований родственных типов (число в число):

int* p = static_cast<int*>(malloc(100));

enum tColor { RED, GREEN, BLUE };
tColor c = static_cast<tColor>(2);

double d = 2.56;
int i = static_cast<int>(d);

reinterpret_cast

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

IO_device* p = reinterpret_cast<IO_device*>(0XffA01);

void* p = allocate_memory_for_programmer();
Programmer* p = reinterpret_cast<Programmer*>(p);

const_cast

Используется для отмены const:

void f(const Worker* pw) {
  Worker* pp = const_cast<Worker*>(pw);

  pp->new_name("Vasya");
}

Использование:

const Worker w("Ivan", "Ivanov");

f(*w);  // неопределнное поведение

Вопросы?

center

If not, just clap your hands!

Например, std::vector<C> при реаллокации будет использовать конструктор перемещения класса C, если он помечен как noexcept. В противном случае будет использован конструктор копирования, который может быть менее эффективен, но зато позволит обеспечить строгую гарантию безопасности при реаллокации.