Метапрограммирование - 4 - тип expr и шаблоны

Еще некоторые интересные особенности метапрограммирования. На этот раз - шаблоны. При внимательном рассмотрении шаблоны очень красиво сочетаются с макросами. По сути, макросы и шаблоны - две стороны одной медали.
Вообще говоря, все, что можно сделать на шаблонах, можно сделать и с помощью макросов. Тогда зачем же нужны шаблоны? На самом деле макросы - это мощная и довольно сложная возможность, связанная с активным взаимодействием пользовательского программы и компилятора. Применение такого мощного (и в общем опасного) инструмента оправдано далеко не всегда. Шаблоны же предоставляют простой и удобный интерфейс, реализующий важную функцию макросов - подстановку кода (квазицитирование).


Тип expr
Любой фрагмент AST  внутри компилятора имеет тип данных expr. Этот тип данных доступен также и в обычном коде (не в макросах).

expr x = <[ if(k>0) k++; ]>;

Тип expr позволяет передавать любые фрагменты кода в макросы и шаблоны.

Шаблоны
Шаблоны решают некоторый класс задач, который конечно можно решить с помощью макросов.  Основная функция шаблонов - параметризация кода.
В С++ существуют шаблонные функции и классы. Параметрами шаблона могут быть или типы данных, или констант (причем почему-то только числовые константы - передать в шаблон, например, строку нельзя). При это синтаксис шаблонов в С++ крайне громоздкий. В C# синтакис уже заметно лучше - нет необходимости при описании шаблонного типа писать слово template.

Шаблоны можно рассматривать как квазицитаты, работающие вне макросов, а аргументы шаблонов - как сплайс-аргументы квазицитат. С этой точки зрения я не вижу никаких ограничений на виды аргументов шаблонов. Так, аргументами могут быть не только типы и числовые константы, но и любые другие константы, имена переменных (не "значения", а именно имена!), функции, фрагменты кода.

Традиционно для аргументов шаблонов применяют угловые скобки. С этими скобками есть одна проблема - символы "<" и ">" используются также как операторы сравнения, или части операторов битовых сдвигов и вращений. Проблема решается или эвристически (как в C#), или вообще отказом от традиционного синтаксиса (как в D). Конечно, я бы мог отказаться от традиционного синтаксиса шаблонов, возможность такая есть. Но на данном этапе я решил его сохранить, за одним исключением: аргументные угловые скобки должны предваряться символом решетки. В результате шаблон в Neo выглядит примерно так:

struct MyStruct  #<class T, int N>
{
  T arr[N];
};

а создание объектов - так

MyStruct#<int,100> x, y, z;

Применение символа # позволяет отказаться от любой эвристики и однозначно идентифицировать угловую скобку как начало аргументов шаблона, а не как знак "меньше". 


Теперь рассмотрим способы передачи в шаблоны различных видов аргументов. С передачей чисел, строк и прочих константных объектов никаких проблем не возникает. Аналогично всем другим языкам передаются и типы данных. Функции также передаются без проблем. Тип функции задается в синтаксисе, похожем на описание прототипа функции


def Foo #< (int) void F, int N > ()

{
  for(int i=0; i<N; i++)
     F(i);
}
 

В данном примере мы имеем шаблонную функцию Foo c двумя шаблонными аргументами: функцией F и числом N. Если у нас есть некая фукция, совпадающая по сигнатуре с F:

void Bar(int x)

{

  return x*2 + 100;

}


то ее можно передать как аргумент шаблона в вызов функции Foo:

Foo #<Bar, 10> ();


такой шаблон сгенерирует реализацию функции Foo, внутри которой будет вызываться функция Bar - в цикле 10 раз. То есть компилятор будет компилировать код, сгенерированный шаблоном:
def Foo()

{

  for(int i=0; i<10; i++)


     Bar(i);

}

Самое интересное - возможность передачи в шаблоны блоков кода. Во многих случаях это позволит создавать код, на порядки более оптимальный по скорости выполнения, чем работа с делегатами. Поскольку все аргументы шаблонов являются в конечном итоге объектати типа expr, то с передачей блоков кода также не возникает никаких проблем. Разумеется, блок кода не должен ссылаться на символы, которые могут быть недоступны в точке использования шаблона. В остальном же все просто - тип аргумента block, в качестве аргумента можно передавать блоки, заключенные в фигурые скобки.

def Foo #<block B> ()

{

  if(some_condition)

    B;

}


и вызов
Foo #< { for(int i=0; i<10; i++) puts("Hello World!"); } > ();


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



No comments:

Post a Comment