Еще некоторые интересные особенности метапрограммирования. На этот раз - шаблоны. При внимательном рассмотрении шаблоны очень красиво сочетаются с макросами. По сути, макросы и шаблоны - две стороны одной медали.
Вообще говоря, все, что можно сделать на шаблонах, можно сделать и с помощью макросов. Тогда зачем же нужны шаблоны? На самом деле макросы - это мощная и довольно сложная возможность, связанная с активным взаимодействием пользовательского программы и компилятора. Применение такого мощного (и в общем опасного) инструмента оправдано далеко не всегда. Шаблоны же предоставляют простой и удобный интерфейс, реализующий важную функцию макросов - подстановку кода (квазицитирование).
Тип 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;
Применение символа # позволяет отказаться от любой эвристики и однозначно идентифицировать угловую скобку как начало аргументов шаблона, а не как знак "меньше".
Теперь рассмотрим способы передачи в шаблоны различных видов аргументов. С передачей чисел, строк и прочих константных объектов никаких проблем не возникает. Аналогично всем другим языкам передаются и типы данных. Функции также передаются без проблем. Тип функции задается в синтаксисе, похожем на описание прототипа функции
В данном примере мы имеем шаблонную функцию 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!"); } > ();
Кроме того, блоки могут быть не только аргументами шаблонов, но и самими шаблонами. Правда, здесь есть одна маленькая особенность, о которой позже.
Вообще говоря, все, что можно сделать на шаблонах, можно сделать и с помощью макросов. Тогда зачем же нужны шаблоны? На самом деле макросы - это мощная и довольно сложная возможность, связанная с активным взаимодействием пользовательского программы и компилятора. Применение такого мощного (и в общем опасного) инструмента оправдано далеко не всегда. Шаблоны же предоставляют простой и удобный интерфейс, реализующий важную функцию макросов - подстановку кода (квазицитирование).
Тип 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