7. Обработка исключительных ситуаций в С++.

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

Перегрузка унарных и бинарных операторов. Перегрузка оператора = : копирование, перенос. Перегрузка операторов ->, *, []. Перегрузка операторов ++, --. Операторы приведения типов.

Начало

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

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

Возникают две проблемы:

  • Создавая объект, мы вызываем его конструктор (создаем конкретный объект конкретного типа) - проблема подмены. Дело в том, что конструктор является не методом объекта, а методом класса, он не может быть виртуальным как любой другой метод.

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

Эти две проблемы мы будем решать в дальнейшем.

Таким образом, объектно-ориентированный подход решает одну из серьезнейших проблем структурного программирования - модификацию без изменения написанного кода.

Обработка исключительных ситуаций

Недостатки обработки ошибок в структурном программировании

Недостатки:

  • Если где-то возникает ошибка в коде, мы вынуждены "протащить" ее через все уровни абстракции/иерархии до того места, пока мы не сможем обработать эту ошибку.

  • Весь код насыщен непрерывными проверками. Обработка ошибки совмещена вместе с кодом.

Идея в том, чтобы при возникновении какой-либо ошибки передавать управление непосредственно в то место, где мы можем обработать ее, не "протаскивая" ее через много уровней.

Обработка ошибок в C++

Инструкции обработки ошибок:

  • Инструкция try - заворачиваем в неё блок кода, в котором может произойти ошибка, и берём его под контроль.

  • Если в блоке try возникает исключительная ситуация, мы можем перейти на обработчик catch. Обработчики идут непосредственно после блока try.

  • Генерируем исключительную ситуацию, используя инструкцию throw.

Обработчиков может быть несколько. Обработчик принимает объект какого-либо типа. Соответственно, если объект, который мы передаем в throw или создаем throw, может быть принят обработчиком (тип совпадает или приводится к этому типу), вызывается этот обработчик. Если не может, он передается следующим обработчикам до тех пор, пока не выберется блок catch. Если на этом уровне ни один из catch не перехватил этот объект, то это передается на более высокий уровень. Ошибка может никем не перехватиться, в этом случае программа "падает".

Мы можем перехватить любые исключительные ситуации, используя catch с 3 точками. Этот обработчик должен быть в конце списка, иначе он перехватит любую ситуацию, и другие обработчики ниже не будут работать.

try
{
    // код
    ---> throw <объект>  // Возникла исключительная ситуация и мы выходим на обработчик
    // код
}
catch  (<тип>& <объект>) // Если объект совпадает с типом или приводится к этому типу,
{						 // вызывается обработчик
    // код
}
catch  (<тип>& <объект>) // иначе переходим к следующему обработчику
{						 // до тих пор пока не найдем нужный catch
    // код
}
catch  (...) // Перехват любых исключительных ситуаций
{
    // код
}

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

Плюсы:

  • Мы не "протаскиваем ошибку"

  • Вся обработка сводится в одно место

Задачи обработчика

Задачи, возлагаемые на обработчик:

  1. Выдать сообщение пользователю или записать его в log-файл. В сообщении нужно писать:

    • Время ошибки

    • Где она произошла

    • Что за ошибка

    • Возможно, данные, которые привели к этой ошибке

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

Проблема с динамической памятью при обработке исключительных ситуаций

В C++, перейдя на обработчик, мы не можем вернуться в место возникновения ошибки (все временные объекты будут уничтожены). Это проблема.

Предположим, у класса А есть метод f(). Если мы динамически выделили память:

try
{
    A* obj = new A; // Динамически выделили память под объект
    obj->f();       // Если внутри функции f() произошла ошибка и мы вышли на обработчик,
    delete obj;     // происходит утечка памяти
}

Если при вызове метода f() возникает исключительная ситуация и мы выходим на какой-то из обработчиков, объект obj не удаляется. Происходит утечка памяти.

Решение проблемы с помощью noexcept или throw()

Можно запретить методу обрабатывать исключительную ситуацию.

Два варианта решения проблемы:

void A::f() noexcept // Первый способ
void A::f() throw()  // Второй способ

С noexcept при возникновении исключительной ситуации вызывается функция terminate(). Функция terminate() приводит к тому, что будут вызываться все деструкторы только временных объектов в порядке, обратном их созданию.

Со throw() результат непредсказуем, это старый синтаксис, который лучше не использовать.

  • Если пишем noexcept без параметров аналогичен noexcept(True) - это говорит о том, что данный метод не должен обрабатывать исключительную ситуацию.

  • Если пишем noexcept(False) или throw(...), то этот метод может обрабатывать все исключительные ситуации, как и в случае если ничего не пишем.

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

Можно указать, какую исключительную ситуацию метод может обрабатывать.

void A::f() throw(<тип>)

Этот метод может обрабатывать все исключительные ситуации с этим типом и производными от него.

Исключительная ситуация в деструкторе

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

Несколько обработчиков одной исключительной ситуации

Ситуацию можно "протащить". Например, обработчик принял объект, но не смог полностью обработать ситуацию. Мы можем "прокинуть" ее до следующего обработчика, который может принять этот объект:

catch(<тип>& <объект>)
{
    // код
    throw; // Объект прокидывается дальше
}

Объект принимается везде по ссылке => объект должен создаваться исключительно при вызове исключительной ситуации. Время жизни этого объекта ограничивается этим блоком.

Исключительная ситуация в разделе инициализации объекта

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

Чтобы было корректно, конструктор можно "обернуть" в try и для него сделать обработчик:

Array::Array() try <раздел инициализации>
{
    // тело конструктора
}
catch(<тип>& <объект>)

Пишем это не при объявлении, а при инициализации конструктора.

Использование exception при обработке ошибок

Наша задача - cделать так, чтобы при модификации мы не исправляли написанный код. Хочется, чтобы при работе с ошибками был такой же подход. При модификации могут возникать новые ошибки и мы должных обрабатывать их, не изменяя написанный код. То есть, те обработчики, которые у нас написаны, должны обрабатывать новые ситуации.

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

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

Для универсальности и распространения на всю программы нам предоставляется базовый класс std::exception, от него можно порождать свои классы.

Для использования мы должны подключить заголовочный файл:

#include <exception>

Идея exception

У нас есть базовый класс std::exception. Этот базовый класс нам представляет виртуальный метод what(), возвращающий строку message. Стандартные ошибки являются производными от этого класса. Производные классы могут подменять метод what().

Например, есть стандартная ошибка bad_alloc - ошибка, связанная с выделением памяти.

И да, мы свои классы можем тоже порождать от этого базового класса! Пусть у нас есть класс Array, мы для него хотим создать объекты, которые отвечают за определенные ситуации. Назовём класс ErrorArray. От него уже будем порождать конкретные ошибки: некорректный индекс - ErrorIndex, ErrorAlloc (перехватываем bad_alloc на себя).

Любую ошибку с нашим классом мы можем перехватить. На уровне класса exp мы можем перехватить любую ошибку, связанную с нашим массивом.

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

Преимущества использования обработки исключительных ситуаций

Плюсы использования обработки исключительных ситуаций:

  • Не нужно прокидывать ошибку через много уровней. Мы сразу переходим в обработчик.

  • Мы разделяем саму логику (код нашей задачи) от обработки исключительных ситуаций, вынося обработчики отдельно, занося их в методы what().

  • Можем легко развивать ПО и модифицировать.

Пример с удобной обработкой исключительных ситуаций:

Пример с тем же самым классом Array, описанным выше.

class ExceptionArray : public std::exception // Базовый класс для отлова ошибок, связанных с классом Array
{
protected:
	char* errormsg;
public:
	ExceptionArray(const char* msg) // В конструкторе получаем строку сообщения
	{
		int Len = strlen(msg) + 1;          // Определяем длину
		this->errormsg = new char[Len];     // Выделяем память
		strcpy_s(this->errormsg, Len, msg); // Копируем в нашу строку
	}
	virtual ~ExceptionArray() { delete[]errormsg; }
	virtual const char* what() const noexcept override { return this->errormsg; } // Подменяем метод what
};

class ErrorIndex : public ExceptionArray // Уже конкретная ошибка, связанная с классом Array
{
private:
	const char* errIndexMsg = "Error Index";
	int ind;
public:
	ErrorIndex(const char* msg, int index) : ExceptionArray(msg), ind(index) {}
	virtual ~ErrorIndex() {}
    
    // Здесь идет просто формирование строки, которая выдает описание ошибки.
	virtual const char* what() const noexcept override
	{
		int Len = strlen(errormsg) + strlen(errIndexMsg) + 8;
		char* buff = new char[Len + 1];
		sprintf_s(buff, Len, "%s %s: %4d", errormsg, errIndexMsg, ind);
		char* temp = errormsg;
		delete[]temp;
		const_cast<ErrorIndex*>(this)->errormsg = buff;
		return errormsg;
	}
};

int main()
{
	try
	{
		throw(ErrorIndex("Index!!", -1)); // Генерим исключительную ситуацию, создаём объект
	}                                     // и отлавливаем объект ниже
	catch (ExceptionArray& error)         
	{
		cout << error.what() << endl;
	}
	catch (std::exception& error)
	{
		cout << error.what() << endl;
	}
	catch (...)
	{
	}

	return 0;
}

Перегрузка операторов

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

Идея

  • Мы создаем свое данное. Почему бы нам для этого данного не определить какие-либо операции? Ведь для стандартных данных они есть.

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

  • Мы не можем задавать новые операторы, а на основе существующих операторов создавать новые операции, то есть перегружать оператор.

Какие операторы нельзя перегружать

Шесть операторов, которые перегружать нельзя:

  • Оператор . - доступ к члену объекта. Если мы перегрузим этот оператор, мы не сможем вызвать для объекта ни одного метода!

  • Оператор .* - указатель на метод.

  • Оператор :: - оператор доступа к контексту. Он применяется для доступа к членам через имя класса или для доступа к глобальному контексту.

  • Оператор ? : - тернарный оператор. Разработчики просто не смогли придумать, как перегрузить этот оператор. Страуструп не пришел ни к одному из решений.

  • Оператор sizeof - определение размера объекта. Если мы перегрузим, мы такое вытворим в программе!

  • Оператор typeid - возвращает id типа объекта. Если мы перегрузим, мы не сможем идентифицировать объект и понять, какого он типа.

Операторы .* и ->*

С++ добавляет два интересных оператора. Напомним, что оператор .* перегружать нельзя, а оператор ->* можно.

Посмотрим на очень интересный пример с функциями:

void f();     // Определили функцию f()
void (*pf)(); // Определили указатель на функцию
pf = f;		  // Этот указатель инициализируем адресом функции(так как имя любой функции - это ее адрес в памяти)
pf();         // Через указатель на функцию вызываем функцию

Примечание: оператор () - оператор разыменования - вызов функции по адресу. Так же вызвать функцию f() можно и таким образом: (*pf)() - синтаксис позволяет.

Что касается методов класса:

void A::f();     // Метод класса A
void (A::*pf)(); // Указатель на метод класса A

// Хотелось бы проинициализировать этот указатель.
// pf = A::f; - Если мы таким образом напишем, мы получим не адрес этого метода
// Метод не находится в классе. Он вызывается по указателю, и чтобы получить этот адрес, 
// было принято решение добавить вот такой синтаксис
pf = &A::f;  // Вычисление адреса метода.

A obj; 
(obj.*pf)(); // Чтобы вызвать метод через указатель, используется оператор .*
			 // Этот указатель имеет более низкий приоритет, чем (),
             // поэтому, чтобы использовать (),
             // надо повысить его приоритет, взяв obj.*pf в круглые скобки.

A* p = &obj;
(p->*pf)();  // Оператор ->* используется для указателя на объект
             // В метод, на который указывавет этот указатель, будет передаваться
             // указатель на объект.

Таким образом, мы разделяем вызов функции и вызов метода. Если мы вызываем метод класса через указатель для объекта, используется оператор ., а если работаем с указателем на объект, используется оператор ->.

Рекомендации по перегрузке операторов

  1. Операторы, которые можно перегрузить только как члены классов:

    • Оператор = - оператор присваивания (бинарный)

    • Оператор () - функтуатор (бинарный)

    • Оператор [] - индексация (бинарный)

    • Оператор -> - унарный

    • Оператор ->* - бинарный, так как принимает указатель на метод и объект, метод которого вызываем

  2. Бинарные операторы можно перегружать как члены класса или как внешние функции-операторы. Это зависит от ситуации. Конечно, надо отдавать предпочтение члену класса. Если мы перегружаем бинарный оператор, как член класса, он принимает 1 параметр (второй параметр он принимает неявно - *this).

  3. Унарные операторы перегружаем как члены класса.

Пример перегрузки бинарных операторов

Бинарный оператор перегружается либо как член, либо как внешняя функция.

class Array
{
...
public:
    bool operator==(const Array& arr) const;
...
};

// Первый вариант - перегрузка оператора, как члена класса
bool Array::operator==(const Array& arr) const
{...}

// Второй вариант - перегрузка оператора, как внешней функции
bool Array::operator!=(const Array& arr1, const Array& arr2) const
{...}

Если мы перегружаем бинарный оператор как член класса, он принимает один параметр (неявно) - указатель this.

Какие бывают проблемы с перегрузкой, как членов?

Рассмотрим пример с типом Complex.

В данном случае оператор - перегружен как член, а оператор + - как друг класса. Это сделано для того, чтобы получить доступ к private-членам без get()-еров.

Если мы перегружаем оператор, как член класса, то в этом случае как левый операнд, так и правый операнд в операторе + может неявно преобразовываться к типу Comlex. Ну, например, мы хотим число сложить с комплексным. Для оператора +, если он реализован, как внешняя функция, можно как число сложить с комплексным числом, так и комплексное число с простым числом. Для бинарной операции - левый операнд всегда должен быть комплексным числом. Мы не можем от числа отнять комплексное число!

class Complex
{
private:
	double re, im;
public:
	Complex(double r = 0., double i = 0.) : re(r), im(i) {}
	Complex operator-() const { return Complex(-re, -im); } // Унарный оператор
	Complex operator-(const Complex& c) const { return Complex(re + c.re, im + c.im); }
	friend Complex operator+(const Complex& c1, const Complex& c2);
	friend ostream& operator<<(ostream& os, const Complex& c); // Два параметра - поток и комплексное число                    
};

Complex operator+(const Complex& c1, const Complex& c2)
{ return Complex(c1.re + c2.re, c1.im + c2.im); }

ostream& operator<<(ostream& os, const Complex& c)
{ return os<<c.re<<" + "<<c.im<<"i"; }

void main()
{
	Complex c1(1., 1.), c2(1., 2.), c3(2., 1.);
	Complex c4 = c1 + c2;
	cout<<c4<<endl;
	Complex c5 = 5 + c3;
	cout<<c5<<endl;
//	Complex c6 = 6 - c3; Error!!! Нельзя отнять от числа комплексное число
	Complex c7 = -c1;
	cout<<c7<<endl;
}

Дополнительные примечания к примеру выше:

  • Операторы >> и << лучше перегружать только для работы с потоком ввода-вывода.

  • Унарный оператор - не принимает параметров (один параметр передается неявно - *this).

  • Когда левый операнд надо неявно приводить к типу класса, удобно определять как внешнюю функцию.

Умные указатели. Перегрузка операторов -> и *.

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

class A
{
public:
	void f();
};

class B
{
public:
    A* operator->();
};

// Создадим объект где-то в коде и вызовем оператор ->
// Такая запись будет означать: оператор возвращает указатель на объект класса A
// и для него, для объекта, мы вызваем метод f()
B obj;
obj->f(); // (obj.operator->())->f()

Пример реализации - использование оператора ->.

Объект класса B по существу является прозрачной оболочкой. Мы через объект B работаем с методами класса A. Совместно с указателем -> еще перегружается оператор *. Он помогает делать примерно то же самое - возвращает ссылку на объект, и по ссылке мы уже вызываем метод. Мы можем перегружать эти операторы как для константных, так и для не константных объектов.

class A
{
public:
	void f() const { cout<<"Executing f from A;"<<endl; }
};

class B
{
private:
	A* pobj;
public:
	B(A* p) : pobj(p) {}
	A* operator->() { return pobj; }
	const A* operator->() const { return pobj; }
	A& operator*() { return *pobj; }
	const A& operator*() const { return *pobj; }
};

void main()
{
	A a;
	B b1(&a);
	b1->f(); // (b1.operator->())->f()
	const B b2(&a);
	(*b2).f(); // (b2.operator*()).f()
}

Оператор->. может возвращать так же ссылку в том случае, если этот класс, на который она возвращает ссылку, содержит перегруженный оператор ->.

Рассмотрим пример ниже:

class A
{
public:
	void f() { cout<<"Executing f from A;"<<endl; }
};

class B
{
private:
	A* pobj;
public:
	B(A* p) : pobj(p) {}
	A* operator->() { cout<<"B -> "; return pobj; }
};

class C
{
private:
	B& alias;
public:
	C(B& b) : alias(b) {}
	B& operator->() { cout<<"C -> "; return alias; }
};

void main()
{
	A a;
	B b(&a);
	C c(b);
	c->f(); // ((c.operotor->()).operator->())->f() сначала возвращаем ссылку на b,
                // потом возвращаем указатель на a, потом вызываем метод f() по указателю
}

Перегрузка оператора ->*. Функтуатор.

Оператор ->* перегружается как член класса и является бинарным (*this и указатель на метод)

class Callee
{
private:
	int index;
public:
	Callee(int i = 0) : index(i) {}
	int inc(int d) { return index += d; }
};

class Caller
{
public:
	typedef int (Callee::*FnPtr)(int); // указан тип
private:
	Callee* pobj;
	FnPtr ptr;
public:
	Caller(Callee* p, FnPtr pf) : pobj(p), ptr(pf) {}
	int operator ()(int d) { return (pobj->*ptr)(d); } // functor
};

class Pointer
{
private:
	Callee* pce;
public:
	Pointer(int i) { pce = new Callee(i); }
	~Pointer() { delete pce; }
	Caller operator->*(Caller::FnPtr pf) { return Caller(pce, pf); } // принимающий указатель на метод
};

void main()
{
	Caller::FnPtr pn = &Callee::inc;
	Pointer pt(1);
	cout<<"Result: "<<(pt->*pn)(2)<<endl; // (pt.operator->*(pn)).operator()(2)
}

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

Перегрузка операторов [], =, ++ и приведения типа. Индексатор

Параметром оператора [] может быть объект любого типа. Таким образом можно создавать ассоциативные массивы.

Я привел пример, когда оператор квадратные скобки принимает объект класса Index и который по индексу получает массив. Второй параметр должен быть целого типа - int.

Мы так же можем определить оператор приведения типа.

Если мы не хотим чтобы не было возможности неявно вызвать оператор, то точно так же как для конструктора записывается модификатор explicit перед оператором приведения типа.

Что касается оператора присваивания

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

Разница оператора присваивания с копированием - создает копию объекта, а оператор присваивания с переносом - захватывает временный объект.

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

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

Операторы инкремент (++) и декремент (--)

Идея: отделить постфиксную от префиксной записи.

Решение: унарный - префиксный, бинарный - постфиксный операторы.

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

# include <iostream>
# include <exception>
# include <stdexcept>
# include <cstring>

using namespace std;

class Index
{
private:
	int ind;
public:
	Index(int i = 0) : ind(i) {}
        // префиксная - унарный
	Index& operator++()		// ++obj	
	{
		++ind;
		return *this;
	}
        // постфиксная - бинарный
	Index operator++(int)	// obj++
	{
		Index it(*this); // временный объект с копией
		++ind;
		return it; // вернуть копию
	}
	operator int() const { return ind; } // Мы так же можем определить оператор приведения типа. 
        // оператор тип() и что мы возвращаем. В данном случае происходит неявное приведение типа int
};

class Array
{
private:
	double* mas;
	int cnt;
	void copy(const Array& arr);
	void move(Array& arr);
public:
        // здесь пример, когда запрещается явно вызвать оператор приведения от целого типа к типу Array против некорректных преобразований.
	explicit Array(int n = 0) : cnt(n)
	{
		mas = cnt > 0 ? new double[cnt] : ((cnt = 0), nullptr);  
	}
	explicit Array(const Array& arr) { copy(arr); }
	Array(Array&& arr) { move(arr);	}
	~Array() { delete[]mas; }
	Array& operator=(const Array& arr);
	Array& operator=(Array&& arr);

        // Мы можем перегружать оператор как для константных, так и не для константных. 
	double& operator[](const Index& index); 
	const double& operator[](const Index& index) const;

	int count() const { return cnt; }
};

Array& Array::operator=(const Array& arr) // оператор присваивания с копированием - создает копию объекта
{
	if( this == &arr ) return *this; // проверка на то, что это не один и тот же объект.
	delete []mas;
	copy(arr);
	return *this;
}

Array& Array::operator=(Array&& arr) // оператор присваивания с переносом - захватывает временный объект
{
	delete []mas; // освободить
	move(arr); // перенести
	return *this;
}

double& Array::operator[](const Index& index)
{
	if(index < 0 || index >= cnt) throw std::out_of_range("Error: class Array operator [];"); 
	return mas[index]; // index должен быть типа int, он типа Index.
                           // Но т.к. определен оператор приведения типа к типу int происходит неявное приведение к типу int
}

const double& Array::operator[](const Index& index) const 
{
	if(index < 0 || index >= cnt) throw std::out_of_range("Error: class Array operator [];"); 	
	return mas[index];
}

void Array::copy(const Array& arr)
{
	cnt = arr.cnt;
	mas = new double[cnt];
	memcpy(mas, arr.mas, cnt*sizeof(double));
}

void Array::move(Array& arr)
{
	cnt = arr.cnt;
	mas = arr.mas;
	arr.mas = nullptr;
}

Array operator*(const Array& arr, double d)
{
	Array a(arr.count());
	for(Index i; i < arr.count(); i++)
		a[i] = d*arr[i];
	return a;
}

Array operator*(double d, const Array& arr) { return arr*d; }

Array operator+(const Array& arr1, const Array& arr2)
{
	if( arr1.count() != arr2.count() ) throw length_error("Error: operator +;");
	Array a(arr1.count());
	for(Index i; i < arr1.count(); i++)
		a[i] = arr1[i] + arr2[i];
	return a;
}

istream& operator>>(istream& is, Array& arr)
{
	for(Index i; i < arr.count(); i++)
		cin>>arr[i];
	return is;
}

ostream& operator<<(ostream& os, const Array& arr)
{
	for(Index i; i < arr.count(); i++)
		cout<<" "<<arr[i];
	return os;
}

void main()
{
	try
	{
		const int N = 3;
		Array a1(N), a2;
		cout<<"Input of massive: ";
		cin>>a1;
//		a2 = a1 + 5; Error!!!
		a2 = 2*a1;
		cout<<"Result: "<<a2<<endl;
	}
	catch(const exception& exc)
	{
		cout<<exc.what()<<endl;
	}
}

Операторы приведения типов

Перегрузка операторов приведения

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

class Number {
private:
    int value;
public:
    Number(int v) : value(v) {}

    operator int() const {
        return value;
    }
};

Number n(10);
int x = n;  // Приведение типа Number к int, x будет 10

Last updated