понедельник, 21 мая 2007 г.

Вычисление количества элементов статического массива на этапе компиляции

Я уверен, что 99% из тех, кто программирует на С++, сталкивался с проблемой, когда имеется статический массив значений, количество элементов которого нужно передать вызываемой функции, присвоить другой переменной или использовать его ещё каким-либо способом. Самое первое, что приходит на ум - это разделить длину массива в байтах на размер типа элемента. Например:

static const int ARRAY[] = { 1, 2, 3, 4, 5, 6 };
static const size_t ELEMENTS_COUNT = sizeof(ARRAY) / sizeof(int);
DoSomethingUseful(ELEMENTS_COUNT);


Конечно, это работает, и работает правильно до тех пор, пока вы не решите изменить тип элементов массива. Хорошим тоном считается определение констант и переменных непосредственно перед их использованием, но представьте себе следующую ситуацию:

static const int ARRAY[] = { 1, 2, 3, 4, 5, 6 };
// 20 тысяч строк полезного кода
static const size_t ELEMENTS_COUNT = sizeof(ARRAY) / sizeof(int);
DoSomethingUseful(ELEMENTS_COUNT);

В этот момент ваш менеджер говорит, что код надо срочно адаптировать под платформу х64, используя все её преимущества, причём это должно быть сделано "вчера", и вы бросаетесь изменять тип int на __int64. В условиях острой нехватки времени я вам гарантирую, что хотя бы в одном участке кода вы забудете исправить sizeof(int) на sizeof(__int64). К чему это приведёт - рассказывать не стоит, особенно если ошибка окажется в редко используемой части кода. Как правило, крах вашей программы на глазах у заказчика заставит вас серьёзно и надолго задуматься.
Но не стоит отчаиваться, поскольку проблема решается просто - при вычислении размера массива вместо размера типа нужно использовать размер одного из элементов этого массива (обычно первого, поскольку в С++ разрешены массивы только ненулевой длины). Таким образом получаем следующее:

static const int ARRAY[] = { 1, 2, 3, 4, 5, 6 };
static const size_t ELEMENTS_COUNT = sizeof(ARRAY) / sizeof(ARRAY[0]);


Не знаю, как вам, но мне не нравится определение ELEMENTS_COUNT, поскольку оно засоряет пространство имён в своей области видимости, да и компилятор всё равно оптимизирует вычисление количества элементов, и в случае с вызовом DoSomethingUseful(ELEMENTS_COUNT) в скомпилированном с включенной оптимизацией бинарном коде это будет выглядеть примерно так:

push 6
call DoSomethingUseful

К тому же, писать постоянно такую конструкцию, как sizeof(ARRAY) / sizeof(ARRAY[0]), довольно утомительно, а если не использовать переменную ELEMENTS_COUNT, то вызов функции DoSomethingUseful будет довольно зловещим:

DoSomethingUseful(sizeof(ARRAY) / sizeof(ARRAY[0]));

Есть ли более элегантный способ вычисления количества элементов? Да, он существует, поскольку существует возможность передачи массива по ссылке в качестве параметра функции:

void F(int (&ARRAY)[6]);

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

template<typename T, size_t N>
inline size_t LengthOf(T (&x)[N])
{
return N;
}

Поскольку функция определена как встраиваемая (хотя компилятор в принципе может и проигнорировать это указание, но в этом случае всё получалось как и задумано), значение N будет вычислено на этапе компиляции. В результате получаем более краткий и читаемый код для вызова DoSomethingUseful:

DoSomethingUseful(LengthOf(ARRAY));

В скомпилированном коде по-прежнему без изменений:

push 6
call DoSomethingUseful

Таким образом, мы получили требуемый результат. Если кто-то знает более изящное решение - не стесняйтесь, напишите мне.

Комментариев нет: