Шаб­лоны мож­но наз­вать самым глав­ным отли­чием и основным пре­иму­щес­твом язы­ка C++. Воз­можность соз­дать шаб­лон алго­рит­ма для раз­личных типов без копиро­вания кода и со стро­гой про­вер­кой типов — это лишь один из аспектов исполь­зования шаб­лонов. Код спе­циали­заций шаб­лона стро­ится на эта­пе ком­пиляции, а это зна­чит, что поведе­нием соз­дава­емых типов и фун­кций мож­но управлять. Как тут удер­жать­ся от воз­можнос­ти поп­рограм­мировать ком­пилиру­емые клас­сы?

Ме­тап­рограм­мирова­ние ста­новит­ся столь же неотъ­емле­мой частью написа­ния кода на C++, как и исполь­зование стан­дар­тной биб­лиоте­ки, часть которой соз­дана имен­но для исполь­зования на эта­пе ком­пиляции. Сегод­ня мы про­изве­дем на свет биб­лиоте­ку безопас­ного при­веде­ния ска­ляр­ных типов C++, метап­рограм­мируя шаб­лонами!

 

Разрыв шаблона

На самом деле все метап­рограм­мирова­ние сво­дит­ся не столь­ко к шаб­лонно­му поведе­нию, незави­симо от типа, сколь­ко к наруше­нию это­го самого шаб­лона поведе­ния. Допус­тим, у нас есть шаб­лонный класс или шаб­лонная фун­кция:

template <class T>
class Some;
template <class T>
T func(T const& value);

Как пра­вило, такие клас­сы и фун­кции опи­сыва­ют сра­зу с телом, общим для любого типа. Но ник­то не меша­ет нам задать явную спе­циали­зацию шаб­лона по одно­му из типов, соз­дав для это­го типа уни­каль­ное поведе­ние фун­кции или осо­бый вид клас­са:

template <>
class Some<int>
{
public:
explicit Some(int value)
: m_twice(value * 2) {
}
int get_value() const {
return m_twice / 2;
}
private:
int m_twice;
};
template <>
double func(double const& value)
{
return std::sqrt(value);
}

При этом общее поведе­ние может опи­сывать­ся силь­но отлично от ука­зан­ного для спе­циали­заций шаб­лона:

template <class T>
class Some
{
public:
explicit Some(T const& value)
: m_value(value) {
}
T const& get_value() const {
return m_value;
}
private:
T m_value;
};
template <class T>
T func(T const& value)
{
return value * value;
}

В этом слу­чае при исполь­зовании шаб­лона будет наб­людать­ся осо­бое поведе­ние для спе­циали­заций Some<int> и func<double>: оно будет силь­но рас­ходить­ся с общим поведе­нием шаб­лона, хотя внеш­не API отли­чать­ся будет нез­начитель­но. Зато при соз­дании экзем­пля­ры Some<int> будут хра­нить удво­енное зна­чение и выдавать исходное зна­чение, деля пополам свой­ство m_twice по зап­росу get_value(). Общий шаб­лон Some<T>, где T — любой тип, кро­ме int, будет прос­то сох­ранять передан­ное зна­чение, выдавая кон­стантную ссыл­ку на поле m_value при каж­дом зап­росе get_value().

Фун­кция func<double> и вов­се вычис­ляет корень зна­чения аргу­мен­та, в то вре­мя как любая дру­гая спе­циали­зация шаб­лона func<T> будет вычис­лять квад­рат передан­ного зна­чения.

За­чем это нуж­но? Как пра­вило, для того, что­бы сде­лать логичес­кую раз­вилку внут­ри шаб­лонно­го алго­рит­ма, нап­ример такого:

template <class T>
T create()
{
Some<T> some(T());
return func(some.get_value());
}

По­веде­ние алго­рит­ма внут­ри create будет отли­чать­ся для типов int и double. При этом отли­чать­ся будет поведе­ние раз­личных ком­понент алго­рит­ма.
Нес­мотря на нелогич­ность кода спе­циали­заций шаб­лона, мы получи­ли прос­той и понят­ный при­мер управле­ния поведе­ния шаб­лонами.

 

Разрыв несуществующего шаблона

Да­вай сде­лаем наш при­мер чуть более веселым — убе­рем общий шаб­лон поведе­ния для Some и func, оста­вив лишь уже написан­ные спе­циали­зации Some и func и, конеч­но же, не тро­гая пред­варитель­ное объ­явле­ние.

Что в этом слу­чае про­изой­дет с шаб­лоном create<T>? Он прос­то перес­танет ком­пилиро­вать­ся для любого типа. Ведь для create<int> не сущес­тву­ет реали­зации фун­кции func<int>, а для create<double> нет нуж­ного Some<double>. Пер­вая же попыт­ка вста­вить в код вызов create для какого‑либо типа при­ведет к ошиб­ке ком­пиляции.

Что­бы оста­вить воз­можность работать фун­кции create<T>, нуж­но спе­циали­зиро­вать Some<T> и func<T> хотя бы от одно­го типа одновре­мен­но. Мож­но реали­зовать Some<double> или func<int>, нап­ример так:

template <>
int func(int const& value)
{
return value;
}
template <>
class Some<double>
{
public:
explicit Some(double value)
: m_value(value*value) {
}
double get_value() const {
return m_square;
}
private:
double m_square;
};

До­бавив две спе­циали­зации, мы не толь­ко ожи­вили ком­пиляцию спе­циали­заций create от типов int и double, получи­лось еще и так, что воз­вра­щать для этих типов алго­ритм будет одни и те же зна­чения. Но поведе­ние при этом будет раз­ным!

info

В C++ типы ведут себя по‑раз­ному и не всег­да шаб­лонный алго­ритм ведет себя эффектив­но для всех типов. Зачас­тую, добавив спе­циали­зацию шаб­лона, мы получа­ем не толь­ко при­рост про­изво­дитель­нос­ти, но и более понят­ное поведе­ние прог­раммы в целом.

 

Да поможет нам std::

С каж­дым годом в стан­дар­тную биб­лиоте­ку добав­ляет­ся все боль­ше инс­тру­мен­тов для метап­рограм­мирова­ния. Как пра­вило, все новое — это хорошо опро­бован­ное ста­рое, поза­имс­тво­ван­ное из биб­лиоте­ки Boost.MPL и уза­конен­ное. Нам все чаще тре­бует­ся #include <type_traits>, и все боль­ше кода идет с при­мене­нием раз­вилок вида std::enable_if, все боль­ше нам тре­бует­ся знать на эта­пе ком­пиляции, не явля­ется ли аргу­мент шаб­лона целочис­ленным типом std::is_integral, или, нап­ример, срав­нить два типа внут­ри шаб­лона с помощью std::is_same, что­бы управлять поведе­нием спе­циали­заций шаб­лона.

Вспо­мога­тель­ные струк­туры шаб­лона выс­тро­ены так, что ком­пилиру­ется толь­ко та спе­циали­зация, что дает истинность выраже­ния, а спе­циали­зации для лож­ного поведе­ния отсутс­тву­ют.

Что­бы ста­ло более понят­но, рас­смот­рим под­робнее std::enable_if. Этот шаб­лон зависит от истиннос­ти пер­вого сво­его аргу­мен­та (вто­рой опци­она­лен), и выраже­ние вида std::enable_if<predicate>::type будет ском­пилиро­вано лишь для истинных выраже­ний, дела­ется это доволь­но прос­то — спе­циали­заци­ей от зна­чения true:

template <bool predicate_value,
class result_type = void>
struct enable_if;
template<class result_type>
struct enable_if<true, result_type>
{
typedef result_type type;
};

Для зна­чения false типа std::enable_if<P, T>::type ком­пилятор прос­то не смо­жет соз­дать, и это мож­но исполь­зовать, нап­ример огра­ничив поведе­ние ряда типов час­тичной спе­циали­зации шаб­лонной струк­туры или клас­са.

Здесь в помощь в качес­тве аргу­мен­тов std::enable_if могут быть исполь­зованы самые раз­нооб­разные струк­туры‑пре­дика­ты из того же <type_traits>: std::is_signed<T>::value истинно, если тип T под­держи­вает тип знак + или - (что очень удоб­но для отсе­чения поведе­ния без­зна­ковых целых), std::is_floating_point<T>::value истинно для вещес­твен­ных типов float и double, std::is_same<T1, T2>::value истинно, если типы T1 и T2 сов­пада­ют.
Струк­тур пре­дика­тов, помога­ющих нам, мно­жес­тво, а если чего не хва­тает в std:: или boost::, мож­но зап­росто сде­лать свою струк­туру.

Что ж, ввод­ная часть завер­шена, перехо­дим к прак­тике.

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

Пре­дикат — это обыч­ная час­тичная спе­циали­зация шаб­лонной струк­туры. Нап­ример, для std::is_same в общем слу­чае все выг­лядит при­мер­но так:

template <class T1, class T2>
struct is_same;
template <class T>
struct is_same<T,T>
{
static const bool value = true;
};
template <class T1, class T2>
struct is_same
{
static const bool value = false;
};

Для сов­пада­ющих типов аргу­мен­тов std::is_same ком­пилятор C++ выберет под­ходящую спе­циали­зацию, в дан­ном слу­чае час­тичную с value = true, а для несов­пада­ющих попадет в общую реали­зацию шаб­лона с value = false. Ком­пилятор всег­да пыта­ется отыс­кать стро­го под­ходящую спе­циали­зацию по типам аргу­мен­тов и, лишь не най­дя нуж­ную, идет в общую реали­зацию шаб­лона.

 

Вход по шаблону строго воспрещен

Что­бы начать прог­рамми­ровать прог­рам­мный код и занять­ся вся­чес­ким метап­рограм­мирова­нием, поп­робу­ем‑ка соз­дать страш­ную фун­кцию, воз­вра­щающую раз­ный резуль­тат для оди­нако­вых и раз­ных типов аргу­мен­тов шаб­лона. В этом нам поможет механизм час­тичной спе­циали­зации для вспо­мога­тель­ной струк­туры.
Пос­коль­ку час­тичной спе­циали­зации для фун­кций не сущес­тву­ет, внут­ри фун­кции мы будем прос­то обра­щать­ся к прос­той соот­ветс­тву­ющей спе­циали­зации струк­туры, у которой мы и зададим час­тичную спе­циали­зацию:

template <class result_type,
class value_type>
struct type_cast;
template <class result_type,
class value_type>
bool try_safe_cast(result_type& result,
value_type const& value)
{
return type_cast<result_type,
value_type>::try_cast(result, value);
}
template <class same_type>
struct type_cast<same_type, same_type>
{
static bool try_cast(result_type& result,
value_type const& value)
{
result = value;
return true;
}
}

Оче­вид­но, что мы соз­дали заготов­ку для фун­кции безопас­ного при­веде­ния типов. Фун­кция осно­выва­ется на типах передан­ных в нее аргу­мен­тов и идет выпол­нять ста­тичес­кий метод try_cast у соот­ветс­тву­ющей спе­циали­зации струк­туры type_cast. В нас­тоящий момент мы реали­зова­ли толь­ко три­виаль­ный слу­чай, ког­да тип зна­чения сов­пада­ет с типом резуль­тата и пре­обра­зова­ние, по сути, не нуж­но. Перемен­ной резуль­тата прос­то прис­ваивает­ся вхо­дящее зна­чение, и всег­да воз­вра­щает­ся true — приз­нак успешно­го при­веде­ния типа зна­чения к типу резуль­тата.

Для несов­пада­ющих типов сей­час будет выдана ошиб­ка ком­пиляции с длин­ным непонят­ным тек­стом. Что­бы нем­ного поп­равить это дело, необ­ходимо ввес­ти общую реали­зацию шаб­лона со static_assert(false, …) в теле метода try_cast — это сде­лает сооб­щение об ошиб­ке более понят­ным:

template <class result_type,
class value_type>
struct type_cast
{
static bool try_cast(result_type&,
value_type const&)
{
static_assert(false,
"Здесь нужно понятное сообщение об ошибке");
}
}

Та­ким обра­зом, каж­дый раз, ког­да будет про­изве­дена попыт­ка при­веде­ния типа фун­кци­ей try_safe_cast типов, для которых нет соот­ветс­тву­ющей спе­циали­зации струк­туры type_cast, будет выдавать­ся сооб­щение об ошиб­ке ком­пиляции из обще­го шаб­лона.

За­готов­ка готова, пора прис­тупать к метап­рограм­мирова­нию!

 

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

Для начала нуж­но поп­равить объ­явле­ние вспо­мога­тель­ной струк­туры type_cast. Нам пот­ребу­ется допол­нитель­ный тип meta_type для логичес­кой раз­вилки без ущер­ба для переда­ваемых парамет­ров и неяв­ного опре­деле­ния их типов. Теперь опи­сание шаб­лона струк­туры будет выг­лядеть чуть слож­нее:

template <class result_type,
class value_type,
class meta_type = void>
struct type_cast;

Как вид­но, новый тип в объ­явле­нии шаб­лона опци­она­лен и никак не меша­ет уже сущес­тву­ющим объ­явле­ниям спе­циали­зации и обще­го поведе­ния шаб­лона. Одна­ко этот малень­кий нюанс поз­воля­ет нам управлять успешностью ком­пиляции, переда­вая треть­им парамет­ром резуль­тат std::enable_if<предикат>::value. Спе­циали­зации с неком­пилиру­емым парамет­ром шаб­лона будут отбро­шены, что нам и нуж­но, что­бы управлять логикой при­веде­ния типов раз­личных групп.

Ведь оче­вид­но, что целые чис­ла при­водят­ся друг к дру­гу по‑раз­ному, в зависи­мос­ти от того, есть ли у обо­их типов знак, какой тип боль­шей раз­ряднос­ти и не выходит ли передан­ное зна­чение value за пре­делы допус­тимых зна­чений для result_type.
Так, если оба типа — зна­ковые целые и тип резуль­тата боль­шей раз­ряднос­ти, нежели тип вхо­дяще­го зна­чения, то мож­но без проб­лем прис­воить резуль­тату вхо­дящее зна­чение, это же вер­но и для без­зна­ковых типов. Давай опи­шем это поведе­ние спе­циаль­ной час­тичной спе­циали­заци­ей шаб­лона type_cast:

template <class result_type,
class value_type>
struct type_cast<result_type, value_type,
typename std::enable_if<...>::value>
{
static bool try_cast(result_type& result,
value_type const& value) {
result = value;
return true;
}
};

Те­перь нуж­но разоб­рать­ся, что за усло­вие нам нуж­но вста­вить вмес­то мно­гото­чия парамет­ром std::enable_if.

По­еха­ли опи­сывать усло­вие вре­мени ком­пиляции:

typename std::enable_if<

Во‑пер­вых, спе­циали­зация не дол­жна пересе­кать­ся с уже сущес­тву­ющей, где тип резуль­тата и вхо­дяще­го зна­чения сов­пада­ют:

!std::is_same<result_type, value_type>::value &&

Во‑вто­рых, мы рас­смат­рива­ем слу­чай, ког­да оба аргу­мен­та шаб­лона — целочис­ленные типы:

std::is_integral<result_type>::value &&
std::is_integral<value_type>::value &&

В‑треть­их, мы под­разуме­ваем, что оба типа либо зна­ковые, либо без­зна­ковые (скоб­ки обя­затель­ны — усло­вия парамет­ров шаб­лона вычис­ляют­ся ина­че, нежели на эта­пе выпол­нения!):

(std::is_signed<result_type>::value ==
std::is_signed<value_type>::value) &&

В‑чет­вертых, раз­рядность целочис­ленно­го типа резуль­тата боль­ше, чем раз­рядность типа передан­ного зна­чения (сно­ва обя­затель­ны скоб­ки!):

(sizeof(result_type) > sizeof(value_type))

И наконец, зак­рыва­ем объ­явле­ние std::enable_if:

::type

В резуль­тате type для std::enable_if будет сге­нери­рован толь­ко при выпол­нении ука­зан­ных четырех усло­вий. В осталь­ных слу­чаях для про­чих ком­бинаций типов дан­ная час­тичная спе­циали­зация даже не будет соз­дана.

По­луча­ется зубод­робитель­ное выраже­ние внут­ри std::enable_if, которое отсе­кает исклю­читель­но ука­зан­ный нами слу­чай. Дан­ный шаб­лон спа­сает от тиражи­рова­ния кода при­веде­ния раз­личных целочис­ленных типов друг в дру­га.

Что­бы зак­репить матери­ал, мож­но опи­сать чуть более слож­ный слу­чай — при­веде­ние без­зна­ково­го целого к типу мень­шей раз­ряднос­ти без­зна­ково­го целого. Тут нам поможет зна­ние бинар­ного пред­став­ления целого чис­ла и стан­дар­тный класс std::numeric_limits:

template <typename result_type, typename value_type>
struct type_cast<result_type, value_type,
typename std::enable_if<...>::type>
{
static bool try_cast(result_type& result,
value_type const& value)
{
if (value != (value &
std::numeric_limits<result_type>::max()))
{
return false;
}
result = result_type(value);
return true;
}
};

В усло­вии if все дос­таточ­но прос­то: мак­сималь­ное зна­чение типа result_type неяв­но при­водит­ся к типу боль­ше раз­ряднос­ти value_type и выс­тупа­ет в качес­тве мас­ки для зна­чения value. В слу­чае если для зна­чения value задей­ство­ваны биты вне result_type, мы получим выпол­ненное неравенс­тво и попадем на return false.

Те­перь прой­дем по усло­вию вре­мени ком­пиляции:

typename std::enable_if<

Пер­вые два усло­вия оста­ются теми же — оба типа целочис­ленные, но раз­личные меж­ду собой:

!std::is_same<result_type, value_type>::value &&
std::is_integral<result_type>::value &&
std::is_integral<value_type>::value &&

Оба типа явля­ются без­зна­ковы­ми целыми:

std::is_unsigned<result_type>::value &&
std::is_unsigned<value_type>::value &&

Тип резуль­тата мень­шей раз­ряднос­ти, нежели тип вхо­дяще­го зна­чения (скоб­ки обя­затель­ны!):

(sizeof(result_type) < sizeof(value_type))

Все усло­вия перечис­лены, зак­рыва­ем усло­вие спе­циали­зации:

::type

Для зна­ковых целых, где резуль­тат мень­шей раз­ряднос­ти, усло­вие будет похожим, но с дву­мя std::is_signed внут­ри std::enable_if, одна­ко усло­вие выхода за пре­делы зна­чений будет нес­коль­ко дру­гим:

static bool try_cast(result_type& result, value_type const& value)
{
if (value != (value &
(std::numeric_limits<result_type>::max() |
std::numeric_limits<value_type>::min())))
{
return false;
}
result = result_type(value);
return true;
}

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

В качес­тве домаш­него задания рас­смот­ри сле­дующие слу­чаи:

  1. При­веде­ние зна­ково­го к без­зна­ково­му с исполь­зовани­ем уже написан­ных спе­циали­заций и модифи­като­ра std::make_unsigned.
  2. При­веде­ние без­зна­ково­го к зна­ково­му боль­шей раз­ряднос­ти с исполь­зовани­ем уже написан­ных спе­циали­заций и модифи­като­ра std::make_signed.
  3. Чуть пос­ложнее: при­веде­ние без­зна­ково­го к зна­ково­му мень­шей или рав­ной раз­ряднос­ти с исполь­зовани­ем усло­вия невыхо­да за пре­делы зна­чений и модифи­като­ра std::make_signed.

Так­же не сос­тавит тру­да написать ана­логич­ные спе­циали­зации для пре­обра­зова­ния из std::is_floating_point типов, а так­же пре­обра­зова­ние из типа bool. Для пол­ного удов­летво­рения мож­но дописать при­веде­ние из и в стро­ковые типы и офор­мить это столь нуж­ной всем биб­лиоте­кой безопас­ного при­веде­ния типов C++.

 

Нешаблонное мышление

Для каж­дого слу­чая исполь­зования шаб­лона может сущес­тво­вать исклю­чение. Теперь ты будешь готов встре­тить­ся с ним и гра­мот­но его обра­ботать. Не всег­да нужен спе­циаль­ный метатип в шаб­лоне вспо­мога­тель­ной струк­туры, но если приш­ла пора обра­баты­вать пре­дика­ты на эта­пе ком­пиляции — что ж, в этом нет ничего страш­ного. Все, что нуж­но, — это засучить рукава и акку­рат­но соз­дать шаб­лонную конс­трук­цию с пре­дика­том вре­мени ком­пиляции.

Но будь акку­ратен, зло­упот­ребле­ние шаб­лонами до доб­ра не доводит! Отно­сись к шаб­лонам исклю­читель­но как к обоб­щению кода для раз­ных типов со схо­жим поведе­нием, шаб­лоны дол­жны появ­лять­ся обос­нован­но, ког­да есть риск тиражи­рова­ния оди­нако­вого кода для раз­ных типов.

Пом­ни так­же о том, что для того, что­бы разоб­рать­ся в логике шаб­лонно­го пре­дика­та без авто­ра кода, нуж­но быть как минимум сме­лым опти­мис­том, поэто­му береги пси­хику кол­лег, офор­мляй шаб­лонные пре­дика­ты акку­рат­но, кра­сиво и читабель­но и не стес­няй­ся ком­менти­ровать чуть ли не каж­дое усло­вие в пре­дика­те.

Шаб­лонизи­руй код акку­рат­но и лишь по необ­ходимос­ти, и кол­леги ска­жут тебе спа­сибо. И не бой­ся ломать шаб­лон в слу­чае исклю­чения из пра­вил. Пра­вила без исклю­чений — это, ско­рее, исклю­чения из пра­вил.

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии