"Пять грустных солдат, пять веселых солдат и ефрейтор..."
Окуджава Б.Ш., 1961
Содержание
Оператор _Countof
Формально представим виновника торжества, согласно п. 6.5.4.5 N3783.
Оператор _Countof применятся к выражению, имеющему полностью определённый тип массива, или к имени такого типа, заключенному в круглые скобки.
Оператор _Countof возвращает количество элементов в своем операнде. Количество элементов определяется типом операнда. Результатом является целое число. Если количество элементов типа массива является переменным, операнд вычисляется, в противном случае операнд не вычисляется, а выражение является целым константным выражением.
В заголовочном файле <stdcountof.h> определяется макрос countof, который определяется как _Countof.
Стоит отметить связанное изменение в определении оператора sizeof. В C99...C23 операнд sizeof должен быть вычислен только, если он VLA. А в текущем проекте C2Y, операнд, и sizeof, и _Countof, должен быть вычислен, только если результат не является целочисленной константой.
Этимология названия
В первоначальном предложении N3369: The _Lengthof Operator, предлагалось дать этому оператору имя: _Lengthof. Однако, после ряда метаний по кругу: _Lengthof => elementsof => nelementsof => neltsof => _Lengthof был проведён всенародный опрос N3469: Big Array Size Survey:

, который убедительно показал, что наиболее предпочтительные варианты, либо просто countof, либо _Countof с заголовочным файлом <stdcountof.h> и макросом countof, который и вошёл в проект C2y. К сожалению, распределение голосов по странам и языкам я нашёл только в виде картинки, но похоже, что русскоязычные респонденты так же чаще предпочитали countof.
Выходит, что Microsoft, с макросом _countof() Visual Studio 2005, попал в точку.
Реализация
На настоящий момент (январь 2026), оператор _Countof реализован:
Clang версии 21;
GNU gcc версии 16 (предварительная);
IntelLLVM (icx) версии 1025.3.
Все эти компиляторы поддерживают полный набор общеупотребительных расширений языков C/C++ для использования массивов или объектов размера 0 и текущие реализации _Countof вполне совместимы с этими расширениями:
#include <assert.h> #include <stdcountof.h> int main(void) { int a00[0][0]; static_assert(0 == sizeof(a00) && 0 == countof(a00)); static_assert(0 == countof(int [0][0])); int a70[7][0]; static_assert(0 == sizeof(a70) && 7 == countof(a70)); static_assert(7 == countof(int [7][0])); volatile unsigned n0 = 0; volatile unsigned n7 = 7; int v00[n0][n0]; assert(0 == sizeof(v00) && 0 == countof(v00)); assert(0 == countof(int [n0][n0])); int v70[n7][n0]; assert(0 == sizeof(v70) && 7 == countof(v70)); assert(7 == countof(int [n7][n0])); int fv[7][n0]; assert(0 == sizeof(fv)); static_assert(7 == countof(fv)); static_assert(7 == countof(int [7][n0])); }
Терминология
Сокращение | Описание | Пример |
|---|---|---|
VLA | Опциональные (до C23) массивы переменной длины (Variable Length Array, C99 п. 6.7.5.2) у которых, либо выражение размера не является константой, либо элементы не имеют известного постоянного размера. Могут быть аргументом функции, размещаться в куче или иметь автоматическое размещение (в блоке). Лично мне известен только один компилятор, который определяет макрос |
|
VM | Тип является VM, если вложенная последовательность определений полного определения типа содержит VLA. Кроме того, любой тип полученный из VM является VM. (variably modified, C99 п. 6.7.5(3)) |
|
Известный постоянный размер | Тип имеет известный постоянный размер, если он полный и не является VLA (known constant size, C11 п. 6.2.5(23)) | |
VLA0 | Все стандарты C99...C2Y определяют как неопределённое поведение (undefined behavior, UB) любые операции с VLA, если выражение размера не является положительным. Однако, все компиляторы более менее лояльно относятся к нулевым выражениям размера VLA. Вероятно, это можно считать общепринятым расширением языка C |
|
VLA_CXX | Все компиляторы, которые имеют поддержку VLA для языка C, расширяют язык C++ в части VLA | |
ZLA | Расширение языков C/C++ в части массивов нулевой длины (Zero Length Array). Весьма распространённое расширение, три четверти компиляторов его поддерживают |
|
EMPTY_STRUCTURE ALONE_FLEXIBLE_ARRAY | Пустые структуры (расширение C), структуры содержащие единственный завершающий массив неполного типа (flexible array), объединения нулевого размера (расширение C/C++). |
|
Жизнь до _Countof
В ранних языках программирования с числом элементов массива традиционно имелись некоторые сложности. Считалось, что если программист определил массив A[N], то он без труда может просто использовать выражение N. В языке C, авторы предусмотрели несколько методов неявного задания размера, но, из соображений минимализма, решили ограничиться единственным оператором sizeof, поскольку в то время, он являлся константным строго положительным выражением.
Идиома количества элементов массива
В K&R первого издания (1978) был пример получения количества элементов массива:
#define NKEYS (sizeof (keytab) / sizeof (struct key) )
В K&R второго издание (1988) был опубликован практически современный вариант идиомы:
#define NKEYS (sizeof keytab / sizeof keytab[0])
В семействе BSD/..../FreeBSD, с давних пор, используется, как идиома в чистом виде, так и множество макросов (ACPI_ARRAY_LENGTH(), ARRAY_LENGTH(), ARRAY_SIZE(), COUNT(), N(), NUM_ELEMENTS(), nitem(), RTE_DIM(), UU_NELEM(), ...) вида:
#define N(ary) (sizeof(ary) / sizeof(*ary))
В ядре Linux, идиома в чистом виде встречается крайне редко, практически всё, процентов на 99,7% или немного больше, сведено к единому макросу ARRAY_SIZE(), который с 2007 года имеет вид:
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]) + __must_be_array(arr))
В Visual Studio 2005 был введён шаблон/макрос _countof():
#if __cplusplus template<typename _CountofType, size_t _SizeOfArray> char (*_countof_helper(_UNALIGNED _CountofType (&_Array)[_SizeOfArray]))[_SizeOfArray]; #define _countof(_Array) (sizeof(*_countof_helper(_Array)) + 0) #else #define _countof(_Array) (sizeof(_Array) / sizeof(_Array[0])) #endif
Классическая идиома в новых временах
Как можно заметить, что, и в K&R, и у MS Visual Studio _countof(), имя массива не берётся в скобки. Для компиляторов, строго следующих стандарту языков C/C++, в этом нет необходимости, т.к. массивы размерности 0 недопустимы.
Однако, большинство современных компиляторов, выражение _countof(int [7]) раскроют в (sizeof(int [7]) / sizeof(int [7][0])) и вставят в генерируемый код - деление на 0. Возможно, сопроводят это дело предупреждением (следует отметить, что поделят на ноль, или иное UB учинят, почти все, а вот предупреждение выдадут, дай Бог, если половина). Поэтому в новом коде чаще встречаются варианты sizeof(*(a)) или sizeof((a)[0]).
Подводные камни оператора sizeof
Лично мне, кажется, что последней соломинкой, которая переломила консерватизм комитета WG14, был вопрос производительности. При работе с теми VLA, у которых младшие размерности переменны, а старшие константны, многие компиляторы не сокращают общие подвыражения в классической идиоме, а много и часто делят (следствие пп. 2 и 4, ниже). Следует заметить, что хотя примеры VLA проще давать в виде создания их на стеке (в блоке), но VLA ,как аргументы функций и/или как результат выделения в куче, не менее важны.
Тем не менее, обычно, рассмотрение этой темы начинают с рефакторинга (п. 1, т.е. вопросов преобразования кода при изменении способов обращения: массив - указатель на массив - указатель на элемент). N3369 не исключение, в нём эти подводные камни тоже изложены начиная с п.1:
sizeofвозвращает размер операнда в байтах, но байт байту рознь;sizeofобычно не вычисляет операнд, но иногда вычисляет;sizeofиногда является константой времени компиляции, а иногда нет;Нет гарантий, что
sizeofне равняется 0.
"Размерность" sizeof
Проблему "размерности" оператора sizeof неплохо иллюстрирует набор предупреждений компиляторов (например, Clang начиная c 3.0 2011 года, предупреждения выдаваемые по умолчанию):
#if __cplusplus || _MSC_VER size_t foo(size_t n, int va[]) { #else size_t foo(size_t n, int va[static n]) { #endif size_t r = n; r += sizeof(va); // W: Clang/GNU/IntelLLVM: sizeof-array-argument return r; } size_t boo(size_t n, int *p) { size_t r = n; r += sizeof(p); r += sizeof(p)/sizeof(p[0]); // W: Clang/IntelLLVM: sizeof-pointer-div // W: GNU (with -Wall): sizeof-pointer-div r += sizeof(p)/(sizeof(p[0])); // W: Clang/IntelLLVM: Ok // W: GNU11 (with -Wall): sizeof-pointer-div r += sizeof(p)/(sizeof(p[0]) + 0); // Ok memset(p, 0, sizeof(p)); // W: Clang/IntelLLVM: sizeof-pointer-memaccess // W: GNU (with -Wall): sizeof-pointer-memaccess return r; } int main(void) { size_t r = 0; int a[1917] = { 10 }; r += foo(1918, a); // W: GNU11 and above: stringop-overflow r += boo(1918, a); r += sizeof(a)/sizeof(short); // W: Clang/IntelLLVM: sizeof-array-div // W: GNU (with -Wall): sizeof-array-div r += sizeof(a)/(sizeof(short)); // Ok r += sizeof(a + 25); // W: Clang/IntelLLVM: sizeof-array-decay printf("Ok %zu\n", r); }
На настоящий момент, если Вы используете Сlang (clang), IntelLLVM (icx) или GNU (gcc -Wall), то Вас, по крайней мере, попытаются предупредить о неоднозначном применении идиомы количества элементов массива и других подводных камнях.
sizeof вычисляет только VLA операнд
int main(void) { size_t r = 0; int a[1917][10]; int (*pa)[1917][10] = &a; r += sizeof(*pa++[0]); // W: Clang/IntelLLVM: unevaluated-expression assert(&a == pa); #if (!__cplusplus && !__STDC_NO_VLA__) || (__cplusplus && HAVE_VLA_CXX) size_t n = 25; int v[n]; __typeof__(v) *pv = &v; r += sizeof(*pv++[0]); // W: Clang/IntelLLVM: unevaluated-expression assert(&v == pv); size_t m = 7; int w[m][n]; __typeof__(w) *pw = &w; r += sizeof(*pw++[0]); #if !__cplusplus && !__NVCOMPILER && !__ORANGEC__ assert(&w != pw); #else (void)(&w != pw); #endif #endif printf("Ok %zu\n", r); }
Но это неточно. Во-первых, все компиляторы, которые поддерживают стандартные VLA языка C, реализуют VLA_CXX, но в части побочных эффектов оператора sizeof для расширений C++ согласия нет. Во-вторых, не все компиляторы точно реализуют стандарты C99...C23.
А в части чистой идиомы количества элементов массива, грубо говоря, для одномерных VLA выражение будет вычислено один раз, а для многомерных VLA или для фиксированного массива содержащего VLA - два раза.
sizeof не является положительным константным выражением
Все известные компиляторы, которые поддерживают VLA, так же поддерживают VLA0, без каких-либо предупреждений или ограничений:
void foo(size_t m, size_t n, int (*v)[m][n]) { printf("m=%zu n=%zu sizeof(*v) = %zu sizeof((*v)[0]) = %zu\n", m, n, sizeof(*v), sizeof((*v)[0])); } int main(void) { for (size_t m = 0; m < 3; m++) { for (size_t n = 0; n < 3; n++) { int v[m][n]; foo(m, n, &v); } } }
Мало того, большинство компиляторов поддерживают расширения ZLA, а так же объекты нулевого размера и массивы таких объектов:
int main(void) { #if !_MSC_VER && !__ORANGEC__ && !__POCC__ && !__IBMC__ struct {} s0; _Static_assert(0 == sizeof(s0), "0 == sizeof(s0)"); int a0[] = {}; _Static_assert(0 == sizeof(a0), "0 == sizeof(a0)"); struct {} a1917s0[1917]; _Static_assert(0 == sizeof(a1917s0), "0 == sizeof(a1917s0)"); #endif }
При применении идиомы количества элементов для массивов такого рода, компилятор, в лучшем случае, выдаст предупреждение о делении на 0, но код скомпилируется и вызовет SIGFPE, SIGSEGV или иное проявление UB, во время выполнения. К примеру, на всех компиляторах, кроме MS Visual Studio, классического IBM® XL C/C++ for AIX®, Orange C и Pelles C, следующий код:
int main(void) { #if !__STDC_NO_VLA__ size_t n = 1917; struct{} v[n][n]; printf("sizeof(v)/sizeof(v[0])=%zu\n", sizeof(v)/sizeof(v[0])); #endif }
, соберётся, и без ошибок, и даже без единого предупреждения, но, конечно, 1917 не напечатает (увы, вечно эти Clang и Intel, всё оптимизируют исходя из отсутствия UB, но, когда это действительно надо, общее подвыражение не могут вынести и сократить 😒). Таким образом, надо за этим как-то отдельно следить, что нулевой размер элемента невозможен.
Альтернативы без sizeof
В народном фольклоре встречается пара вариантов получения количества элементов массива без использования оператора sizeof вида:
#define wrong_countof(a) ((&(a))[1] - (a))
Или вариант проверки:
#define wrong_must_be_array(a) (sizeof(*(a)) && (void *)&(a) == (a) \ ? 0 : raise(SIGABRT))
Проблема этих вариантов в том, что в случае, если a - массив, то они выдают корректный результат, но если a не является стандартным массивом C, то результат может быть некорректным и/или поведение может быть неопределённым (UB).
Переход к countof()
В простейшем случае, переносимый код переходного периода может выглядеть примерно так:
#if __has_include(<stdcountof.h>) #include <stdcountof.h> #else #define countof(a) ... #endif
, где многоточие надо будет заменить на что-то с интерфейсом стандартного макроса countof(). Вариантов замены немало.
Во-первых, вместо многоточия можно использовать обычную классическую идиому. Сообщество BSD как раз примерно так и живёт, при использовании Сlang (clang), IntelLLVM (icx) или GNU (gcc -Wall)) будут выдаваться предупреждения для большинства случаев некорректного использования.
Во-вторых, в самом предложении N3369, есть макрос NITEMS(), который является вариацией макроса ARRAY_SIZE() сообщества Linux Kernel. Эти макросы используют достаточно широко распространённую встроенную функцию __builtin_types_compatible_p(), которая имеется у многих компиляторов: Clang (clang), GNU (gcc), Intel (icc), IntelLLVM (icx), LCC (lcc), NVHPC (pgcc/nvc), XLClang (IBM xlclang) и др.
Правда, при использовании этих двух вариантов, элементы нулевого размера всё равно придётся как-то из диеты исключить. Вероятно, постом и молитвой. Ключи вида -Werror=division-by-zero, -Werror=div-by-zero, --diag_error divide_by_zero, -errwarn=E_DIVISION_BY_ZERO и т.п., могут помочь, но гарантий не дают. Поэтому необходимо следить за этим вопросом при рефакторинге.
Далее рассмотрим иные варианты, которые, как минимум:
Вызывают ошибку компиляции в тех случаях, когда нарушаются ограничения (constraints) оператора
_Countof;Гарантировано не вызывают деления на 0.
Реализация countof_ns()
Без поддержки С23 и расширения __typeof__()
Такие компиляторы ещё существуют, как минимум, MS Visual Studio 2019 будет поддерживаться до 2029 года.
#define _countof_ns_unsafe(a) \ (0 == sizeof(*(a)) ? 0 : sizeof(a)/sizeof(*(a))) #define _countof_ns_must_array(a) (0) #define countof_ns(a) (_countof_ns_unsafe(a) + _countof_ns_must_array(a))
В отличие от классической идиомы, используется проверка на нулевой размер элемента, грубо говоря, если деление 0 на 0 не определено, то пусть будет 0 в этом случае. Для тех компиляторов, которые не поддерживают VLA и не поддерживают расширений ZLA (например, для MSVC), эта проверка не требуется, но и вреда от неё никакого (константное выражение будет вычислено во время компиляции).
Конечно, для случаев VLA или обычного массива, которые содержат элементы VLA, этот вариант вычислит аргумент три раза, в отличие от двух раз классической идиомы. Но, поскольку, стандартный countof() его должен вычислить не более одного раза, что одно неправильно, что другое неправильно. А гарантии отсутствия деления на 0, на мой взгляд, стоят непринципиального "увеличения" этой "неправильности".
Впрочем, для большинства компиляторов можно использовать оператор Элвиса (Conditionals with Omitted, расширение C/C++):
// Если 0 == sizeof((a)[0]), то sizeof(a) == 0 или sizeof(void *) #define _countof_ns_unsafe(a) \ (sizeof(a)/( sizeof((a)[0]) ?: 2*sizeof(void *) )) #define _countof_ns_must_array(a) (0*sizeof(sizeof(a)/sizeof((a)[0]))) #define countof_ns(a) (_countof_ns_unsafe(a) + _countof_ns_must_array(a))
В этом случае, член (0*sizeof(sizeof(a)/sizeof((a)[0]))) обеспечивает сохранение выдачи предупреждений компиляторов, в части неоднозначного использования оператора sizeof().
К сожалению, оба этих варианта теряют предупреждения компиляторов о делении на 0, в случае известных нулевых размеров элементов. Если таковые предупреждения представляются достаточно ценными можно, либо сначала проверить __builtin_constant_p(sizeof((a)[0])), а потом проверить sizeof((a)[0]), уже без нарушения константности выражения, и породить собственное предупреждение, либо воспользоваться другими реализациями, описанными ниже, у которых некорректное деление приводит к ошибке компиляции.
При условии использовании ключей -Werror=sizeof-pointer-div -Werror=sizeof-array-decay -Werror=sizeof-array-argument этот способ не хуже остальных.
Производительность
Здесь всё не так однозначно. Во-первых, в большинстве случаев это константное выражение. Во-вторых, в случае неконстантного выражения, лишняя проверка может, как снизить производительность, так и повысить, например, NVHPC (nvc 25.11) эта проверка позволяет оптимизатору убрать деление из кода.
Примеры кода: https://godbolt.org/z/vn35hP9eP
С23 или расширение __typeof__()
В предложении N3369 описано почему точная реализация макроса countof() без оператора _Countof на практике - невозможна, даже в рамках стандарта C23. Рассмотрим эти причины на предмет возможности реализации более менее близкого переносимого аналога без UB.
Переносимая проверка must_be_array(a)
Действительно, увы, нет прозрачного, простого и переносимого способа проверки того, что аргумент является массивом, но можно скомбинировать несколько способов.
Проверка в рамках C11
В стандартах C11...С23 для операции вычитания указателей указаны ограничения (constraints) - совместимость полных типов. Теоретически эти ограничения ничуть не хуже, чем точно такие же ограничения (constraints) на константное выражения определения _Static_assert() .
#if __STDC_VERSION__ >= 202311L #define _countof_ns_typeof(t) typeof(t) #else #define _countof_ns_typeof(t) __typeof__(t) #endif // Проверка, что `a` это массив содержащий `_countof_ns_unsafe(a)` // элементов (для VLA число элементов не ограничивается) #define _countof_ns_must_array(a) (0*sizeof( \ (_countof_ns_typeof(a) **)&(a) - \ (_countof_ns_typeof(*(a))(**)[_countof_ns_unsafe(a)])&(a)))
Но в реальной жизни у этого способа есть, как плюсы, так и, увы, минусы.
Плюсы:
Используется только расширение
__typeof__(), которое имеется почти у всех компиляторов (кроме, наверное,_MSC_VER < 1939);Практическое отсутствие UB. Строго говоря, согласно C11...С23 неопределённым поведением является, как несовпадение спецификаторов размера массивов используемых в операции, так и его равенство 0. Однако, такое может случиться только в случае, когда
aявляется расширением языка C на массивы с нулевым размером элементов, а на практике, у таковых расширений, поведение выглядит вполне определённым;Проверяется совместимость полных типов, поэтому массивы с нулевым размером элементов должны быть, либо ZLA, либо VLA. Массив фиксированной ненулевой длины с известным нулевым размером элементов вызовет синтаксическую ошибку, т.к.
_countof_ns_unsafe(a)вернёт константное выражение равное 0 и длина массива у типов сравнения будет отличаться;
Минусы:
Ввиду обратной совместимости с C99, у которого ограничения для операции вычитания указателей были более мягкие (такие же как у операции их сравнения), многие компиляторы предоставляют возможность управлять этими ограничениями;
Мало того, у некоторых компиляторов, по умолчанию, эти ограничения так и остались мягкими.
Использование _Generic()
Использование VLA в _Generic() возможно только в проекте C2Y, в котором уже есть стандартный _Countof. Однако, в случае __STDC_NO_VLA__ (в интерпретации C99...C17) это, возможно, наилучший вариант:
// Проверка, что `a` это массив содержащий `_countof_ns_unsafe(a)` // элементов (поддержка VLA или массивов содержаших VLA требует C2Y) #define _countof_ns_must_array(a) (_Generic( \ (_countof_ns_typeof(a) *)&(a), \ _countof_ns_typeof(*(a))(*)[_countof_ns_unsafe(a)]: 0))
Плюсы:
Используется только расширение
__typeof__(), которое имеется почти у всех компиляторов (кроме, наверное,_MSC_VER < 1939);Ограничения
_Generic()однозначно понимаются всеми компиляторами и однозначно вызывают ошибку компиляции, при нарушении;Проверяется совместимость полных типов, поэтому массивы с нулевым размером элементов должны быть ZLA. Массив фиксированной ненулевой длины с известным нулевым размером элементов вызовет синтаксическую ошибку, т.к.
_countof_ns_unsafe(a)вернёт константное выражение равное 0 и длина массива у типов сравнения будет отличаться;
Минусы:
Подходит только в случаях
__STDC_NO_VLA__(в интерпретации C99...C17) или уже для C2Y (или на некоторых компиляторах).
Использование __builtin_types_compatible_p()
Сама эта функция достаточно распространена и, если использовать её в позитивном варианте:
#if __STDC_VERSION__ >= 202311L #define _countof_ns_assert static_assert #else #define _countof_ns_assert _Static_assert #endif // Проверка, что `a` это массив содержащий `_countof_ns_unsafe(a)` // элементов (для VLA число элементов не ограничивается) #define _countof_ns_must_array(a) \ (0*sizeof(struct { int _countof_ns; _countof_ns_assert( \ __builtin_types_compatible_p( \ _countof_ns_typeof(a) *, \ _countof_ns_typeof(*(a))(*)[_countof_ns_unsafe(a)] \ ), "countof_ns: " #a " : Must be array"); }))
, то получим ограничения идентичные ограничениям C11 на разность указателей (см. выше). Кроме того, получим возможность гарантировать и документировать настройки тех компиляторов, которые застряли во временах C99, если вообще не "во временах Очаковских и покоренья Крыма", к примеру:
$ icc -diag-error=1121 \ -D'__builtin_types_compatible_p(ta,tb)=(0==0*sizeof((ta)0-(tb)0))' > cl /std:clatest /wd4116 /we4047 /we4048 /FImsvc_builtin_types_compatible_p.h $ pgcc --diag_error=nonstandard_ptr_minus_ptr \ -D'__builtin_types_compatible_p(ta,tb)=(0==0*sizeof((ta)0-(tb)0))' $ suncc -errwarn=E_BAD_POINTER_SUBTRACTION \ -D'__builtin_types_compatible_p(ta,tb)=(0==0*sizeof((ta)0-(tb)0))' $ xlc -qlanglvl=extc1x -qhaltonmsg=1506-068 \ -qinclude=xlc_builtin_types_compatible_p.h
Относительно этих вариантов флагов, следует заметить, что наиболее привередливые компиляторы предупреждают: "разность нулевых указателей это не есть хорошо". Но у этих зануд уже есть своя функция __builtin_types_compatible_p(), мы их просто не настраиваем, поэтому и не слушаем. 😉
Многие используют эту встроенную функцию в негативном варианте !__builtin_types_compatible_p(_countof_ns_typeof(&(a)[0]), _countof_ns_typeof(a)), т.е. как проверку на то, что a не является указателем, без проверки корректности оценки числа элементов массива. Но такой вариант проверки невозможно реализовать вычитанием указателей на чистом C11, без этой встроенной функции.
Поддержка аргумента "тип массива"
В принципе, этот аргумент N3369 вполне разумный. И хотя для C, как кажется, это несложно:
#define _countof_ns_typ2arr(a) (*(_countof_ns_typeof(a) *)0) #define countof_ns(a) (_countof_ns(_countof_ns_typ2arr(a)))
Проблема в том, что typeof() должен вычислять свой операнд для изменяемых типов, но sizeof() не должен вычислять свой операнд, если результат имеет известное константное значение. В результате, число вычислений операнда составной конструкции должно совпадать с числом вычислений операнда простого sizeof(). Но, похоже, только GNU (gcc) реализует корректное число вычислений для выражения вида: sizeof(*(typeof(v[i++]) *)0)/sizeof((*(typeof(v[i++]) *)0))[0]).
Фиксированый массив элементов VLA
По определению, _Countof от фиксированного массива a должен быть целочисленной константой, даже если его элементы VLA. Но sizeof(a) и sizeof((a)[0]) не являются константами, и современные компиляторы не считают sizeof(a)/sizeof((a)[0]) константой.
Второй момент, по определению, в этом случае, _Countof не должен вычислять свой операнд, а любые полезные конструкции: typeof(a), sizeof(a), auto v = (a), typedef typeof(a) t_t и т.п., его должны вычислить.
На настоящий момент это выглядит, как принципиальное ограничение.
Двойное вычисление
В частном случае VLA содержащего VLA (многомерный VLA и т.п.) для некоторых компиляторов можно было бы обеспечить единственное вычисление аргумента, примерно так:
#define countof_ns(a) (__builtin_constant_p(a) \ ? _countof_ns(a) \ : ({ _countof_ns_typeof(a) *_countof_ns_a; \ _countof_ns(*_countof_ns_a); }))
К сожалению это вариант имеет множество ограничений:
Это только частный случай, поскольку не обеспечивает полного исключения побочных эффектов
typeof(a)для предыдущего случая фиксированного массива элементов VLA;GNU расширение оператора-выражения неприменимо вне тела функции;
Некоторые компиляторы не поддерживают в операторах-выражениях использование VM типов.
По всей видимости, в языке C нет способа реализации макроса countof_ns() для случая аргумента с побочными эффектами.
Реализация countof_ns() в C++
Как говорил Сунь-цзы: "...лучшее из лучшего — покорить чужую армию, не сражаясь...", возможно, проще всего, VLA или ZLA расширения С++ сразу превращать в std::span и/или std::mdspan, к примеру:
size_t _n = 0; // Или иной размер/квалификатор int _a[_n]; auto a = std::span(_a, _n); size_t _m = 0; // Или иной размер/квалификатор int _b[_m][_n]; auto b = std::mdspan(_b[0], _m, _n);
В этом случае std::size(a) или b.extent(r) будут замечательно работать при любом сочетании размеров.
Правда, автоматизация превращения расширенных массивов с неявным заданием размера может вызывать некоторые сложности:
union T0 { int z[0]; }; T0 a0, a1, a2; T0 _a[] = { a0, a1, a2 }; T0 _b[] = {}; auto a = std::span(_a); // Для обычного массива можно размерность опустить auto b = std::span(_b, 0); // А для ZLA она нужна, но немного мешает 0/0 static_assert(0 == sizeof(_a) && 0 == sizeof(_b)); assert(3 == std::size(a) && 0 == std::size(b));
В общем, практический смысл countof_ns() для C++ неясен, но теоретическая задача имеет более менее чёткое определение. Тем более интересная, поскольку C++ позволяет обойтись без деления, правда, только для фиксированных массивов.
ZLA и шаблоны
К сожалению, все компиляторы единодушны, std::size() не приемлет ZLA расширений C++, хотя для контейнеров, он успешно возвращает 0. Так же, у всех, свойство std::extent_v<>, для ZLA возвращает 0, собственно, оно возвращает 0 для любых типов, которые не являются std::is_bounded_array_v<>, даже для void.
А вот в вопросе свойств std::is_array_v<> и std::rank_v<>, и в вопросе сопоставления с шаблонами, мнения компиляторов расходятся:
Сравнение компиляторов
int zla[0]; #if __NVCOMPILER_MAJOR__ > 21 || __SUNPRO_CC || (__clang__ && \ __clang_major__ <= 18 && !__apple_build_version__ && !__INTEL_COMPILER) static_assert(std::is_array<decltype(zla)>::value, "std::is_array_v<>"); #else static_assert(!std::is_array<decltype(zla)>::value, "!std::is_array_v<>"); #endif #if __SUNPRO_CC || __GNUC__ >= 15 || (__clang_major__ >= 21 && \ !__INTEL_LLVM_COMPILER) static_assert(1 == std::rank<decltype(zla)>::value, "1 == std::rank_v<>"); #else static_assert(0 == std::rank<decltype(zla)>::value, "0 == std::rank_v<>"); #endif static_assert(0 == std::extent<decltype(zla)>::value, "0 == std::extent_v<>"); template <class T, size_t N> constexpr static size_t size(const T (&)[N]) { return N; } template <class T> constexpr static size_t size(const T (&)[0]) { return 0; } template <class T> constexpr static size_t size(const T&) { return 1917; } #if !__clang__ || __INTEL_COMPILER static_assert(0 == size(zla), "ZLA is T[0]"); #else static_assert(1917 == size(zla), "ZLA not T[0]"); #endif
В общем, для ZLA сложно применить общепринятые шаблоны свойств, проще иметь собственную реализацию. Кроме того, по аналогии с std::size(), будем считать контейнером любой тип, который имеет метод size(). Для того, что бы аргумент не вычислялся, используем sizeof() от разыменованного возвращаемого значения, за исключением случая контейнера, для которого вызовем метод size() и, только при этом вызове, аргумент будет вычислен:
// Непроверяемый размер заглушка, только для успеха компиляции constexpr size_t unthinkable = 1917; constexpr size_t bias = 1; class no_t { char no_[1]; }; class yes_t { long long yes_[2]; }; // T - контейнер (имеется `size()`) template <class T> struct has_size { template <class C> static yes_t test_(decltype(&C::size)); template <class> static no_t test_(...); static constexpr bool value = sizeof(test_<T>(0)) == sizeof(yes_t); }; // T - ZLA template<class T> struct is_zla : std::integral_constant<bool, 0 == sizeof(T) && 0 == std::extent<T>::value && !std::is_class<T>::value && !std::is_function<T>::value && !std::is_scalar<T>::value && !std::is_union<T>::value && !std::is_void<T>::value> {}; // Число элементов фиксированного массива (стандартный C++) template<class T, size_t N> static char (*match(const T (&)[N]))[bias + N]; // Число элементов ZLA template <class T, typename std::enable_if< is_zla<T>::value, int>::type = 0> static char (*match(const T&))[bias + 0]; // Заглушка контейнера template <class C, typename std::enable_if< has_size<C>::value, int>::type = 0> static char (*match(const C&))[unthinkable]; // Число элементов контейнера template <class C, typename std::enable_if< has_size<C>::value, int>::type = 0> constexpr static auto cnt_size(const C &c) noexcept(noexcept(c.size())) { return c.size(); } constexpr static auto cnt_size(...) noexcept { return unthinkable; } #define countof_ns(a) (_countof_ns_::has_size<decltype(a)>::value \ ? _countof_ns_::cnt_size(a) \ : sizeof(*_countof_ns_::match(a)) - \ _countof_ns_::bias)
Результаты этого варианта countof_ns(a) идентичны результатам оператора _Countof проекта C2Y для массивов фиксированного размера.
VLA и __is_same()
Использование VM типов в шаблонах C++ - нерешённая проблема, подавляющее большинство компиляторов просто запрещают такое использование.
Однако, почти все компиляторы, которые реализуют расширение VLA C++, так же поддерживают встроенную функцию __is_same(), при помощи которой можно различать указатели, массивы и т.п. У SunPro (sunCC) этой функции нет, зато он поддерживает использование VM типов в качестве аргументов шаблонов, хотя и ограничено. Кроме того, к сожалению, многие компиляторы не поддерживают __has_builtin(__is_same), но поскольку, мне пока известен только один SunPro, который поддерживает расширение VLA С++ и её не имеет, думаю, можно её использовать по умолчанию:
template<bool IsArray> constexpr static size_t zero_assert() noexcept { static_assert(IsArray, "Must be VLA"); return 0; } #if !__SUNPRO_CC // Проверка, что аргумент - VLA #define _countof_ns_must_vla(a) (_countof_ns_::zero_assert< \ !__is_same(decltype(&(a)[0]), decltype(a))>()) // Тип аргумента не является изменяемым // !match_not_vmt для VM типов template<class T> static yes_t match_not_vmt(const T&); #else #define _countof_ns_must_vla(a) (_countof_ns_::zero_assert< \ !std::is_pointer<decltype(a)>::value>()) // !match_not_vmt для VLA или ZLA template <class T, typename std::enable_if< !std::is_array<T>::value || 0 < std::extent<T>::value, int>::type = 0> static yes_t match_not_vmt(const T&); #endif static no_t match_not_vmt(...); // Число элементов VLA #define _countof_ns_vla(a) \ (_countof_ns_unsafe(a) + _countof_ns_must_vla(a)) // Аргумент - конейнер template <class C, typename std::enable_if< has_size<C>::value, int>::type = 0> static yes_t match_cnt(const C&); static no_t match_cnt(...); // Число элементов фиксированного массива, возможно, ZLA template <class T> static auto match(yes_t not_vmt, const T& a) -> decltype(match(a)); static char (*match(no_t not_vmt, ...))[unthinkable]; #define countof_ns(a) (sizeof(_countof_ns_::match_not_vmt(a)) == \ sizeof(_countof_ns_::no_t) \ ? _countof_ns_vla(a) \ : sizeof(_countof_ns_::match_cnt(a)) == \ sizeof(_countof_ns_::yes_t) \ ? _countof_ns_::cnt_size(a) \ : sizeof(*_countof_ns_::match( \ _countof_ns_::match_not_vmt(a), \ (a))) - \ _countof_ns_::bias)
Результаты этого варианта countof_ns(a) идентичны результатам оператора _Countof проекта C2Y для массивов фиксированного размера не содержащих VM типов. В случае VLA, вычисление числа элементов будет происходить во время выполнения и, если 0 == sizeof(a), будет всегда возвращаться 0.
Для VLA, число вычислений аргумента зависит от компилятора, т.к. некоторые придерживаются правил языка C в части VLA, а некоторые нет, и не вычисляют операнд sizeof().
Статическая рефлексия C++26
Использование оператора рефлексии ^^decltype(a) позволяет:
Получить свойства типа выражения минуя сопоставления с шаблонами, что даёт возможность избавиться от вызовов встроенных функций;
Улучшить диагностику с использованием исключений времени компиляции, и упростить её тестирование;
Упростить анализ допустимости типов;
Спецификатор
decltype()не вычисляет свой операнд, что упрощает мимикрию под_Countof.
// Тип аргумента не является изменяемым template<class T> constexpr bool _detect_not_vmt; consteval bool _not_vmt(info type) { return can_substitute(^^_detect_not_vmt, {type}); } #define not_vmt(a) _not_vmt(^^decltype(a)) // TODO: обход ошибки clang // Проверка, что аргумент - VLA consteval size_t _must_vla(info type) { if (!_not_vmt(type) && 0 == rank(type)) { throw exception("Must be VLA", ^^type); } do { type = remove_extent(type); if (_not_vmt(type) && 0 == size_of(type)) { throw exception("VLA has zero size elements", ^^type); } } while (rank(type)); return 0; } #define must_vla(a) _must_vla(^^decltype(a)) // TODO: обход ошибки clang // Число элементов VLA #define _countof_vla(a) (_countof_ns_unsafe(a) + must_vla(a)) // Аргумент - контейнер (имеется `size()`) consteval bool has_size(bool not_vmt, info type) { // TODO: not_vmt - обход ошибки clang: крах на _not_vmt(type) if (not_vmt && is_class_type(type)) { auto ctx = access_context::current(); auto ms = define_static_array(members_of(type, ctx) |std::views::filter(std::meta::is_function) |std::views::filter(std::meta::has_identifier) |std::views::filter([](const std::meta::info& m){ return identifier_of(m) == "size"; })); return !ms.empty(); } return false; } // Размер заглушки, только для успеха компиляции constexpr size_t unthinkable = 1917; // Число элементов фиксированного массива, возможно, ZLA consteval size_t count_of(bool not_vmt, info type) { if (not_vmt) { // TODO: обход ошибки clang: крах на _not_vmt(type) if (1 <= rank(type)) { return extent(type); } else if(!has_size(not_vmt, type)) { throw exception("Must be array or container", ^^type); } } return unthinkable; } // Число элементов контейнера template<class C> requires (has_size(true, ^^C)) constexpr static auto cnt_size(const C& c) noexcept(noexcept(c.size())) { return c.size(); } constexpr static size_t cnt_size(...) { return unthinkable; } #define countof_26(a) (!not_vmt(a) \ ? _countof_vla(a) \ : has_size(not_vmt(a), ^^decltype(a)) \ ? cnt_size(a) \ : count_of(not_vmt(a), ^^decltype(a)))
Недостатком оператора рефлексии ^^decltype(a) можно назвать тот факт, что тип std::meta::info является consteval, соответственно, функции для работы с ним должны быть consteval. В частности поэтому, в случае VLA, невозможно просто вызвать extent(^^decltype(a)) [meta.reflection.traits (8)].
Наверное, было бы неплохо, что бы для VM типов были бы функции вида:
consteval bool has_known_constant_size(info type); consteval bool has_known_constant_extent(info type); class variably_modified_size_of { public: consteval variably_modified_size_of(info type); constexpr size_t size_of(void) const; }; class variably_modified_extent { public: consteval variably_modified_extent(info type); constexpr size_t size(void) const; };
, но, увы, пока ничего похожего нет, ни в проекте C++26, ни в расширениях Clang или GNU (в принципе, у Clang есть встроенная функция __array_extent(), но для VLA она возвращает 0).
Таким образом, в проекте C++26, так же как в C++14, для VLA приходится делить sizeof() на sizeof() и невозможно получить точное число элементов для VLA с элементами нулевого размера. С тем отличием, что в проекте C++26 must_vla() может быть выражен с использованием стандартных интерфейсов rank(^^decltype(a)) [meta.reflection.traits (7)] или is_pointer_type(^^decltype(a)) [meta.reflection.traits (2)]. Поэтому, есть шанс, что в C++26 это можно будет сделать более менее "чисто". Будем посмотреть, спецификации интерфейсов этого не запрещают, но не и обязывают, поскольку не специфицируют реализацию std::meta::info.
Примечание: ни один из компиляторов не смог собрать вышеприведённый код этого раздела полностью, где-то у одного ошибка, где-то у другого. Версии ещё предварительные и сырые, без помощи #ifdef и какой-то матери не обойтись.
On-line вариант C++26 кода: https://godbolt.org/z/oPhe68adc
Заключение
В предварительной переносимой версии C23/C++14 (или с использованием расширений компиляторов, ссылка на GitHub ниже) более менее удалось объединить выше упомянутые методы для большинства компиляторов, со следующими ограничениями для C:
Ошибка компиляции для стандартных фиксированных массивов с элементами известного нулевого размера;
Для массивов ZLA и VLA, содержащих элементы нулевого размера, возвращается 0 (для ZLA это точное значение);
Для VLA, число вычислений аргумента может отличаться от оператора
_Countof, но совпадает с числом вычислений аргумента классической идиомы.
Вариант для C++:
Для фиксированных массивов, включая ZLA, результат идентичен результату
_Countof, аргумент не вычисляется;Для VLA, содержащих элементы нулевого размера, возвращается 0;
Для VLA, число вычислений аргумента зависит от компилятора и может отличаться, как от оператора
_Countof, так и от выполнения классической идиомы в языке C.
В принципе, переход от классической идиомы к использованию оператора _Countof устраняет многие проблемы и риски ошибок при рефакторинге или во время выполнения, но поведение для аргументов с побочными эффектами будет отличаться. Проект C2Y, конечно, ещё не окончательный, но, во-первых, не видно прямых путей для исключения этого отличия. И, во-вторых, в обосновании предложения N3369 прямо указано - уменьшение накладных расходов является одной из целей введения нового оператора.
Поэтому, для поиска проблемных мест, стоит использовать какие-нибудь анализаторы кода. К примеру, совместно с компилятором Clang можно использовать:
$ clang-tidy -config="{ Checks: bugprone-assert-side-effect, CheckOptions: { bugprone-assert-side-effect.AssertMacros: 'assert,NSAssert,NSCAssert,sizeof,_Countof,countof,countof_ns' } }" ...
Ссылки
GitHub: countof_ns.h;
GitHub: C23/C++14 platform independent implementation of C2y countof();
WG14: N3469: Big Array Size Survey;
WG14: N3783 working draft;
Stack Overflow (SO): How do I determine the size of my array in C?;
SO: Is there a way for countof() to test if its argument is an array?;
