Как определить число элементов массива C?
Хотелось бы что-то более менее компактное, понятное и переносимое (в рамках стандартов). Из готовых имеем:
ARRAY_SIZE(arr)от Linux Kernel/QEMU, два сапога - пара. Из хорошего - выдают либо размер, либо ошибку на этапе сборки. Из плохого - не всякий компилятор сможет это скомпилировать, даже если чем-то заменить__builtin_types_compatible_p(). Впрочем и её замена неочевидна;_countof(array)от MS, здесь другая крайность. Из хорошего - поддерживает все стандарты, от C99/С++03 (думаю, гораздо дальше, чем кому-либо может понадобиться). Из плохого - MS "любит" C++ и "не любит" C, если C++ вариант - неплох, то вариант C - рассчитан на ответственных и квалифицированных пользователей;- В clang-21 появился
stdcountof.hиз черновиковC2y, но это ж чистый интерфейс к оператору_Countof.
На enSO нашлось несколько похожих вопросов:
- Array-size macro that rejects pointers
- How do I determine the size of my array in C?
- Is there a way for countof() to test if its argument is an array?
К сожалению, там практически не нашлось хороших, корректных и переносимых ответов. Увы, хотя ответов много, но те, которые кажутся более менее корректными - непереносимы, а те которые более менее переносимы - кажутся некорректными.
Было навеяно вопросом Имя массива это адрес первого элемента или указатель на его первый элемент в Си?
P.S.
Пункт дополнительный, насчет VLA или обычных массивов c младшими индексами длины 0 (пока, это ещё расширение clang/gcc, но включенное по умолчанию). Оператор _Countof в clang-21 выдаёт такое:
m=5, n=0, countof(vlm) = 5, sizeof(vlm)=0
countof(int[m][n]) = 5
m=5, n=0, countof(fixm) = 5, sizeof(fixm)=0
countof(int[5][0]) = 5
countof(int[0][5]) = 0
Что сиё означает, и как это дело повторить на C, не очень понятно. Когда младшие длины нули, как узнать старшие?
P.P.S.
Собственно, а на фига козе баян?
Первоначально, я послал человека на эти вопросы enSO, для иллюстрации различия между указателем и массивом. А потом сам почитал ответы, ну и, как бы, оказался ими немного неудовлетворен. Особенно, в свете C23. На днях, два года прошло, это уже срок.
Но скажите, чем на практике не устраивает макрос -- SIZE_A(A) (sizeof(A) / sizeof((A)[0]))
Примерно до середины 2024 года, было два соображения:
sizeof(A) / sizeof((A)[0])- замечательное выражение и, если бы, не clang/gcc/intel/... Однако, при использовании этих компиляторов, можно получить деление на ноль, на ровном месте. Они, конечно, напечатают предупреждение при компиляции, но это не помешает коду запуститься;Можно сослаться на замечательное дацзыбао от Линуса Торвальдса, "...Господи, люди Твоя! Изучайте C, а не просто соединяйте случайные символы друг с другом, пока код не скомпилируется (с предупреждениями)...". Увы, глас вопиющего в пустыне, с одной стороны, рефакторинг - объективная реальность, а с другой стороны, не в человеческих это силах, выполнить его без такого рода идиотских ошибок.
К примеру:
int cross_debts[Nusers][Nadmins][Nprogrammers];
for (size_t i = 0; i < SIZE_A(cross_debts); i++) {
for (size_t j = 0; j < SIZE_A(cross_debts[0]); i++) {
for (size_t k = 0; k < SIZE_A(cross_debts[0][0]); k++) {
...
struct ticket_t {
struct hdr_t hdr;
struct payload_t payloads[]; // или payloads[0]
};
struct ticket_t tickets;
for (size_t i = 0; i < SIZE_A(tickets); i++) {
...
Если, изменить
const size_t Nprogrammers = 0или перенести полеhdrвнутрьpayloads, то, при использовании clang/gcc/..., получим предупреждение времени компиляции (возможно) и ошибку деления времени выполнения;Если заменить структуру хранения/передачи
cross_debtsилиtickets, типа, массивы на указатели, то получим предупреждение времени компиляции (возможно) и какую-то хрень времени исполнения.
И, в середине 2024, WG14 наконец прислушался к народным чаяниям и проголосовал за введение: _Lengthof, потом переименовал его в elementsof => nelementsof/neltsof() => _Lengthof => _Countof/countof(), на чём пока и остановился. Не суть. Главное, Улита едет, когда-то будет.
Если до середины 2024, это было чаще личное дело. Чаще каждый сам решал, делать ли макрос, или так делить, или что-то ещё, причём локально, по месту. Ну, кроме некоторых проектов, к примеру Linux Kernel или QEMU, с их ARRAY_SIZE(arr). То сейчас уже понятно, что рано или поздно во многих местах появятся строки:
#ifndef countof // Для компиляторов без поддержки C2y...
...
А вот, что стоит записать вместо этого многоточия, это вопрос (он же - третье соображение).
Ответы (1 шт):
Начну с предупреждений и критики. На enSO весьма много ответов основанных на предикате sizeof(*arr) && ((void *)&(arr) == (arr)) или его вариациях. Такого рода ответов, может четверть, а может и больше. К сожалению, этот предикат проблемный, хотя и привлекательный:
- Да, так сказать, "переносимость", на первый взгляд, отличная. На C99 будет компилироваться, но...;
- Но есть нюансы, главный из которых: он некорректный(!), поскольку указателю никто не запрещает указывать на самого себя. Особенно, если он неинициализированный, впрочем и инициализированному никто не запрещает. Вероятность этого может быть любая, к примеру, некоторые списочные, очередные или древовидные структуры любят и умеют в такие ссылки;
- Кроме того, поскольку, формально, результат зависит (или может зависеть) от памяти, многие компиляторы не признают его за
constexpr. Поэтому ошибку времени компиляции от него получить затруднительно. Если вообще возможно, по крайней мере, переносимым образом; - Во многих ответах реализацию ошибок времени выполнения бросают на пол дороге и сводят её к делению на 0 (UB). Хотя встречалось и более корректное сведение к
raise()/abort().
В общем, популярность этих, не очень корректных, вариантов понятна. До C23, лично мне неизвестно, корректных стандартных вариантов. Наверное, при определённых ограничениях, с помощью memcpy() и какой-то матери, его можно сделать более менее корректным, но таких ответов не нашлось.
С23/C++ вариант countof_ns(a)
#if !defined(__cplusplus)
#if __STDC_VERSION__ >= 202311L
#define _countof_ns_typeof(t) typeof(t)
#elif (!defined(_COUNTOF_NS_WANT_STDC) && \
(__clang_major__ >= 4 || __GNUC__ >= 5 || _MSC_VER >= 1939)) // TODO
#define _countof_ns_typeof(t) __typeof__(t)
#else
#error "typeof(t) don't detected, need C23 or clang/gcc/MSVC/... extension"
#endif
#define _countof_ns_unsafe(a) (sizeof(*(a)) ? sizeof(a)/sizeof(*(a)) : 0)
#define _countof_ns_must_be(a) ((_countof_ns_typeof(a) **)8 - \
(_countof_ns_typeof(*(a))(**)[_countof_ns_unsafe(a)])8)
#define countof_ns(a) (_countof_ns_unsafe(a) + (size_t)_countof_ns_must_be(a))
#else
template<size_t A, size_t E, class T, size_t N>
constexpr static size_t _countof_ns_aux(T (&)[N]) { return N; }
template<size_t A, size_t E, class T>
constexpr static size_t _countof_ns_aux(T (&)) {
static_assert(0 == A, "Argument zero-length array");
return 0;
}
#define countof_ns(a) _countof_ns_aux<sizeof(a), sizeof(*(a))>(a)
#endif
Т.е. проверяем совместимость указателей - их вычитанием, в случае неуспеха - ошибка сборки. Дополнительный уровень косвенной адресации, при проверке совместимости, необходим для gcc/clang расширений массивов со старшей размерностью нулевой длины, как обычных, так и VLA.
Это вариант был получен по мотивам ответов James Z.M. Gao: копия №1, копия №2.
Альтернативные варианты
- Очень много ответов по мотивами Linux Kernel, например, самый подробный. Были рассуждения, как его прикрутить к MSVC, но до дела не дошло;
- Несколько вариантов проверки совместимости указателей при помощи
_Generic(&(p)..., поддерживает одномерные VLA или даже_Generic(a, T *: _Generic(&a,..., несовместим с VLA. Но в них, кроме проверки совместимости, ещё ж надо ошибку сборки как-то вызывать, что непросто; - Проверка совместимости типа с помощью присвоения указателя, но для переносимости требуется имитация GCC Statement Exprs, наверное, сведением к
do { ... } while(0); - Достаточно интересный вариант: проверяем возможность создания и инициализации временного массива, но опять GCC Statement Exprs;
Это всё варианты на основе typeof() (С23). До конца 2023, многие комментировали, а зачем? Что __typeof__() - gcc/clang расширение, что __builtin_types_compatible_p() - gcc/clang расширение, так зачем? См. п. 1.
Все известные мне альтернативные варианты реализации на C (так же как и C часть этого варианта), в отличие от текущего черновика C2y countof(), неспособны вычислить число элементов массива, если размер элемента равен 0. На настоящий момент, мне неизвестны способы получения числа элементов в этом случае. Пока максимум, что можно потребовать это: исключить деление на 0 (во многих вариантах, об этом забыто, а это UB, иногда с забавными последствиями) и обеспечить ошибку компиляции в этом случае.
Альтернативный вариант С++ части
Можно немного сократить и унифицировать код, если начать его так:
#ifdef __cplusplus
#include <type_traits>
#define _countof_ns_typeof(t) std::remove_reference_t<decltype(t)>
#elif __STDC_VERSION__ >= 202311L
...
Однако, в этом случае не все варианты использования расширений clang/gcc/Intel/... для обычных массивов с размерностями равными 0 будут работать, т.е. это всё равно останется ещё лучше, чем std::size(), но уже станет хуже, чем C2y countof().
Я предпочёл точное соответствие семантики C++ части текущему черновику C2y countof() (но, в процессе стандартизации, он ещё может измениться).
P.S.
Я не имел ввиду сказать, что std::size() чем-то плох, это отличный инструмент, который не подкинет проблем на ровном месте. Не то, что иные. Если подать на вход странный массив, он холодно, во время компиляции, Вас пошлёт, и будет по своему прав. Другое дело, если C часть кода использует C2y count(), то для C++ части нужна реализация, желательно, с идентичным представлением о добре и зле. Пока же, для clang-21, у C2y countof() и C++ std::size() представления о добре и зле несколько разные.
Пример использования
#include "countof_ns.h"
#include <assert.h>
#include <stdio.h>
#if defined __has_include
#if __has_include(<stdcountof.h>) && !defined(__cplusplus)
#include <stdcountof.h>
#endif
#endif
#if __cplusplus >= 201703L || __cpp_lib_nonmember_container_access >= 201411L
#include <vector>
#define std_size(a) (std::size(a))
#endif
#if !defined(__cplusplus) && !defined(static_assert)
#define static_assert(e, s) assert((e) && (s))
#endif
#if !defined(__cplusplus) && !defined(thread_local)
#define thread_local _Thread_local
#endif
#if __clang__ || __GNUC__
#define MIN_DIM (0)
#else
#define MIN_DIM (1)
#endif
#if __clang_major__ >= 19 || __GNUC__ >= 15
#define HAVE_FLEXIBLE_ONLY (1)
#endif
int main() {
const int a1[42] = {};
volatile int a2[42][56];
static thread_local int a3[42][56][23];
int min1[MIN_DIM] = {};
int min2[MIN_DIM][42];
int min3[MIN_DIM][MIN_DIM];
int min4[42][MIN_DIM][56];
struct {
int mins[MIN_DIM];
} s;
#if HAVE_FLEXIBLE_ONLY
struct {
int flex[];
} flexsmin[MIN_DIM];
struct {
int flex[];
} flexs42[42];
#endif
size_t res = countof_ns(a1);
// Negative examples
// res += countof_ns(&a1);
// res += countof_ns(a1[0]);
// res += countof_ns(&a1[0]);
// res += countof_ns(&min1);
// res += countof_ns(&min3[0]);
// res += countof_ns(s);
#if HAVE_FLEXIBLE_ONLY
// res += countof_ns(flexs42[0]);
#endif
// Positive examples
static_assert(countof_ns(a1) == 42, "a1");
static_assert(countof_ns(a2) == 42, "a2");
static_assert(countof_ns(a2[0]) == 56, "a2[0]");
static_assert(countof_ns(a3) == 42, "a3");
static_assert(countof_ns(a3[0]) == 56, "a3[0]");
static_assert(countof_ns(a3[0][0]) == 23, "a3[0][0]");
static_assert(countof_ns(min1) == MIN_DIM, "min1");
static_assert(countof_ns(min2) == MIN_DIM, "min2");
static_assert(countof_ns(min2[0]) == 42, "min2[0]");
static_assert(countof_ns(min3) == MIN_DIM, "min3");
static_assert(countof_ns(min3[0]) == MIN_DIM, "min3[0]");
#if defined(__cplusplus) || MIN_DIM > 0
static_assert(countof_ns(min4) == 42, "min4");
#endif
static_assert(countof_ns(min4[0]) == MIN_DIM, "min4[0]");
static_assert(countof_ns(min4[0][0]) == 56, "min4[0][0]");
static_assert(countof_ns(s.mins) == MIN_DIM, "s.mins");
#if HAVE_FLEXIBLE_ONLY
static_assert(countof_ns(flexsmin) == MIN_DIM, "flexsmin");
#if defined(__cplusplus)
static_assert(countof_ns(flexs42) == 42, "flexs42");
#endif
(void)flexs42;
#endif
#ifdef countof
printf("Comparison with C2y countof()\n");
static_assert(sizeof(min4[0]) == 0 &&
countof(min4) == 42, "C2y countof(min4)");
#if HAVE_FLEXIBLE_ONLY
static_assert(countof(flexs42) == 42, "C2y countof(flexs42)");
#endif
#endif
#if !__STDC_NO_VLA__ && !defined(__cplusplus)
for(size_t n = MIN_DIM; n < 4; n++) {
int vla[n];
assert(countof_ns(vla) == n);
for(size_t m = MIN_DIM; m < 4; m++) {
int vlm[n][m];
assert(m <= 0 || countof_ns(vlm) == n);
assert(countof_ns(vlm[0]) == m);
#ifdef countof
assert(countof(vlm) == n);
assert(countof(vlm[0]) == m);
#endif
for(size_t l = MIN_DIM; l < 4; l++) {
int vlt[n][m][l];
assert(m <= 0 || l <= 0 ||
countof_ns(vlt) == n);
assert(l <= 0 ||
countof_ns(vlt[0]) == m);
assert(countof_ns(vlt[0][0]) == l);
#ifdef countof
assert(countof(vlt) == n);
assert(countof(vlt[0]) == m);
assert(countof(vlt[0][0]) == l);
#endif
}
}
}
#endif
#ifdef std_size
printf("Comparison with std::size()\n");
static_assert(std_size(a1) == 42);
static_assert(std_size(a2) == 42);
static_assert(std_size(a2[0]) == 56);
static_assert(std_size(a3) == 42);
static_assert(std_size(a3[0]) == 56);
static_assert(std_size(a3[0][0]) == 23);
static_assert(std_size(min4) == 42);
static_assert(std_size(min4[0][0]) == 56);
#if HAVE_FLEXIBLE_ONLY
printf("Comparison with std::size() "
"and flexible array member only\n");
static_assert(std_size(flexs42) == 42);
#endif
#if MIN_DIM > 0
static_assert(std_size(min1) == MIN_DIM);
static_assert(std_size(min2) == MIN_DIM);
static_assert(std_size(min3) == MIN_DIM);
static_assert(std_size(min3[0]) == MIN_DIM);
static_assert(std_size(min4[0]) == MIN_DIM);
static_assert(std_size(s.mins) == MIN_DIM);
#if HAVE_FLEXIBLE_ONLY
static_assert(std_size(flexsmin) == MIN_DIM);
#endif
#endif
#endif
#ifdef __cplusplus
#ifdef __cpp_lib_nonmember_container_access
printf("__cpp_lib_nonmember_container_access %ld ",
__cpp_lib_nonmember_container_access);
#endif
printf("__cplusplus %ld\n", __cplusplus);
#else
#ifdef __STDC_NO_VLA__
printf("__STDC_NO_VLA__ %d ", __STDC_NO_VLA__);
#endif
printf("__STDC_VERSION__ %ld\n", __STDC_VERSION__);
#endif
(void)res;
return 0;
}