C++ и ООП фича для чуть более продвинутых новичков

Discussion in 'С/С++, C#, Rust, Swift, Go, Java, Perl, Ruby' started by Ra$cal, 17 Oct 2009.

  1. Ra$cal

    Ra$cal Elder - Старейшина

    Joined:
    16 Aug 2006
    Messages:
    670
    Likes Received:
    185
    Reputations:
    78
    Пожалуй на форуме не хватает топика про преимущества ООП. Нада бы исправить сей недостаток. А то многовато мнений, что с++ мощный язык, но при этом используют его ровно так же, как и си и паскаль. Я опишу интересный прием программирования с использованием ООП, который пригодился мне. Если еще что нить интересное вспомню и будет интерес - продолжу тему.

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

    Code:
    bool ReadData (char* File, char* buffer)
    {
    	bool is_readed = false;
    	MappingAddr = 0;
    	hFile = CreateFile (File, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
    	if (hFile == INVALID_HANDLE_VALUE)
    		goto ERROR_CREATE_FILE;
    	
    	hMapping = CreateFileMapping (hFile, 0, PAGE_READWRITE, 0, 0, 0);
    	if (hMapping == 0)
    		goto ERROR_CREATE_MAPPING;
    
    	MappingAddr = (char*)MapViewOfFile(hMapping, FILE_MAP_READ|FILE_MAP_WRITE, 0, 0, 0);
    	if (MappingAddr == 0)
    		goto ERROR_VIEW_MAPPING;
    
    	strcpy(buffer, (char*)MappingAddr);
    	is_readed = true;
    
    ERROR_VIEW_MAPPING:
    	CloseHandle (hMapping);
    
    ERROR_CREATE_MAPPING:
    	CloseHandle (hFile);
    
    ERROR_CREATE_FILE:
    
    	return is_readed;
    }
    Вариант рабочий, но не очень красивый. Кутерьма из-за необходимости освободить хэндлы, которые удалось открыть, но произошла ошибка в дальнейшем доступе к хэндлам. Давайте поиграем в игру - найдите решение. Оно должно свестись к убиранию CloseHandle'ов из кода, т.е. чтобы ресурсы освобождались сами. Хинт - тема этого поста - деструкторы. Попробуйте придумать свое решение, потом глядите сюда. Если впадлу - просто читаем дальше.

    Ну а теперь перейдем к решению. Итак, первым делом создаем 3 класса - KFile для файла, KMapping для маппинга и KViewOfFile для проекции файла в память.

    Code:
    #include <iostream>
    #include <Windows.h>
    using namespace std;
    
    class KFile{
    private:
    	HANDLE hFile;
    public:
    	KFile(){hFile = INVALID_HANDLE_VALUE;}
    	~KFile(){
    		if(hFile != INVALID_HANDLE_VALUE)
    			CloseHandle(hFile);
    
    		hFile = INVALID_HANDLE_VALUE;
    	}
    
    	HANDLE handle() const{
    		return  hFile;
    	}
    
    	bool isOpened(){
    		return !(hFile == INVALID_HANDLE_VALUE);
    	}
    
    	bool open(char* path){
    		hFile = CreateFile (path, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
    		return isOpened();
    	}
    };
    
    class KMapping{
    private:
    	HANDLE hMapping;
    public:
    	KMapping(){hMapping = NULL;}
    	~KMapping(){
    		if(hMapping != NULL)
    			CloseHandle(hMapping);
    
    		hMapping = NULL;
    	}
    
    	HANDLE handle() const{
    		return hMapping;
    	}
    
    	bool isOpened(){
    		return !(hMapping == NULL);
    	}
    
    	bool open(const KFile& file){
    		hMapping = CreateFileMapping (file.handle(), 0, PAGE_READWRITE, 0, 0, 0);
    		return isOpened();
    	}
    };
    
    class KViewOfFile{
    private:
    	BYTE* MappingAddr;
    public:
    	KViewOfFile(){MappingAddr = NULL;}
    	~KViewOfFile(){
    		if(MappingAddr != NULL)
    			UnmapViewOfFile(MappingAddr);
    
    		MappingAddr = NULL;
    	}
    
    	BYTE* value() const{
    		return MappingAddr;
    	}
    
    	bool isOpened(){
    		return !(MappingAddr == NULL);
    	}
    
    	bool open(const KMapping& mapping){
    		MappingAddr = (BYTE*)MapViewOfFile(mapping.handle(), FILE_MAP_READ|FILE_MAP_WRITE, 0, 0, 0);;
    		return isOpened();
    	}
    };
    
    void test()
    {
    	KFile file;
    	KMapping mapping;
    	KViewOfFile view;
    
    	if(!(file.open("test.txt") & mapping.open(file) & view.open(mapping))){
    		cout << "error opening file\n";
    		return;
    	}
    
    	char text[50];
    	strcpy(text, (char*)view.value());
    	cout << "file readed\n";
    	cout << text << "\n";
    }
    
    void main()
    {
    	test();
    	system("pause");
    }
    
    Вся работа осуществляется в функции test(). Открываем маппинг одной строкой в ифе. Если хоть один из классов не отрабатывает - выходим из функции, причем не заморачиваясь об закрытии хэндлов. Так же не заморачиваемся этим вопросом и в случае удачного чтения. А все почему?

    Суть приема. А потому, что мы объявили объекты классов локально. Это значит, что при выходе из функции они будут уничтожены. Уничтожение объекта приводит к вызову его деструктора. Всю логику контроля хэндлов мы как раз там и реализовали. В итоге все работает и без нашего вмешательства.

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

    Code:
    class KFileMapping{
    private:
    	KFile file;
    	KMapping mapping;
    	KViewOfFile view;
    
    public:
    
    	KFileMapping(){
    
    	}
    
    	~KFileMapping(){
    		close();
    	}
    
    	void close(){
    		view.close();
    		mapping.close();
    		file.close();
    	}
    
    	bool open(char* path){
    		if(!(file.open(path) & mapping.open(file) & view.open(mapping))){
    			close();
    			return false;
    		}
    
    		return true;
    	}
    
    	BYTE* value() const{
    		return view.value();
    	}
    };
    
    Ну и использовать так:

    Code:
    void test2()
    {
    	KFileMapping mapping;
    
    	if(!mapping.open("test.txt")){
    		cout << "error opening file\n";
    		return;
    	}
    
    	char text[50];
    	strcpy(text, (char*)mapping.value());
    	cout << "file readed\n";
    	cout << text << "\n";
    
    }
    Итого имеем:
    1) Защита от ошибок
    2) Упрощение кодирования
    3) Повторное использование кода. Постепенно со временем вы соберете подборку своих классов, реализующих наиболее нужный вам функционал, с нужными вам интерфейсами(например не часто приходится указывать, как открывать файл - только для чтения, только для записи, итп. Вы делаете так, чтобы этим было максимально удобно пользоваться. Когда понадобится указывать доступ - просто добавите новый метод, минимально трогая старую реализацию). Ну вот собсно и все.

    PS: то, что мы с вами сделали, это по сути паттерн RAII (велкам ту вики), только немного модифицированный(в идеале метод open нада заменить вызовом конструктора, но по мне итак вполне юзабельно)
     
    6 people like this.
  2. razb

    razb Active Member

    Joined:
    24 Mar 2009
    Messages:
    658
    Likes Received:
    133
    Reputations:
    18
    Хороший обзоз, пример паттерна + многократное использование кода )
    Вот только жаль что все чаще и чаще встречаешь с++ код подобный первому примеру, в натив си, еще такой подход приемлен, но не в плюсах )
     
  3. scrat

    scrat кодер

    Joined:
    8 Apr 2007
    Messages:
    625
    Likes Received:
    541
    Reputations:
    3
    тут важно понять, что нужно всегда учитывать требования к задаче, вероятность доработки программы и прочее. Один разок записать/прочитать один файл - можно и процедурным программированием обойтись, также как не стоит перегружать оператор сложения для собственных классов, чтобы сложить два числа.

    А вообще в продвинутых(читать высокоуровневых) платформах реализованы Garbage Collector'ы, которые автоматически подчищают все объекты и прочее.
     
  4. Ra$cal

    Ra$cal Elder - Старейшина

    Joined:
    16 Aug 2006
    Messages:
    670
    Likes Received:
    185
    Reputations:
    78
    это типичная ошибка. GC адекватно подходит для очистки памяти. Но вот для других ресурсов(файлы, сокеты, все, что влияет на систему целиком, а не только на среду выполнения) его применимость под большим вопросом. Советую прочитать о специфике деструкторов в Java и C#. Выяснится не очень приятная их особенность. В дотнете это решено IDisposable, в Java хз как. так вот это я к тому, что GC без использования мозга и адекватного программирования не решает проблем, а лишь добавляет.
     
  5. scrat

    scrat кодер

    Joined:
    8 Apr 2007
    Messages:
    625
    Likes Received:
    541
    Reputations:
    3
    я думаю на закрытие потоков и прочих стандартных вещей они его заточили, в остальном - да, никаких тебе "правильных" закрытий
     
  6. greki_hoy

    greki_hoy Member

    Joined:
    4 Mar 2010
    Messages:
    326
    Likes Received:
    57
    Reputations:
    41
    2Ra$cal
    можно попробовать не писать отдельный класс для каждой сущности а заюзать типа этого
    Code:
    template<class fn_t,class res_t>
    struct jk{
    	fn_t pfn;
    	res_t some;
    	jk(fn_t pfn,res_t some):pfn(pfn),some(some){}
    	void detach(){some=0;}
    	~jk(){some?pfn(some):0;}
    };
    
    #include <boost/typeof/typeof.hpp>
    
    #define do_exit(pfn,some)\
    	jk<typeof(&pfn),typeof(some)> _##some(pfn,some)
    
    получится что то типа
    Code:
    bool ReadData (char* File, char* buffer)
    {
    	hFile = CreateFile (File, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
    	if (hFile == INVALID_HANDLE_VALUE)
    		return false;
    	do_exit(CloseHandle,hFile);
    	
    	hMapping = CreateFileMapping (hFile, 0, PAGE_READWRITE, 0, 0, 0);
    	if (hMapping == 0)
    		return false;
    	do_exit(CloseHandle,hMapping);
    
    	MappingAddr = (char*)MapViewOfFile(hMapping, FILE_MAP_READ|FILE_MAP_WRITE, 0, 0, 0);
    	if (MappingAddr == 0)
    		return false;
    	
    
    	strcpy(buffer, (char*)MappingAddr);
    	
    	UnmapViewOfFile(MappingAddr);
    
    	return true;
    }
    
     
    #6 greki_hoy, 13 Jan 2011
    Last edited: 13 Jan 2011
  7. Gar|k

    Gar|k Moderator

    Joined:
    20 Mar 2009
    Messages:
    1,166
    Likes Received:
    266
    Reputations:
    82
    Вот за это я и не учил С++, куча лишней писанины. Мне интересно во что превращается в итоге ассемблерный код в кучу вызовов call ret ов? + насколько я скудно знаю дополнительная память на всевозможные указатели иерархии классов.

    Code:
    	bool isOpened(){
    		return !(hFile == INVALID_HANDLE_VALUE);
    	}
    
    не могу это понять... вернуть НЕ инвалид хендл чо? o_O :)
    Ну а так прикольно, сам сейчас пытаюсь мудрить с классами переносимый между проектами код - задолбало копипастить, но реально выходит кучу лишней писанины и движений, особенно в WinAPI коде кое где нужно указывать ссылки на CALLBACK функции, а ведь напрямую метод класса не укажешь и начинаются пляски с бубнами :)
     
    _________________________
  8. GRRRL Power

    GRRRL Power Elder - Старейшина

    Joined:
    13 Jul 2010
    Messages:
    823
    Likes Received:
    185
    Reputations:
    84
    То же самое, что и return hFile != INVALID_HANDLE_VALUE; :)

    Мелкие функции принято инлайнить, а еще последние компиляторы очень хорошо оптимизируют код.

    Укажешь, но только статический.
     
  9. bad_boy

    bad_boy Elder - Старейшина

    Joined:
    30 Dec 2004
    Messages:
    187
    Likes Received:
    33
    Reputations:
    3
    Тьфу блин, археологи. А я кинулся раскритиковывать "преимущества". 60 строк накатал :(