Pull to refresh

Шаблоны на C. Да! На чистом С. Не С++

Reading time 4 min
Views 62K

Зачем?


Давайте представим себе, что нам нужно написать набор функций, которые отличаются друг от друга лишь парой ключевых слов (и, как правило, одно из них — название типа). Ну, вот, например, взгляните на функции, рассчитывающие суммы элементов массивов для разных типов (упрощения ради, проверки указателей на неравенство нулю опущены /*упрощения ради также не рассматривается возможность переполнения для int — прим. пер.*/)

  void sum_float(int n, float *a, float *b)
  {
    /* computes a:=a+b where a and b are two arrays of length n */
    int i;
    for(i=0;i<n;i++) a[i]+=b[i];
  }

  void sum_double(int n, double *a, double *b)
  {
    /* computes a:=a+b where a and b are two arrays of length n */
    int i;
    for(i=0;i<n;i++) a[i]+=b[i];
  }

  void sum_int(int n, int *a, int *b)
  {
    /* computes a:=a+b where a and b are two arrays of length n */
    int i;
    for(i=0;i<n;i++) a[i]+=b[i];
  }

Ну согласитесь же, насколько бы было лучше описать тело функции один раз, указав названия принимаемого (и возвращаемого) типа в виде «параметра», а потом определить экземпляры функции для конкретных типов? И эти функции еще относительно просты, а представьте себе, если бы они были длиннее, а их набор — больше.

Вот именно для этого случая в C++ существует ключевое слово template. Но увы, не в чистом С.

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

Шаблоны в С.


Нам понадобятся некоторые ингридиенты.

1: Заготовки

Для начала, определим пару макросов. Они будут располагаться в отдельном заголовочном файле, и этот файл нам еще понадобится. Для ясности, назовем этот отдельный заголовочный файл «templates.h»

templates.h
#ifndef TEMPLATES_H_
#define TEMPLATES_H_

#define CAT(X,Y) X##_##Y
#define TEMPLATE(X,Y) CAT(X,Y)

#endif 

Макрос Template нам понадобится в дальнейшем чтобы объединять макроопределения X и Y в виде X_Y, таким образом, чтобы написав TEMPLATE(function,type) мы получили бы в этом месте function_type.

В препроцессоре С директива ## позволяет объединить два токена в один. Причиной, по которой мы здесь используем два макроса вместо одного #define TEMPLATE(X,Y) X##Y является то, что если X, в свою очередь, тоже будет макроопределением… Впрочем, неважно. Этот вопрос выходит за пределы рассматриваемого в этой статье.

2: Готовим

Любая нормальная функция должна находиться в в файле с расширением .c и ее прототип должен быть описан в файле .h, верно? Ну так, давайте уже их напишем. Чтобы обозначить параметр, соответствующий типу данных, для которых предназначена функция, традиционно будем использовать букву «T». Вы потом еще встретите ее в директивах #define.

sum_as_template.h
#ifdef T    
#include "templates.h"    
void TEMPLATE(sum,T)(int n, T *a, T *b);
#endif 

Наверняка вы уже заметили, что в этом заголовочном файле отсутствует типовая конструкция для защиты от повторного включения #ifndef HEADER_H #define HEADER_H… #endif. И это неспроста, и мы потом еще к этому моменту вернемся. С другой стороны, #ifdef T не то чтобы обязателен, но очень полезен на тот случай, если заголовочный файл включен, а тип не определен. А то сообщения об ошибках могут быть не очень информативными.

А теперь С

sum_as_template.c
#ifdef T    
#include "templates.h"    
void TEMPLATE(sum,T) (int n, T *a, T *b)  
{       
/* computes a:=a+b where a and b are two arrays of length n */       
int i;        
for(i=0;i<n;i++) a[i]+=b[i];  
}   
#endif


3. Сервируем.

Не помню, сколько строк мы до этого написали, но для резюме вы смело можете умножить их число на 3. Или 4?

all_possible_sums.c
#include "templates.h"  
#include "all_possible_sums.h"
#ifdef T 
#undef T  
#endif  
#define T float  
#include "sum_as_template.c"    
#ifdef T  
#undef T  
#endif  
#define T double  
#include "sum_as_template.c"    
#ifdef T  
#undef T  
#endif 
#define T int  
#include "sum_as_template.c"


Мелочи жизни: для GCC 3 строки #ifdef T #undef T #endif можно заменить на одну #undef T, но вот Visual C++ (как минимум, до 7 версии включительно, не переносит подобных вольностей)

Ну, и Хииииииииидер!

all_possible_sums.h
#ifndef ALL_POSSIBLE_SUMS_H_   
#define ALL_POSSIBLE_SUMS_H_     
#include "templates.h"    
#ifdef T  
#undef T  
#endif  
#define T float  
#include "sum_as_template.h"    
#ifdef T  
#undef T  
#endif  
#define T double  
#include "sum_as_template.h"    
#ifdef T  
#undef T  
#endif  
#define T int  
#include "sum_as_template.h"
#endif
 


Теперь должно быть понятно, почему мы не защищали sum_as_template.h от множественных включений: мы включаем его по разу на каждый задействованный тип.

4. Подаем

Ну, собственно, и все. Можно вызывать:

main.c
#include "all_possible_sums.h"  
int main(int argc, char **argv)  
{    
int ai[3] = {1,2,3};    
int bi[3] = {4,5,6};    
float af[3] = {1.0,2.0,3.0};
float bf[3] = {1.5,2.5,3.5};   
TEMPLATE(sum,int)(3,ai,bi);    
TEMPLATE(sum,float)(3,af,bf);    
return 0;  
}



И вот еще что.

Пытливый читатель спросит переводчика, а что если мне нужен тип «unsigned long long»? Ведь у нас получится функция «void sum_unsigned long long()»? К счастью для переводчика, автор предусмотрел и это. Используйте typedef:

typedef unsigned long long uint64; 
TEMPLATE(sum,uint64)


вместо
TEMPLATE(sum,unsigned long long)



(Это довольно-таки вольный перевод. Свою статью писать уже лень, раз гугл знает ответ на вопрос function template in plain c, и к той статье мне решительно нечего добавить, но раз на хабре и вообще в русскоязычном секторе ответ гуглом не находится, чтобы добру не пропадать, опубликую пост-мортем)

— 8< — [здесь закончился оригинальный пост] —

UPD: Хочу сказать огромное спасибо хабраюзеру GRAFIN99 за как минимум 4 выявленные ошибки в исходниках, причем три из них — на глаз, просто в ходе прочтения статьи.
Tags:
Hubs:
+49
Comments 65
Comments Comments 65

Articles