Изповедите на един метапрограмист

изповедите

Шаблоните могат да се нарекат най-важната разлика и основното предимство на езика C ++. Възможността за създаване на шаблон на алгоритъм за различни типове без копиране на код и със силна проверка на типа е само един аспект от използването на шаблони. Специализациите на шаблона се кодират по време на компилиране, което означава, че можете да контролирате поведението на генерираните типове и функции. Как можете да устоите на възможността да програмирате компилирани класове?

Метапрограмирането става също толкова част от писането на C++ код, колкото и използването на стандартната библиотека, част от която е проектирана специално за използване по време на компилиране. Днес ще създадем C++ скаларен тип безопасна кастинг библиотека чрез метапрограмиране на шаблон!

Прекъсване на шаблона

Всъщност цялото метапрограмиране се свежда не толкова до модел на поведение, независимо от вида, а до нарушаване на този модел на поведение. Да кажем, че имаме шаблонен клас или шаблонна функция:

По правило такива класове и функции се описват веднага с тяло, общо за всеки тип. Но никой не ни притеснява да зададем изрична специализация на шаблона за един от типовете, създавайки за този тип уникално функционално поведение или специален вид клас:

Въпреки това, общото поведение може да бъде описано много различно от шаблона, посочен за специализации:

В този случай, когато използвате шаблона, ще има специално поведение за специализациите `Some` и `func`: то ще бъде много различно от общото поведение на шаблона, въпреки че външният API ще се различава леко. Но когато бъдат създадени, екземплярите „Някои“ ще се съхраняватудвоете стойността и върнете първоначалната стойност, като намалите наполовина свойството `m_twice`, когато бъде поискано от `get_value()`. Общият шаблон „Някои“, където T е всеки тип, различен от int, просто ще съхранява предадената стойност, като дава постоянна препратка към полето „m_value“ при всяка заявка „get_value()“.

Функцията `func` изобщо изчислява коренната стойност на аргумента, докато всяка друга специализация на шаблона `func` ще изчисли квадрата на предадената стойност.

Защо е необходимо това? Като правило, за да направите логическо разклонение в алгоритъм на шаблон, например:

Поведението на алгоритъма вътре в create ще бъде различно за типове int и double. В този случай поведението на различните компоненти на алгоритъма ще се различава. Въпреки нелогичността на кода за специализация на шаблона, получихме прост и разбираем пример за контрол на поведението на шаблона.

Разбиване на несъществуващ шаблон

Какво се случва с шаблона „създай“ в този случай? Той просто ще спре да компилира за всеки тип. В крайна сметка за `create` няма имплементация на функцията `func`, а за `create` няма необходимото `Some`. Първият опит за вмъкване на извикване за създаване за произволен тип в кода ще доведе до грешка при компилиране.

За да позволите на функциите `create` да работят, трябва да специализирате `Some` и `func` от поне един тип едновременно. Можете да имплементирате `Some` или `func` така:

Чрез добавянето на две специализации, ние не само съживихме компилацията на създаване на специализации от типове int и double, но също така се оказа, че алгоритъмът ще върне същите стойности за тези типове. Но поведението ще е различно!

В C++ типовете се държат различно и алгоритъмът на шаблона не винаги се държи ефективно за всички типове. Често, като добавим специализация на шаблон, получаваме не самоповишаване на производителността, но и по-разбираемо поведение на програмата като цяло.

Помогнете ни std::

Всяка година към стандартната библиотека се добавят повече инструменти за метапрограмиране. По правило всичко ново е добре тествано старо, заимствано от библиотеката Boost.MPL и легализирано. Имаме все по-голяма нужда от `#include` и все повече и повече код използва разклонения като `std::enable_if`, все повече и повече трябва да знаем по време на компилиране дали даден аргумент на шаблона е целочислен тип `std::is_integral` или, например, да сравним два типа в шаблон, използвайки `std::is_same`, за да контролираме поведението на специализациите на шаблона.

Помощните структури на шаблона са изградени така, че да се компилира само специализацията, която прави израза верен, и няма специализации за грешно поведение.

За да стане по-ясно, нека разгледаме по-отблизо `std::enable_if`. Този шаблон зависи от истинността на първия си аргумент (вторият е незадължителен) и израз като `std::enable_if::type` ще бъде компилиран само за истински изрази, това се прави съвсем просто - чрез специализация от истинската стойност:

За невярна стойност от тип `std::enable_if

::type` просто не може да бъде генериран от компилатора и това може да се използва, например чрез ограничаване на поведението на набор от частични типове специализация на шаблонна структура или клас.

Тук голямо разнообразие от предикатни структури от същия `` може да се използва като аргументи на `std::enable_if`: `std::is_signed::value` е вярно, ако тип T поддържа знак + или - (което е много удобно за прекъсване на поведението на цели числа без знак), `std::is_floating_point::value` е вярно за float и double реални типове, `std::is_sam e ::value` е вярно, ако типовете T1и Т2 съвпадение. Има много предикатни структури, които ни помагат и ако нещо липсва в `std::` или `boost::`, можете лесно да създадете своя собствена структура.

Е, уводната част е завършена, нека да преминем към практиката.

Как са структурирани предикатите?

Предикатът е само частична специализация на шаблонна структура. Например за `std::is_same` като цяло всичко изглежда така:

За съвпадащи типове аргументи `std::is_same` компилаторът на C++ ще избере подходящата специализация, в този случай частична специализация със стойност = true, а за несъвпадащи типове ще попадне в общата реализация на шаблон със стойност = false. Компилаторът винаги се опитва да намери строго подходяща специализация за типовете аргументи и само като не намери правилния, преминава към общата реализация на шаблона.

Въвеждането на шаблони е строго забранено

За да започнете да програмирате код и да правите всякакви видове метапрограмиране, нека се опитаме да създадем страшна функция, която връща различен резултат за едни и същи и различни типове аргументи на шаблона. В това ще ни помогне механизмът на частична специализация за спомагателната структура. Тъй като няма частична специализация за функции, вътре във функцията просто ще се позоваваме на простата съответстваща специализация на структурата, за която ще зададем частичната специализация:

Очевидно сме създали мъниче за функцията за безопасно преобразуване на типове. Функцията се основава на типовете на предадените й аргументи и отива да изпълни статичния метод `try_cast` на съответната специализация на структурата `type_cast`. В момента сме реализирали само тривиален случай, когато типът на стойността е същият като типа на резултата и всъщност не е необходимо преобразуване. На резултатната променлива просто се присвоява входната стойност и винаги се връща true - знакуспешно прехвърляне на типа стойност към типа резултат.

Несъответстващите типове вече ще генерират грешка при компилиране с дълъг объркващ текст. За да поправите това малко, трябва да въведете обща имплементация на шаблон с `static_assert(false, ...)` в тялото на метода `try_cast` - това ще направи съобщението за грешка по-ясно:

По този начин всеки път, когато функцията `try_safe_cast` се опита да преобразува типове, за които няма съответна специализация на структурата `type_cast`, ще бъде издадено съобщение за грешка при компилиране от общия шаблон.

Заготовката е готова, време е да започнете метапрограмирането!

Програмирайте ме тук!

В края на краищата е очевидно, че целите числа се преобразуват едно в друго по различни начини, в зависимост от това дали и двата типа имат знак, кой тип има по-голяма битова дълбочина и дали стойността на предадената стойност не надхвърля допустимите стойности за `result_type`. Така че, ако и двата типа са цели числа със знак и типът резултат е по-голям от типа на входната стойност, тогава можете лесно да присвоите входната стойност на резултата, същото важи и за типовете без знак. Нека опишем това поведение със специална частична специализация на шаблона `type_cast`:

Сега трябва да разберем какъв тип условие трябва да вмъкнем вместо многоточие с параметъра `std::enable_if`.

Нека да опишем условието по време на компилиране:

Първо, една специализация не трябва да се припокрива със съществуваща специализация, където типът на резултата и входната стойност са еднакви:

Второ, разглеждаме случая, когато и двата аргумента на шаблона са цели числа:

Трето, приемаме, че и двата типа са със знак или без знак (необходими са скоби - условията на параметрите на шаблона се оценяват по различен начин, отколкото в стъпкатаекзекуция!):

Четвърто, битовостта на целочисления тип на резултата е по-голяма от битовостта на типа на предадената стойност (скобите са необходими отново!):

В резултат на това типът за `std::enable_if` ще бъде генериран само ако посочените четири условия са изпълнени. В други случаи, за други комбинации от типове, тази частична специализация дори няма да бъде създадена.

Получава се яростен израз вътре в `std::enable_if`, който отрязва само посочения от нас регистър. Този шаблон ви спестява от репликиране на кода за прехвърляне на различни цели числа един в друг.

За да консолидираме материала, можем да опишем малко по-сложен случай - прехвърляне на цяло число без знак към типа на по-малка битова дълбочина на цяло число без знак. Тук познанията за двоичното представяне на цяло число и стандартния клас `std::numeric_limits` ще ни помогнат:

В условието if всичко е съвсем просто: максималната стойност на тип „result_type“ имплицитно се прехвърля към тип, по-голям от битовостта на „value_type“ и действа като маска за стойността на „value“. Ако стойността на `value` използва битове извън `result_type`, ще получим изпълненото неравенство и ще ударим return false.

Сега нека преминем през условието по време на компилация:

Първите две условия остават същите - и двата типа са цели числа, но се различават един от друг:

И двата типа са цели числа без знак:

Типът резултат е по-малък от типа на входната стойност (необходими са скоби!):

Всички условия са изброени, затворете условието за специализация:

За цели числа със знак, където резултатът е по-малък от битовата ширина, условието ще бъде подобно, но с две `std::is_signed` вътре в `std::enable_if`, но условието за извън границите ще бъде малко по-различно:

Запомнете отноводвоично представяне на цели числа със знак: тук маската ще бъде знаковият бит на входната стойност и битовете на стойността на резултатния тип, с изключение на знаковия бит. Съответно, минималният брой от типа `value_type`, където се попълва само знаковият бит, комбиниран побитово с максималния брой от типа `result_type`, където всички битове с изключение на знаковия бит са попълнени, ще ни даде желаната маска от валидни стойности.

За домашна работа разгледайте следните случаи:

  1. Преобразуване на подписани в неподписани с помощта на вече написани специализации и модификатора `std::make_unsigned`.
  2. Преобразуване на неподписан в подписан с по-голяма битовост с помощта на вече написани специализации и модификатора `std::make_signed`.
  3. Малко по-сложно: конвертиране на неподписано в подписано с по-малка или равна битова ширина, като се използва условието за стойности, които не са извън диапазона и модификатора `std::make_signed`.
Също така е лесно да се напишат подобни специализации за конвертиране от типове `std::is_floating_point`, както и конвертиране от тип `bool`. За пълно удовлетворение можете да добавите кастинг от и към типове низове и да го подредите с така необходимата C ++ библиотека за безопасни типове за преобразуване.

Нестандартно мислене

Може да има изключение за всяко използване на шаблона. Сега ще сте готови да се срещнете с него и да го обработите компетентно. Не винаги се нуждаете от специален метатип в шаблон на помощна структура, но ако е време да обработите предикатите по време на компилиране, добре, няма за какво да се притеснявате. Всичко, което трябва да направите, е да запретнете ръкави и внимателно да създадете шаблонна конструкция с предикат по време на компилация.

Но внимавайте, злоупотребата с шаблони не води до добро! Лекувайте моделиединствено като обобщение на код за различни типове с подобно поведение, шаблоните трябва да се показват разумно, когато има риск от репликиране на един и същ код за различни типове.

Кодирайте шаблона внимателно и само когато е необходимо и вашите колеги ще ви благодарят. И не се страхувайте да нарушите модела в случай на изключение от правилата. Правилата без изключения са по-скоро изключения от правилата.

изповедите