C++/Додатковий функціонал/Шаблони

Матеріал з Вікіпідручника
< C++
Перейти до навігації Перейти до пошуку

Шаблони[ред.]

Шаблони - механізм C++, який дозволяє створювати узагальнені функції і класи, які працюють з типами даних які передаються в параметрі. Можна наприклад створити функцію яка сортує масив цілих чисел, а можна створити шаблон функції, який буде сортувати масиви будь-яких даних, над якими задані операції порівняння і присвоєння. Таким чином одну, і туж, функцію сортування можна застосовувати для різних масивів, не створюючи окрему для кожного типу.

В початковій версії С++ не було шаблонів, але зараз вони підтримуються кожним компілятором, і разом з бібліотекою шаблонів STL входять до стандарту C++.

Функції[ред.]

Шаблон функції виглядає так:

 template <class Ідентифікатор_типу> Тип_результату Назва_функції(Список_параметрів)
 {
     // Тіло функції
 }

Параметр "Ідентифікатор_типу" задає тип з яким працює функція. Всюди в тілі і заголовку функції компілятор замість цього ідентифікатора підставить фактичний тип, який передається. Наприклад функція перестановки двох змінних:

 template <class Type> void swap(Type &a, Type &b)
 {
       Type tmp=a;a=b;b=tmp;
 }

Замість ключового слова class можна застосовувати typename, але більше поширений перший варіант.

Шаблон може узагальнювати кілька різних типів даних:

 template <class Type1, class Type2> void foo(Type1 a, Type2 b)
 {
     printf("Тут ми щось робимо з даними");
     // Неправда
 }

Крім того, параметрами шаблону можуть слугувати не тільки імена типів, а і конкретні значення:

 template <int n> int mul(int x)
 {
      return n*x;
 }

Шаблонну функцію можна перевантажувати. Наприклад:

 unsigned int foo(unsigned int a,unsigned int b)
 {
      return ((a+b+1)*(a+b)/2)+a;
 }

Примітка: за MSVC++ помічена звичка замість генерування коду для кожного типу параметрів просто для найбільшого з використовуваних (наприклад, внаслідок викликів swap<int>() і swap<double>() буде згенеровано код лише для swap<double>(), який при потребі викликатиметься з перетворенням типів)

Тоді компілятор сам буде вибирати потрібну функцію залежно від параметрів. Наприклад програма:

#include <stdio.h>

template <class Type1, class Type2> void foo(Type1 a, Type2 b)
{
	printf("Template foo\n");
}
int foo(int a, int b)
{
	return ((a+b+1)*(a+b)/2)+a;
}
int main(void)
{
	float x;
	foo(x,x);
	for(int i=0;i<3;i++)
		for(int j=0; j<3;j++)
			printf("foo(%i,%i)=%i\n",i,j,foo(i,j));
	return 0;
}

Буде давати такий вивід:

Template foo
foo(0,0)=0
foo(0,1)=1
foo(0,2)=3
foo(1,0)=2
foo(1,1)=4
foo(1,2)=7
foo(2,0)=5
foo(2,1)=8
foo(2,2)=12

Класи[ред.]

Шаблони класів описуються аналогічно до шаблонів функцій. Наприклад шаблон класу для стеку:

#include <stdio.h>

const int SIZE=5;
template <class Type> class Stack
{
	Type st[SIZE];
	int sp;
public:
	Stack()
	{
		sp=0;
	}
	void push(Type a);
	Type pop();
};
template <class Type> void Stack<Type>::push(Type a)
{
	if(sp==SIZE) 
	{
		printf("Переповнення стеку\n");
		return;
	}
	st[sp]=a;
	sp++;

};
template <class Type> Type Stack<Type>::pop()
{
	if(sp==0) 
	{
		printf("Стек порожній\n");
		return 0;
	}
	sp--;
	return st[sp];
}

int main(void)
{
	printf("Введіть \"e\",або \"q\" щоб вийти. \n");
	printf("Введіть \"u\" і ціле число, щоб заштовхнути його в стек.\n");
	printf("Введіть \"p\" щоб витягнути число зі стеку і надрукувати його\n");
	
	Stack<int> s;
	char command=' ';
	while(!((command=='e')||(command=='q')))
	{
		command=getchar();
		switch(command)
		{
			case 'u':
				{
					int tmp;
					scanf("%i",&tmp);
					s.push(tmp);
				}break;
			case 'p':printf("%i\n",s.pop()); 
				 break;
		}
	}
	return 0;
}

Ключове слово typename[ред.]

Добрі люди підказують, що окрім ключового слова class, можна використовувати typename, і вони еквівалентні. Коли Страуструп придумував C++ йому не хотілось використовувати ще одне ключове слово, тому він використав клас. А потім за роботу взявся цілий комітет, і вони вирішили, що це може створити неоднозначності. Тому додали ще і typename.

Правда є випадки, коли typename означає не те, що і клас.

Наприклад, що наступний за ним ідентифікатор є типом:

template < class T>
class MyClass {
    typename T::SubType* ptr;
    ...
};


В даному випадку слово typename означає, що SubType є підтипом класу T, відповідно ptr - показчик на тип T::SubType. Без typename ідентифікатор SubType інтерпретується, як статичний член класу і рядок сприймається, як операція множення значення SubType класу T на значення ptr; Але в даному випадку будь-який тип, який буде підставлений у шаблон, повинен мати внутрішній тип SubType. SubType може бути як класом оголошеним в середині іншого, так і простим typedef-ом.

Розумні вказівники (Smart pointers)[ред.]

В С++ пам’ять, яка виділяється під ресурси, необхідно звільняти. При маніпуляціях з пам’яттю часто виникають програмні помилки:

  • витоки пам’яті;
  • розіменування нульового вказівника, або звернення до неініціалізованї області пам’яті;
  • видалення уже видаленого раніше об’єкту;

Існує техніка управління ресурсами за допомогою локальних об’єктів – RAII. Тобто при отриманні якого-небудь ресурсу, його ініціалізують в конструкторі, а попрацювавши з ним в функції, коректно звільняють в деструкторі. Ресурсом може бути що завгодно, наприклад файл, з’єднання по мережі або блок пам’яті. Приклад:

class VideoBuffer {
	Int *myPixels;
	public:
	
	VideoBuffer() {
		myPixels = new int[640 * 480];
	}
	
	void makeFrame() { /* працюємо з растром */ }

	~VideoBuffer() {
		delete [] myPixels;
	}
};

int game() {
	VideoBuffer screen;
	screen.makeFrame();
}

[1]

Це зручно, отскільки по виходу із функції, нам не потрібно турбуватися про звільнення буфера, оскільки для об’єкту screen, буде викликано деструктор, которий в свою чергу звільнить інкапсульований в собі массив пікселів.

Інакше довелось би писати так:

int game() {
	int *myPixels = new int[640 * 480]
	//  виконуємо дії
	delete [] myPixels;
}

Такий код виглядає не сладним, але коли послідовність дій не є тривіальною, часто звільнити ресурси забувають, наприклад, якщо в коді будуть виключні ситуації і багато умовних конструкцій із клаузою return. Для того, щоб не створювати подібні класи, для таких ось випадків, особливо коли таких полів може бути декілька, пропонують використовувати розумні вказівники: об’єкти, які зберігають вказівники на динамічно аллоковані ділянки пам’яті довільного типу. При чому вони автоматично звільняють пам’ять при виході з області видимості. Найпростіший розумний вказівник:

template <typename T>
class smart_pointer {
	T *m_obj;
	public:
	// Віддаємо в управління деякий об’єкт 
	smart_pointer(T *obj) : m_obj(obj)
	{ }
	// По виходу з області видимості цей об’єкт знищується
	~smart_pointer() {
		delete m_obj;
	}
	// Перевантажені оператори
	// Селектор. Дозволяє звертатися до даних типу T за допомогою “стрілочки”
	T* operator->() { return m_obj; }
	// За допомогою такого оператора ми можемо розіменувати вказівник і 	отримати посилання на об’єкт, которий він зберігає
	T& operator* () { return *m_obj; }
};
	
int test() {
	// Віддаємо myClass в управління розумному вказівнику.

	smart_pointer<MyClass> pMyClass(new MyClass(/*параметри*/);    
	// Звертаємось до методу класу MyClass за допомогою селектору
	pMyClass->something();

	// Допустимо, що для нашого класу існує функція виводу його в ostream.
	// Ця функція зазвичай отримує посилання на об’єкт,
	// котрий має вивестись на екран
	std::cout << *pMyClass << std::endl;

	// по виходу із зони видимості об’єкт MyClass буде видалений.
}

Розглянемо конкретні реалізації розумних вказівників у відомих бібліотеках розширення С++:

  • boost::scoped_ptr
  • std::auto_ptr
  • std::tr1::shared_ptr (він же std::shared_ptr в C++11, або boost::shared_ptr із boost)

boost::scoped_ptr[ред.]

Він находиться в бібліотеці boost. Реалізація його проста і зрозуміла, практично ідентична нашій, з наявністю декількох відмінностей. Цей вказівник не можна скопіювати (тобто в нього приватні конструктор копій і оператор присвоювання). Наприклад:

#include <boost/scoped_ptr.hpp>
int test() {
	boost::scoped_ptr<int> p1(new int(6));
	boost::scoped_ptr<int> p2(new int(1));    
	p1 = p2; // Помилка!
}

Якби було дозволено присвоювання, то і p1 і p2 вказували б на одну і ту саму область пам'яті, а по виходу із функції обидва б видалились, що б призвело до помилки з неоднозначною поведінкою. Відповідно, цей вказівник не можна передавати також у функції.

scoped_ptr – шаблонний клас, який зберігає вказівник на динамічно створений об'єкт. (за допомогою виразу new). Об'єкт гарантовано буде видалений, в разі знищення scoped_ptr, або в разі явного знищення.

Приклад використання scoped_ptr:

#include <boost/scoped_ptr.hpp>
#include <iostream>

struct Shoe { ~Shoe() { std::cout << "Buckle my shoe\n"; } };

class MyClass {
    boost::scoped_ptr<int> ptr;
  public:
    MyClass() : ptr(new int) { *ptr = 0; }
    int add_one() { return ++*ptr; }
};

int main()
{
    boost::scoped_ptr<Shoe> x(new Shoe);
    MyClass my_instance;
    std::cout << my_instance.add_one() << '\n';
    std::cout << my_instance.add_one() << '\n';
}

Результат програми: 1 2 Buckle my shoe

std::auto_ptr[ред.]

Трохи більш досконалий варіант попереднього. Для нього реалізований оператор присвоювання і конструктор копій, але праціює він своєрідно:

#include <memory>
int test() {
	std::auto_ptr<MyObject> p1(new MyObject);
	std::auto_ptr<MyObject> p2;    
	p2 = p1;
}

Після виконання операції присвоювання, новий об'єкт MyObject буде знаходитись в p2, а p1 не буде містити нічого. Це так звана семантика переміщення. Оператор копіювання працює таким самим чином. Це потрібно, наприклад, у разі, коли якась функція буде створювати об'єкт і повертати його:

std::auto_ptr<MyObject> giveMeMyObject();

Функція повертає вам тільки що створений об'єкт, але таким чином ви впевнені, що вказівник на цей об'єкт не зберігається ще в якомусь місці. В силу такої поведінки вказівник не можна використовувати в контейнерах STL. Для цих цілей використовують shared_ptr.

std::shared_ptr (С++11)[ред.]

Розумний вказівник з підрахунком посилань. Це означає, що десь існує деяка змінна, яка зберігає кількість вказівників, які посилаються на даний об’єкт. Якщо ця змінна приймає значення 0, то об’єкт знищується. Лічільник збільшується при виклику або оператору копіювання, або оператору присвоювання. Також у вказівника shared реалізовано оператор приведення типів до bool, що дає нам можливість використовувати його в логічних виразах і перевіряти його наявність.

#include <memory>
#include <iostream>
int test() {
	std::shared_ptr<MyObject> p1(new MyObject);
	std::shared_ptr<MyObject> p2;    
	p2 = p1;    
	if (p2)
		std::cout << "Hello, world!\n";
}

Тепер p2 і p1 вказують на один об'єкт, але лічильник посилань рівний двом. По виходу із області видимості лічильник стає рівним нулю, і об'єкт знищується. Ми можемо передати цей вказівник у функцію:

int test(std::shared_ptr<MyObject> p1) {
	// робимо щось
}

Якщо ви передасте вказівник за посиланням, то лічильник не буде збільшений, ви маєте бути впевнені, що об'єкт MyObject не буде знищено, доки буде виконуватись функція test.

Недоліки[ред.]

Недоліки використання розумних вказівників:

  • Це робить програму повільнішою, адже потребує виконання додаткових операцій.
  • Це може призвести до коду, який важко сприймати наприклад:
std::tr1::shared_ptr<MyNamespace::Object> ptr = std::tr1::shared_ptr<MyNamespace::Object>(new MyNamespace::Object(param1, param2, param3))

Для того, щоб спростити такі вирази користуються деклараціями препроцесора, наприклад:

#define sptr(T) std::tr1::shared_ptr<T>
  • Існує проблема циклічних посилань.
  1. habrahabr.ru - Smart pointers для начинающих