среда, 25 мая 2011 г.

"Черная магия" гугловских программистов

На хабре наткнулся на пост об безопасном определении числа элементов массива.
Суть состоит в следующем. Например, большинство программистов для определения количества элементов в массиве используют следующую конструкцию:
int XX[] = { 1, 2, 3, 4 };
size_t N = sizeof(XX) / sizeof(XX[0]);
Обычно это оформляют в макрос следующего вида:
#define count_of(arg) (sizeof(arg) / sizeof(arg[0]))
Однако он может послужить причиной ошибки, так как ему случайно можно подсунуть простой указатель, и он не будет возражать по этому поводу:
void Test(int C[3])
{
  int A[3];
  int *B = Foo();
  size_t x = count_of( A ); // Ok
  x = count_of( B ); // Error
  x = count_of( C ); // Error
}
Конструкция count_of(A) работает корректно и возвращает количество элементов в массиве A, равное трём. Но если случайно применить count_of() к указателю, то результатом будет малоосмысленное значение.
В проекте Chromium гугловские программисты использовали такой макрос, который позволяет избежать описанных выше ошибок. Вот этот магический макрос:
template <typename T, size_t N>
char (&ArraySizeHelper(T (&array)[N]))[N];
#define arraysize(array) (sizeof(ArraySizeHelper(array))) 
Объяснено на хабре было плохо (ну для меня по крайней мере), потом нашел другое объяснение, где уже было немного понятнее.
К моему позору, я начисто забыл, как в С++ выглядит прототип функции, возвращающей ссылку на массив. А выглядит он, например, так:
int (&get_array())[10];
Это прототип функции, возвращающей ссылку на массив из 10 интов. Такой способ определения ссылки на массив из N элементов - это наследие C. Выглядит точно так же, как и определение указателя на массив, достаточно "&" заменить на "*".
Далее все очень просто:
template <typename T, size_t N> char (&ArraySizeHelper(T (&array)[N]))[N];
- это шаблонизированный прототип функции, принимающей ссылку на массив произвольного типа T из N элементов и возвращающий ссылку на массив типа char из N элементов. Очевидно, что char потому, что число элементов в массиве в этом случае равно количеству байт, занимаемым массовом. Смысл шаблона функции именно в том, что размер массива "магически" передается сразу на выход функции, фактически определяя тип возвращаемого значения (а конкретно размер) во время компиляции. То есть, фактически, саму функцию определять не надо - достаточно только ее прототипа. А он будет сгененерирован во время компиляции. То, что функция реально не вызывается, вызвало у меня некое недопонимание. Однако именно так дело и обстоит. Такова особенность работы функции sizeof() - ей достаточно знать только тип вычисляемого выражения. А он известен на этапе компиляции. То есть нет необходимости вызывать саму функцию. А значит, нет необходимости и ее реализовывать - достаточно только объявления. Почитав немного Александреску, такие фокусы воспринимать уже гораздо легче и привычнее.Чтобы было понятнее, лучше всего привести пару примеров. У нас определен такой массив:

const int arr_size = 10;
const int int_arr[arr_size]; 
 
Теперь, если раскрыть шаблон ArraySizeHelper для него, то мы получим такое определение функции:

char (&ArraySizeHelper(int (&array)[10]))[10]; 
 
То есть прототип функции, получающей параметром ссылку на массив из 10 целых чисел и возвращающую ссылку на массив из 10 char. Естественно, что sizeof() для массива из 10 элементов типа char вернет 10.
 
Теперь, при попытке подсунуть что-то не то будет ошибка компиляции, что позволит выявить ошибку:
template <typename T, size_t N>
char (&ArraySizeHelper(T (&array)[N]))[N];
#define arraysize(array) (sizeof(ArraySizeHelper(array)))
 
void Test(int C[3])
{
  int A[3];
  int *B = Foo();
  size_t x = arraysize( A ); // Ok
  x = arraysize( B ); // Ошибка компиляции
  x = arraysize( C ); // Ошибка компиляции
}


P.S.
Для подсветки синтаксиса использовал highlight.hohli.com