Шаблонне метапрограмування

Матеріал з Вікіпедії — вільної енциклопедії.
Перейти до навігації Перейти до пошуку

Шаблонне метапрограмування це техніка метапрограмування в якій шаблони використовуються компілятором для створення тимчасового вихідного коду, які за допомогою компілятора об'єднуються з усім вихідним кодом програми і компілюється. В результаті цих шаблонів під час компіляції отримують константи, структури даних, і цілі функції. Використання шаблонів можна розглядати як створення коду, що виконується під час компіляції. Ця техніка використовуються рядом мов, найвідоміша з яких C++, а також Curl, D, і XL.

Деякі мови підтримують схожі, але не більш потужні функції часу компіляції (це такі мови як Lisp із підтримкою макросів).

Компоненти шаблонного метапрограмування[ред. | ред. код]

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

Шаблонне метапрограмування є повним за Тюрингом, це означає, що будь-яке обчислення задане комп'ютерною програмою може бути обчислене, в деякій формі, за допомогою шаблонного метапрограмування.[1]

Шаблони відрізняються від макросів. Макрос, який також є властивістю мови програмування, яка дозволяє писати код, що виконується під час компіляції, вбудовує згенерований програмний код шляхом текстових маніпуляцій і заміни. Макро системи часто мають обмежені можливості розгалуження процесу під час компіляції і зазвичай недостатню обізнаність про семантику і систему типів мови, з якою вони використовуються (винятком може бути макроси мови Lisp, які самі написані на мові Lisp і здійснюють маніпуляції і заміщення коду Lisp представленого у вигляді структур даних, на відміну від тексту).

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

Використання шаблонного метапрограмування[ред. | ред. код]

Хоча синтаксис шаблонного метапрограмування зазвичай дуже відрізняється від мови програмування з якою він використовується, він має практичну користь. Однією з причин використовування шаблонів, це можливість реалізації загального коду програми (уникаючи схожого коду, що мають незначні відмінності) або виконувати автоматичну оптимізацію під час компіляції, таку як виконання якоїсь дії один раз під час компіляції, а не кожен раз під час виконання програми — наприклад, при наявності циклів в програмі, зменшити кількість підрахунку умови виходу з циклу чи підрахунку кількості циклів всякий раз при виконанні програми.

Генерація класу під час компіляції[ред. | ред. код]

Що насправді являє собою "програмування під час компіляції" можна показати на прикладі з функцією підрахунку факторіалу, яка без використання шаблонів C++, може бути реалізована за допомогою рекурсії як показано нижче:

unsigned int factorial(unsigned int n) {
	return n == 0 ? 1 : n * factorial(n - 1); 
}

// Приклади використання:
// factorial(0) дасть 1;
// factorial(4) дасть 24.

Код наведений в прикладі буде виконуватись під час роботи програми для визначення значення факторіалу для літералів 4 і 0. З використанням шаблонного метапрограмування і спеціалізації шаблонів, для реалізації умови закінчення для рекурсії для факторіалів, які використовуються в програмі, ігноруючи будь-які факторіали, які не використовуються і можуть бути підраховані під час компіляції за допомогою цього коду:

template <int n>
struct factorial {
	enum { value = n * factorial<n - 1>::value };
};

template <>
struct factorial<0> {
	enum { value = 1 };
};

// Приклади використання:
// factorial<0>::value дасть 1;
// factorial<4>::value дасть 24.

Наведений код розраховує значення факторіала для літералів 4 і 0 під час компіляції і використовує результат так ніби вони є константами, що були розраховані заздалегідь. Для того, щоб мати змогу використовувати шаблони таким чином, компілятор має знати значення параметру під час компіляції. Іншими словами значення буде відоме заздалегідь лише коли X є константним виразом.

В C++11, було додане ключове слово constexpr, яке дозволяє компілятору виконувати прості константні вирази. Використовуючи constexpr, можна застосовувати звичайну рекурсивну функції підрахунку факторіалу.[1] [Архівовано 16 лютого 2015 у Wayback Machine.]

Оптимізація коду під час компіляції[ред. | ред. код]

Код з підрахунком факторіалу, наведений раніше є прикладом оптимізації коду під час компіляції, при якій всі значення факторіалів, які використовуються в програмі пре-компілюються і вводяться в код як числові константи під час компіляції, тим самим заощаджуючи накладні витрати на час виконання і обсяг пам'яті. Але, тим не менш, це відносно не значна оптимізація.

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

template <int length>
Vector<length>& Vector<length>::operator+=(const Vector<length>& rhs) 
{
    for (int i = 0; i < length; ++i)
        value[i] += rhs.value[i];
    return *this;
}

Коли компілятор створює екземпляр шаблонної функції, описаної вище, може бути отриманий наступний код:

template <>
Vector<2>& Vector<2>::operator+=(const Vector<2>& rhs) 
{
    value[0] += rhs.value[0];
    value[1] += rhs.value[1];
    return *this;
}

Оптимізатор компілятора може розгорнути цикл for оскільки параметр шаблону length, що задає його довжину відомий під час компіляції.

Однак, слід мати на увазі, що це може призвести до значного розростання об'єму коду, який генерується для кожного 'N' (розмір вектора), екземпляр якого ви створили.

Статичний поліморфізм[ред. | ред. код]

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

class Base
{
public:
    virtual void method() { std::cout << "Base"; }
    virtual ~Base() {}
};

class Derived : public Base
{
public:
    virtual void method() { std::cout << "Derived"; }
};

int main()
{
    Base *pBase = new Derived;
    pBase->method(); //outputs "Derived"
    delete pBase;
    return 0;
}

Де всі виклики віртуальних методів (із ключовим словом virtual) будуть викликати методи найнижчих класів в ієрархії наслідування. Ця динамічна поліморфна поведінка, зазвичай досягається використанням спеціальних віртуальних таблиць функцій для класів із віртуальними методами, по цим таблицям здійснюється прохід під час виконання, для знаходження методу, який необхідно викликати. Таким чином, поліморфізм під час виконання обов'язково має додаткові витрати на виконання коду(хоча для сучасних архітектур ці накладні витрати малі).

Тим не менш, в багатьох випадках поліморфна поведінка, яка потребується в коді інваріантна і може бути визначена під час компіляції. Тоді може бути використаний шаблон "recurring template pattern" для досягнення ефекту статичного поліморфізму, який є імітацією поліморфізму у вигляді програмного коду, але він вирішується під час компіляції і тому не потребує звернень до віртуальних таблиць під час виконання. Наприклад:

template <class Derived>
struct base
{
    void interface()
    {
         // ...
         static_cast<Derived*>(this)->implementation();
         // ...
    }
};

struct derived : base<derived>
{
     void implementation()
     {
         // ...
     }
};

Тут шаблон базового класу користується тим, що реалізації функції члена ініціалізуються лише після їх оголошення, і він буде використовувати члени похідного класу у своїх власних функціями через використання статичного приведення типів static_cast, таким чином, при компіляції створюються об'єкт, який має поліморфні характеристики. Як приклад наглядного використання, така техніка використана в бібліотеці Boost для створення ітераторів.[2] [Архівовано 24 липня 2008 у Wayback Machine.].

Інший подібний приклад використання це "трюк Бартона-Накмана" (Barton–Nackman trick),який іноді називають "обмеженим розширенням шаблону" ("restricted template expansion"), де спільна функціональність розміщується в базовому класі, який використовується не як умовний, а як необхідний компонент для забезпечення сумісної поведінки мінімізуючи надмірність коду.

Переваги та недоліки шаблонного метапрограмування[ред. | ред. код]

Компроміс між часом компіляції і часом виконання
При значному застосуванні шаблонного метопрограмування компіляція може стати повільною. В залежності від способу використання, шаблони можуть компілюватися швидше або повільніше ніж розгорнутий код.
Узагальнене програмування
Шаблонне метопрограмування дозволяє програмісту зосередитися на архітектурі і делегувати компіляцію створення будь-якої реалізації необхідних у клієнтському коді. Таким чином, шаблонне метапрограмування може втілювати по справжньому універсальний код, мінімізуючи код і полегшуючи його підтримку.
Читабельність
Що стосується мови C++, синтаксис і ідіоми шаблонного програмування таємничі в порівнянні зі звичайним програмуванням на C++, і шаблонні метапрограми можуть бути важкими для розуміння. Таким чином метапрограми можуть викликати складності при їх підтримці у недосвідчених в шаблонному програмування програмістів, в залежності від складності синтаксису і реалізації. [2][3]

Див. також[ред. | ред. код]

Примітки[ред. | ред. код]

  1. Veldhuizen, Todd L. C++ Templates are Turing Complete (PDF). http://ubietylab.net/. Indiana U. CS dept. Архів оригіналу (PDF) за 23 листопада 2015. Процитовано 18 грудня 2014.
  2. DSL implementation in metaocaml, template haskell, and C++. — University of Waterloo, University of Glasgow, Research Centre Julich, Rice University, 2004. Архівовано з джерела 5 березня 2016. Процитовано 16 лютого 2015.
  3. Template Meta-programming for Haskell. — ACM 1-58113-415-0/01/0009, 2002. Архівовано з джерела 1 березня 2015. Процитовано 16 лютого 2015.

Зовнішні посилання[ред. | ред. код]