пятница, 8 августа 2014 г.

Корректный подсчет числа вариативных параметров в BOOST_PP_VARIADIC_SIZE

Макроопределение BOOST_PP_VARIADIC_SIZE из библиотеки Boost Preprocessor никогда не возвращает значение 0, это видно из его определения.
#define BOOST_PP_VARIADIC_SIZE(...)                                       \
        BOOST_PP_VARIADIC_SIZE_I(__VA_ARGS__, 64, 63, 62, 61, 60, 59,     \
                                 58, 57, 56, 55, 54, 53, 52, 51, 50, 49,  \
                                 48, 47, 46, 45, 44, 43, 42, 41, 40, 39,  \
                                 38, 37, 36, 35, 34, 33, 32, 31, 30, 29,  \
                                 28, 27, 26, 25, 24, 23, 22, 21, 20, 19,  \
                                 18, 17, 16, 15, 14, 13, 12, 11, 10, 9,   \
                                 8, 7, 6, 5, 4, 3, 2, 1,)

#define BOOST_PP_VARIADIC_SIZE_I(e0, e1, e2, e3, e4, e5, e6, e7, e8, e9,  \
                                 e10, e11, e12, e13, e14, e15, e16, e17,  \
                                 e18, e19, e20, e21, e22, e23, e24, e25,  \
                                 e26, e27, e28, e29, e30, e31, e32, e33,  \
                                 e34, e35, e36, e37, e38, e39, e40, e41,  \
                                 e42, e43, e44, e45, e46, e47, e48, e49,  \
                                 e50, e51, e52, e53, e54, e55, e56, e57,  \
                                 e58, e59, e60, e61, e62, e63, size,      \
                                 ...) size
Я позволил себе немного видоизменить определения из boost/preprocessor/variadic/size.hpp: разбил их на несколько строк, убрал обрамляющий служебный макрос BOOST_PP_VARIADICS и проигнорировал вариант для Microsoft Visual C. Видите, убывающий список чисел начинается с 64 и заканчивается единицей: нулевое значение не предусмотрено. Почему так сделано? Потому что препроцессор все равно не в состоянии стандартным образом вычислить этот 0. Представьте, что значение __VA_ARGS__ как первый аргумент в макросе BOOST_PP_VARIADIC_SIZE_I пустое. По-хорошему, мы бы хотели, чтобы на месте e0 из его определения оказалось значение 64, тогда при условии, что убывающий список оканчивался бы нулем, мы получили бы желаемый результат 0. Но этого не произойдет потому, что за пустым __VA_ARGS__ следует вполне четкая запятая, и не важно, что перед ней пусто: препроцессор даже это пустое значение сопоставит с e0! Таким образом, случаи, когда __VA_ARGS__ пуст либо содержит лишь один элемент, неразличимы для этого определения BOOST_PP_VARIADIC_SIZE! Вот почему в нем нет нулевого значения. Итак, главный враг на пути к корректному определению BOOST_PP_VARIADIC_SIZE — это запятая после __VA_ARGS__. Первое, что приходит на ум для борьбы с ней — это использование макроопределения BOOST_PP_COMMA_IF из той же библиотеки Boost Preprocessor. Однако, для того, чтобы определить, ставить запятую или нет, нужно сначала выяснить, пуст ли __VA_ARGS__, другими словами, посчитать число его элементов. Таким образом, мы вернулись к исходной задаче, а значит этот подход не годится. Я нашел в интернете несколько предложений решения этой проблемы (например, здесь и здесь), но все они кажутся какими-то очень громоздкими. Между тем, gcc предлагает в виде расширения для __VA_ARGS__ оператор ##, который умеет съедать предваряющую __VA_ARGS__ запятую, если тот оказывается пустым (см. документацию gcc по вариативным макроопределениям). Это похоже на то, что нам нужно, и это и есть то, что нам нужно. Именно для решения подобных проблем разработчики gcc представили это расширение. Теперь внутри вызова BOOST_PP_VARIADIC_SIZE_I вместо __VA_ARGS__, мы можем записать , ##__VA_ARGS__,, добавить в конец убывающего списка чисел 0, а в определение BOOST_PP_VARIADIC_SIZE_I — еще один элемент e64.
#define FROM_BOOST_PP_VARIADIC_SIZE(...)                                       \
        FROM_BOOST_PP_VARIADIC_SIZE_I(, ##__VA_ARGS__, 64, 63, 62, 61, 60, 59, \
                                      58, 57, 56, 55, 54, 53, 52, 51, 50, 49,  \
                                      48, 47, 46, 45, 44, 43, 42, 41, 40, 39,  \
                                      38, 37, 36, 35, 34, 33, 32, 31, 30, 29,  \
                                      28, 27, 26, 25, 24, 23, 22, 21, 20, 19,  \
                                      18, 17, 16, 15, 14, 13, 12, 11, 10, 9,   \
                                      8, 7, 6, 5, 4, 3, 2, 1, 0)

#define FROM_BOOST_PP_VARIADIC_SIZE_I(e0, e1, e2, e3, e4, e5, e6, e7, e8, e9,  \
                                      e10, e11, e12, e13, e14, e15, e16, e17,  \
                                      e18, e19, e20, e21, e22, e23, e24, e25,  \
                                      e26, e27, e28, e29, e30, e31, e32, e33,  \
                                      e34, e35, e36, e37, e38, e39, e40, e41,  \
                                      e42, e43, e44, e45, e46, e47, e48, e49,  \
                                      e50, e51, e52, e53, e54, e55, e56, e57,  \
                                      e58, e59, e60, e61, e62, e63, e64, size, \
                                      ...) size
В случае, если __VA_ARGS__ окажется пуст, мы получим одну запятую перед элементом 64 (то есть эффективно первый дополнительный элемент) и, за счет добавления e64, size в BOOST_PP_VARIADIC_SIZE_I окажется равным 0. Если в __VA_ARGS__ будет один элемент, то это равносильно двум дополнительным элементам перед значением 64 (несъеденная первая запятая и запятая после единственного элемента из __VA_ARGS__), и на месте size окажется значение 1. Дальнейшее увеличение количества элементов в __VA_ARGS__ на единицу будет сдвигать убывающий список чисел на единицу и в месте, соответствующем size, будет каждый раз оказываться число, большее на единицу. Проверяем на следующей простой программе.
#define PRINT( A, ... ) \
        printf( A ": %d\n", FROM_BOOST_PP_VARIADIC_SIZE( __VA_ARGS__ ))

int  main( void )
{
    PRINT( "Number of arguments" );
    PRINT( "Number of arguments", hahgsagsgdg );
    PRINT( "Number of arguments", 576oo, 3354 );
}
Программа выведет на экран
Number of arguments: 0
Number of arguments: 1
Number of arguments: 2
Мне кажется, что если бы оператор ## для __VA_ARGS__ был стандартизован, то макроопределение BOOST_PP_VARIADIC_SIZE в Boost Preprocessor было бы реализовано именно таким способом.

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

Отправить комментарий