Шаг 13 - Нетрадиционная медицина: некоторые преимущества класса set

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

Итак, класс set. Данный шаг, конечно же, не ставит своей целью охватить все возможности класса. Просто мне хотелось немного поговорить о тех его свойствах, которые кажутся мне интересными и довольно-таки часто оказывались мне полезны.

Но сначала. Возможно, я немного повторю Артема если скажу: set или, говоря языком математики, множество, представляет собой объект, контролирующий произвольной длины последовательность уникальных элементов какого-либо типа. В общем, классический подход ко множеству такой: set используется для хранения неповторяющихся (уникальных) ключей, либо для проверки, есть ли элемент в наборе данных.

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

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

Но вот в чем возникла загвоздочка: в любой функции по опреденению не может быть повторяющихся абсцисс (то есть иксов с разными игреками). Что делать? Писать проверочку ручками (да и сортировать вдобавок!) - брала тоска. Выход я нашел такой (привожу пример, конечно же, сильно упрощенный):

1. Создайте проект Win32 Console Application (пустой). Назовем, например, set_test.

2. В нем создайте два файла: заголовок (set_test.h) и cpp-шник (соответственно, set_test.cpp). Хотя можете, конечно, хранить все в одном: проект маленький.

3. В заголовке (set_test.h) напишите:

#include <set>          // это подключаем класс множеств
#include <iostream.h>   // консольный ввод/вывод - он нам понадобится
#include <windows.h>    // для MessageBox - лень задавать вопросы через
                        // консоль - вы меня поймете
using namespace std;    // подключаем STL-ное пространство имен - как иначе?

// Это класс точки. Не мудрствуя лукаво, я назвал его stl_point.
// Всякие конструкторы копий мне не нужны - вот и не пишу :)

class stl_point
{
public:
	double x, y;	// собственно, абсцисса и ордината нашей точки
			// ну и конструктор, поприветствуем:
	stl_point(double tx=0, double ty=0)
	{
		x = tx;
		y = ty;
	}
};

// Теперь самое интересное. Перегружаем оператор "меньше":

bool operator<(stl_point first, stl_point second)
{
	return first.x < second.x;
}

Что сие значит? Во-первых, почему именно оператор "меньше". Эта прелесть (set) осуществляет все свои сравнения (по крайней мере, при попытке добавить элемент) именно через оператор "меньше". Как узнать, какой оператор перегружать? А вы попробуйте откомпилить проект (когда мы его закончим) без его перегрузки. Выскочит соответствующий error... :) .

Во-вторых, почему именно такая перегрузка. Ну, мне же нужно повторяющиеся иксы откинуть. И вдобавок по их значениям рассортировать. Вот я и заставляю беднягу-компа сравнивать вместо двух классов два икса... поделом... :)

Но закончим с проектом. Пишем cpp-шник:

#include "set_test.h" // подключаем, что уже выстрадали

set <stl_point> set_exact; // наш список точек

int main(void)
{
	int x, y;

	while(1)
	{
		// запрашиваем иксы-игреки
		cout << "Abscissa: ";
		cin >> x;
		cout << "Ordinate: ";
		cin >> y;

		// Теперь пытаемся добавить то, что навводил бедолага-пользователь,
		// добавить в set. Выводим отчет о результате.
		// Впрочем, на этом остановлюсь подробней.

		if(!set_exact.insert(stl_point(x, y)).second)
			cout << "not inserted" << endl << endl;
		else
			cout << "success" << endl << endl;
	}
	return 0;
}       

Итак, что означает фраза

if(!set_exact.insert(stl_point(x, y)).second)
	cout << "not inserted" << endl << endl;
else
	cout << "success" << endl << endl;

Во-первых, вызвывается конструктор stl_point(x, y) - в созданный экземпляр класса сразу же кидаются икс с игреком (см. конструктор). Далее. Возвращаемое значение сразу же кидается в set_exact и не запоминается больше нигде:

set_exact.insert(stl_point(x, y)) 

Метод insert объекта set возвращает простенький объект типа pair:

template
	struct pair 
{
	typedef T first_type;
	typedef U second_type
	T first;
	U second;
	pair();
	pair(const T& x, const U& y);
	template
	pair(const pair& pr);
};

Как видите, pair чем-то подобен моей точке: все, что содержит - это два элемента любого (возможно, одинакового) типа - под загадочными именами first и second. :) Наш же insert в случае удачи возвращает pair(it, true), иначе - pair(it, false). Ну, что такое загадочное it - никогда не интересовался. Наверное, на воткнутый элемент итератор. А вот второй элемент, second, - правда ведь на определенную мысль наталкивает?

Итак, смотрим что там, у second'а внутри. Если элемент воткнулся - радуемся. Нет - гм... Ну я печалиться не буду :)) Компилим проект. Запускаем. Попробуйте ввести элементы с одинаковыми иксами. И наоборот - с одинаковыми игреками. Да и вообще - потестьте :))

Теперь усложняем проект. Смотрите, чем поменялся main:

#include "set_test.h" // подключаем, что уже выстрадали

set  set_exact; // наш список точек

int main(void)
{
	int x, y;

	while(1)
	{
		// запрашиваем иксы-игреки - здесь пока то же самое
		cout << "Abscissa: ";
		cin >> x;
		cout << "Ordinate: ";
        cin >> y;

		// А с этого места начинается новенькое - опишу ниже
		// Итак, проверяем, чегой-то там вставилось или не вставилось
		if(!set_exact.insert(stl_point(x, y)).second)
		{
			// Ну, пользователь - существо нежное. Если уж он повторно
			// ту же абсциссу ввел, нужно узнать, поменять он хочет значение,
			// или же просто пальчик у него почесался

			if(MessageBox(NULL,
				"Введенному значению абсциссы уже "
				"сопоставлено значение ординаты.\nХотите заменить его "
				"новым значением?",
				"Неувязочка вышла!",
				MB_ICONWARNING | MB_YESNO | MB_DEFBUTTON2) == IDYES)
			{
			 // Ну, раз уж решили менять значение, для начала, не мудрствуя
			// лукаво, удалим старое. Метод erase удаляет из множества
			// элемент с каким-то определенным значением. Раз уж в нашем
			// множестве ключевую (во всех смыслах) роль играет икс, то на
			// игрек я в этом случае чихать хотел. Вы видели конструктор -
			// он по дефолту вместо игрека ноль подставляет. Ну и пусть
			// себе - элемент все равно будет нужный удален :)

				set_exact.erase(stl_point(x));
			 // Напоследок еще раз проверим вставился ли наш элемент
			// Увидите, вставился

				if(set_exact.insert(stl_point(x, y)).second)
					cout << "success" << endl << endl;
				else
					cout << "not inserted" << endl << endl;
			}
			else cout << "not inserted" << endl << endl;
		}
		else cout << "success" << endl << endl;

		// Теперь пройдем все множество с помощью старичка-итератора
		// Смысл в том, что после каждого ввода данных ныне будет
		// распечатываться все множество
		// О том, что есть итератор, смотри в предыдущем шаге :)

		set::iterator i;

		for(i=set_exact.begin(); i!=set_exact.end(); i++)
			cout << "x = " << (*i).x << ", y = " << (*i).y << endl;
		cout << endl;
	}
	return 0;
}

Фухх! Нелегкое, оказывается, это дело - шаги писать! Ладно, выдохся я на сегодня. Если Артем будет респондиться да еще и это все опубликует - может, еще шажков пришлю. :))

Итак, на сегодня все. Удачи!

Шаг прислал Владимир Головков (teen@ATNET.RU).


Предыдущий Шаг | Оглавление
Автор Каев Артем - 19.04.2002