15
Oct
2010

Acest articol explica folosirea functiilor ptr_fun, mem_fun si mem_fun_ref. Una dintre principalele functionalitati ale acestora este de a acoperii diferentele sintactice in ceea ce priveste apelarea functiilor in limbajul C++, si anume, daca avem o functie f si un obiect x, putem avea sintaxa:

  1. f(x); - in cazul cand f() nu este functie membru
  2. x.f(); - in cazul in care f() este membru si x este un obiect sau referinta
  3. p->f(); - in cazul in care f() este membru si p este un pointer spre x

Acum sa presupunem ca avem o functie care testeaza obiecte de tip C declarata astfel: bool test(C const &c) si un container std::list<C> care va contine obiecte de tip C, implementarea va fi cat mai simpla, practic clasa C va avea un singur membru int iar functia test() va testa daca el este par sau impar.

Pentru a gasi primul element care trece functia test() ( primul element par) vom folosi algoritmul find_if(), exemplu:

#include <iostream>
#include <list>
#include <algorithm>

//clasa C
class C {
public:
	C(int t = 10) : mem(t){}
	int getMem() const {return mem;} 
private:
	int mem;
};

//functia test
bool test(C const &c) {
	if(c.getMem() % 2)
		return false;
	return true;
}

int main () {
	// pentru simplificare am lasat constructorul implicit 
	// si vor crea lista dintr-un array
	C c[10] = { 5, 7, 8, 1, 2};

	std::list<C> l(c, c+10);

	for(std::list<C>::iterator it = l.begin(); it != l.end(); ++it){
		std::cout << it->getMem() << '\n';
	}
	
	//algoritmul find_if returneaza un iterator
	std::list<C>::iterator f = std::find_if(l.begin(), l.end(), test);

	//in cazul in care nici un element nu este gasit se returneaza l.end()
	if(f != l.end())
		std::cout << "Elementul " << f->getMem() << " a fost gasit!\n";

	std::cin.get();
	return 0;
}

Ok, pana aici nimic foarte interesant, dar acum sa presupunem ca dorim sa gasim primul element impar, solutia ar trebui sa fie simpla si rapida deoarece avem deja functia care verifica daca membrul int al clasei noastre C este par, tot ce trebuie sa facem este sa inversam valoarea bool returnata de aceasta functie, deci ajungem la un astfel de apel al find_if:

std::list<C>::iterator f = std::find_if(l.begin(), l.end(), std::not1(test));

Dar avem parte de o surpriza, codul de mai sus nu compileaza. Pentru a il face sa compileze avem nevoie de ptr_fun inainte de a aplica adaptorul not1, iar apelul va deveni:

std::list<C>::iterator f = std::find_if(l.begin(), l.end(), std::not1(std::ptr_fun(test)));

Aceasta rezolvare ne duce la o noua serie de intrebari. De ce treuie sa aplicam ptr_fun inaite de a aplica not1? Ce face ptr_fun si cum face el codul sa compileze? Raspunsul este urmatorul: singurul lucu pe care il face ptr_fun este sa puna la dispozitie unele definitii typedef. Aceste typedef sunt cerute de catre adaptorul not1 si de aceea nu este posibila aplicarea directa asupra functiei test(). Adaptorul not1 nu este singura componenta din STL care impune astfel de cerinte. Aceste definitii typedef sunt: argument_type, first_argument_type, second_argument_type si result_type, dar in afara de cazul cand scriem proprii adaptori (nu o sa tratam subiectul in acest articol) nu avem nevoie sa stim prea multe despre aceste definitii typedef, decat situatiile in care trebuie sa utilizam ptr_fun.

In situatia functiei separate cred ca am facut putina lumina, dar ce facem daca avem o functie (o sa o numim tot test, desi aceasta nu va mai testa nimic, doar va printa membrul int), containerul de data aceasta va fi un std::vector<C*> (adica un vector care va contine pointeri catre obiecte de tip C) si vom incerca sa printam toti membrii cu ajutorul algoritmului for_each.

Atentie la cod, daca avem un container STL care contine pointeri si alocam memorie pe heap folosind acei pointeri, suntem responsabili sa stergem acea memorie inainte de a sterge pointerul din container si toti pointerii inainte de a sterge containerul sau inainte ca acesta sa iasa din scope. Am pus cate un mesaj in constructorul si destructorul clasei pentru a se observa rularea acestora.

//prima incercare:
#include <iostream>
#include <algorithm>
#include <vector>

//clasa C
class C {
public:
	C(int t = 10) : mem(t){std::cout << "Constructor \n";}
	
	//functia test este o functie membru de data aceasta
	void test() {std::cout << mem << '\n';}
	~C() {std::cout << "Destructor\n";}
private:
	int mem;
};

int main(){

	std::vector<C*> v(5);
	//alocam dinamic memorie pt obiectele de tip C
	// prin intermediul pointer-iilor din vector
	for(unsigned int i = 0; i != v.size(); ++i)
		v[i] = new C();
	
	//incercam sa pasam functia membru ca parametru pentru for_each
	std::for_each(v.begin(), v.end(), &C::test);
	
	// stergem memoria folosind delete
	// inaite ca vectorul sa iasa din scope si sa avem un memory leak
	for(unsigned int i = 0; i != v.size(); ++i)
		delete v[i];

	std::cin.get();
	return 0;
}

Dar acest cod nu va compila:

std::for_each(v.begin(), v.end(), &C::test);

Cauza: in cazul de fata avem un pointer si o functie membru, deci sintaxa numarul 3 ar trebui folosita in interiorul algoritmului for_each, deci STL-ul ar trebui sa aiba trei implementari ale algoritmului for_each, dar nu are numai una, care foloseste sintaxa 1 (la fel ca si functiile nemembre), aceasta este o conventie in biblioteca STL. Pentru a rezolva si aceasta problema avem nevoie de mem_fun. Functia mem_fun preia un pointer catre o functie membru (test) si returneaza un obiect de tip mem_fun_t, care contine pointerul spre functia membru si ofera operatorul () pentru a apela functia membru indicata pentru obiectul transmis. Astfel linia corecta va arata asa:

std::for_each(v.begin(), v.end(), std::mem_fun(&C::test));

Algoritmul for_each preia un obiect de tip mem_fun_t care contine un pointer spre functia C::test, pentru fiecare pointer din container apeleaza functorul mem_fun_t folosind sintaxa 1). Pe scurt mem_fun adapteaza sintaxa de apelare a functiei membru.

Am ajuns la ultimul caz pe care o sa il tratam in articol, si anume folosirea functiei mem_fun_ref in cazul in care container-ul contine direct elemente de tip C.

#include <iostream>
#include <algorithm>
#include <vector>

class C {
public:
	C(int t = 10) : mem(t){std::cout << "C-tor \n";}
	void test() {std::cout << mem << '\n';}
	~C() {std::cout << "D-tor\n";}
private:
	int mem;
};

int main(){

	std::vector<C> v(5);
	// in cazul container-ului care contine direct elemente de tip C
	// vom utiliza functia mem_fun_ref
	std::for_each(v.begin(), v.end(), std::mem_fun_ref(&C::test));

	std::cin.get();
	return 0;
}

In loc de concluzie: Sper ca dupa lecturarea acestui mic articol va veti descurca mai usor la customizarea algoritmilor STL. Astept comentarii, sugestii si corectii.