Хорошие новости для пользователей gcc — при использовании gcc 5.1 и выше им будет проще быстро находить вот такую распространенную ошибку вычисления размера массива, объявленного как параметр функции:void something( char arr[100] )
{
// this loop is broken
for( size_t index = 0; index < sizeof(arr)/sizeof(arr[0]); index++ ) {
//WHATEVER
}
}Хотя параметр и объявлен как массив известного размера, с точки зрения компиляторов C и C++ это указатель типа char*, поэтому sizeof(arr) даст то же значение, что и sizeof(char*) – скорее всего, 4 или 8. Цикл, скорее всего, будет работать не так, как ожидалось.
Другой вариант:
void something( char encryptionKey[9000] )
{
// WHATEVER, PROFIT
// this call is broken
SecureZeroMemory( encryptionKey, sizeof(encryptionKey)); // erase the key
}здесь разработчики хотели перезаписать нулями какие-то данные, но из-за ошибки будут перезаписаны только первые несколько байт. Такую ошибку сложнее найти тестированием, чем первую.
Чтобы найти такой код было проще, в gcc 5.1 и новее на такой код выдается предупреждение и оно включено по умолчанию.
Отдельные читатели уже спешат в комментарии, чтобы рассказать о кривизне рук авторов кода из примеров выше. Тем не менее, проблема настолько распространенная, что в коде на C++ рекомендуется использовать следующий фокус (отсюда) с шаблонной функцией:
template<typename StoredType, size_t Size>
char ( &ArrayElementsCountHelper(StoredType( &Array )[Size]) )[Size];
#define countOfElements(Array) sizeof(ArrayElementsCountHelper (Array))
Использование countOfElements() в коде выше приведет к ошибке компиляции, зато такой код:
char arr[100]
for( size_t index = 0; index < countOfElements(arr); index++ ) {
//WHATEVER
}скомпилируется и будет работать правильно™.
Помимо явного указания sizeof(smth)/sizeof(smth[0]) также используют макрос:
// in a header far, far away...
#define errorProneCountOfElements( arr ) (sizeof(arr)/sizeof((arr)[0]))
for( size_t index = 0; index < errorProneCountOfElements (arr); index++ ) {
//WHATEVER
}Посмотрим, как новое предупреждение работает в перечисленных случаях. Пробовать будем на gcc.godbolt.org
Сначала выберем в качестве компилятора gcc 4.9.2 – с параметрами по умолчанию предупреждений о неверном вычислении размера не будет ни в одном из примеров. Потом поменяем на gcc 5.1.0 – в примерах с циклом получаем на строку с заголовком цикла
warning: 'sizeof' on array function parameter 'arr' will return size of 'char*' [-Wsizeof-array-argument]
В таком коде:
void somethingExplicitCount( char arr[] )
{
for( size_t index = 0; index < sizeof(arr)/sizeof(arr[0]); index++ ) {
//WHATEVER
}
}выдается то же предупреждение. Аналогично и в коде с макросом:
void somethingMacroCount( char arr[9000] )
{
for( size_t index = 0; index < errorProneCountOfElements(arr); index++ ) {
//WHATEVER, PROFIT
}
}Код с перезаписью тоже дает предупреждение (используем переносимый memset() только для демонстрации):
void somethingMemset( char key[9000] )
{
//WHATEVER, PROFIT
memset(key, 0, sizeof(key)); // don't use memset for sensitive data
}ПРИБЫЛЬ.
Отдельного внимания заслуживает тот факт, что clang версии 3.0 уже умел выдавать то же самое предупреждение. Об этом в блоге LLVM было некогда сказано, что это специфичное для clang предупреждение и gcc так не умеет™. NO MOAR.
Предупреждение включено по умолчанию, разработчикам останется только правильно исправить проблемный код.
Дмитрий Мещеряков,
департамент продуктов для разработчиков
Комментарии (35)

grossws
27.07.2015 13:43+3
а не первые 4/8 байт по размеру указателя?void something( char encryptionKey[9000] ) { // WHATEVER, PROFIT // this call is broken SecureZeroMemory( encryptionKey, sizeof(encryptionKey)); // erase they key }
здесь разработчики хотели перезаписать нулями какие-то данные, но из-за ошибки будет перезаписан только первый байт
grafmishurov
27.07.2015 15:25По размеру слова на таргете. А ошибка с передачей размера не отдельным аргументом — действительно детская ошибка, не знал что в С++ она распространена, думал, что они со своим STL уже и забыли про массивы.

khim
27.07.2015 16:21+1Очень короткая память нужна, чтобы за какие-то три-четыре года со своим STL успеть забыть классику, однако.

mapron
27.07.2015 17:17почему 3-4 года? это array, а вектору уже лет 20 =) а массив char — это std::string в большинстве случаев.

khim
27.07.2015 18:21+2Вектору действительно много-много лет, но это ни разу не замена старому доброму массиву. Написать что-нибудь типа
вы в С++98 не сможете. Это если даже забыть об эффективности. В C++11 зато можно написатьstd::vector<int> a = { 1, 2 };
не создавая отдельной константы, что иногда удобнее, чем вариант с обычными массивами. Строки тоже дёргают конструкторы и размещают данные «в куче», что далеко не всегда хорошо. Посмотрите в код какого-нибудь Chrome и вы обнаружите, что там все строковые константы — это по прежнемуfoo(std::array<int, 3>({1, 2, 3}));const char *и никак иначе!operator""s— это даже не C++11, а С++14!
В общем — да, потихоньку «сырые» массивы из C++ изживаются, но до полной замены ещё ой как далеко.

mapron
27.07.2015 17:20+1Вроде как с 1993 года в STL есть vector, map и прочее. Я вектор уже в BCB 6 и MSVC 6 активно юзал, работало =)

khim
27.07.2015 18:04+2Ну как вам сказать, чтобы не обидеть… люди, которых не волнует тот факт, что массив из пары целых чисел занимает байт этак 40 и размещает данные «в куче» (то есть, к примеру, не может быть размещён как статический глобальный объект) склонны использовать python, и ruby, а не C++.

burjui
28.07.2015 00:16+2Мне кажется, вы перегибаете палку, делая поспешные выводы о квалификации и/или личных предпочтениях оппонента. Ваш гипотетический массив из пары чисел (даже пары сотен) можно передать и вектором (ссылкой) с данными в куче, если функция вызывается редко, и у вас не жёсткий real-time. Профессионал должен быть в состоянии отличить микроскопический overhead от подлинного транжирства (вызова той же самой функции в цикле с N>1000 итераций, например).
Общее правило: когда пишешь код, используй мозг, а когда код уже написан — подключай профайлер.
khim
28.07.2015 01:17+2Мне кажется, вы перегибаете палку, делая поспешные выводы о квалификации и/или личных предпочтениях оппонента.
Нет — я делаю выводы всего лишь о его неумении использовать адекватные инструменты.
Ваш гипотетический массив из пары чисел (даже пары сотен) можно передать и вектором (ссылкой) с данными в куче, если функция вызывается редко, и у вас не жёсткий real-time.
Вы издеваетесь или зачем? Вот это вот: "пары чисел (даже пары сотен)" — это как? Это где? Это почему? Конечно массив в пару сотен элементов можно передать с данными в куче, на таким масштабах бедствие уже не смертельно. А вот как раз массив в два-три элемента так передавать не стоит, так как тогда у вас вспомогательные действия будут занимать раз в 10 больше времени, чем основная операция.
Профессионал должен быть в состоянии отличить микроскопический overhead от подлинного транжирства (вызова той же самой функции в цикле с N>1000 итераций, например).
Давайте отделим мухи от котлет, а? Вызов функции, в которую передаётся массив в цикле с N>1000 итераций — это нормальный, вполне себе грамотный подход. Если функция объявлена какinlineили испольщуется LTO — то нормальный компилятор (на сегодня — фактически любой современный компилятор кроме MSVC) все ваши «лишние телодвижения» схлопнет и время работы будет таким же, как если бы функции там и не было. А вот если у вас в этом месте будет создаваться вектор — тоды ой, избавляться от лишних аллокаций ни один из известных мне оптимизаторов не умеет.
Общее правило: когда пишешь код, используй мозг, а когда код уже написан — подключай профайлер.
Ну если ваша цель — программа, которая жрёт гигабайты и требует восьмиядерного процессора для того, чтобы справиться с задачей, над которой до того работал какой-нибудь PentiumMMX с 64MiB памяти — то да, этот подход имеет смысл. Но… Тут C#/Java точно сгодится, часто Python и Ruby тоже подойдут. Зачем вам в этом случае C++? C++ — это очень тяжёлый, сложный язык, работать с ним сложно. И единственное оправдание многим решениям, которые усложняют работу с ним — это его эффективность. Если же от неё отказываетесь, то зачем вообще весь огород городить?

NeoCode
27.07.2015 14:57+1Да, древние хвосты вылезают и будут вылезать вечно… очевидная ошибка при проектировании языка — эквивалентность имени объекта массива и указателя на первый элемент массива (имя объекта структуры ведь не эквивалентно указателю на первый элемент… да это и невозможно).

fshp
27.07.2015 15:59что в коде на C++ рекомендуется использовать следующий фокус (отсюда) с шаблонной функцией:
Что только не придумают, лишь бы не использовать boost (boost::size).
NeoCode
27.07.2015 16:11Я занимаюсь проектированием собственного языка программирования, и могу сказать, что в языке кроме sizeof() должен быть оператор cardof() — для определения количества элементов любой агрегатной сущности (не только массива, но и структуры и т.п.). В С++ тоже не мешало бы такое ввести, только увы — не введут… так и будут использовать всякие хаки на шаблонах, которые в одном месте компилируются а в другом — нет.

khim
27.07.2015 16:25+1А не расскажите в какое место в С++ можно будет засунуть
cardof()для структур? Интересно же.
P.S. Или вы предлагаете ввести полный reflection? Тут да, тут возможны разговоры. Но одинcardofя не понимаю куда пристроить от слова совсем…
Bas1l
27.07.2015 16:23-2<да начнется холивар> А я вот с некоторых пор стараюсь избегать буст, потому я хочу, чтоб код был самодостаточным (компилировался сразу после загрузки из системы контроля версий)—то есть буст надо класть в код—но даже минимальные плюшки из него, типа boost::array (хотя он уже не актуален), обычно тащат за собой мегабайты кода. Впрочем, я не уверен, сколько тащит boost::size.

dbanet
27.07.2015 16:31+3Зачем класть в репу буст?
Достаточно декларировать зависимость и использовать нормальную систему сборки (i.e. CMake).
phprus
28.07.2015 18:44> Зачем класть в репу буст?
Иногда приходится носить буст с собой, например, из-за патчей, а не использовать готовые пакеты из репозиториев операционной системы.
А CMake вполне может отслеживать зависимости от кастомного буста и, при необходимости, его (пере)компилировать.

fshp
27.07.2015 17:01+2Какие мегабайты кода? Вот меня всегда удивлял такой подход.
1) Размер бинарика при использовании std::array не отличается от использования boost::array. Я не в курсе, сколько процентов кода было скопипащено, но они эквивалентны.
2) >90% boost`а — это header-only библиотеки. Они не требуют никакой линковки и дополнительных зависимостей.
3) boost модульный. Что бы использовать a, вам не нужно b.
Bas1l
28.07.2015 16:271. Я не про бинарник, а про исходный код.
2, 3. Я знаю. Но если с помощью boost bcp вытащить только то, что нужно, к примеру, для boost array, то получается 2.9 Мб исходного кода (проект начинался еще до C++11).

fshp
27.07.2015 17:03+1компилировался сразу после загрузки из системы контроля версий
А компилятор вы в репозиторий кладёте?

dyadyaSerezha
27.07.2015 18:53+3Ошибка хоть и детская, но совсем не очевидная, потому что:
1. Код
char arr[100]; std::cout << sizeof(arr)/sizeof(arr[0]) << std::endl;
напишет: 100
2. А вот код:
void sample(char a[100]) { std::cout << sizeof(a)/sizeof(a[0]) << std::endl; }
напишет: 4 или 8.
Хотя вроде бы обе декларации массива одинаковые. То есть, в одном случае правило эквивалентности массива и первого элемента не работает, а во втором работает. Что, конечно же, тяжелое наследие старого С.
Кстати, на днях увидел такую вот хрень:
Компилирование кода:
int sample(int& i) { i = 0; }
заканчивается с ошибкой компиляции (Оракловский C++ компилятор 2006 года на Солярисе) типа «ошибка, функция не возвращает значение.»
А вот код:
bool sample(int& i) { if (i) return false; }
компилируется вообще без ошибок и даже без предупреждений, хотя ошибка та же. Согласно стандарту, невозврат значения во всех ветках функции — undefined behavior. Но в одном случае это ошибка компиляции, а в другом — чистая компиляция, которая ведет к возрату почти всегда неправильного значения.
khim
27.07.2015 21:32+1Согласно стандарту, невозврат значения во всех ветках функции — undefined behavior.
Undefined behaviour — это вообще не о том. Вы имеете право иметь и функцию, которая иногда не возвращает значение и функцию, которая никогда его не возвращает в программе — и это будет валидная программа на C++.
Компилирование кода:
Ну, то, что компилятор по неизвестной причине бузит — это дело десятое. Строго говоря никто не запрещает подобную функцию вам в программе иметь. Использовать её будет нельзя — это да. Warning'ом всё должно бы ограничиться.
int sample(int& i) { i = 0; }
заканчивается с ошибкой компиляции (Оракловский C++ компилятор 2006 года на Солярисе) типа «ошибка, функция не возвращает значение.»
bool sample(int& i) { if (i) return false; }
Как это? Тут совсем другая история. Тут функцию вполне себе можно использовать без того, чтобы вызвать undefined behavior. Более того, если вспомнить правила игры, то можно понять, что это — очень даже полезная функция! Её вызовом (если она
компилируется вообще без ошибок и даже без предупреждений, хотя ошибка та же.inlineи описана в заголовке) вы говорите, что переменнаяi— никогда не может быть равна нулю. Компилятор может это с пользой использовать (хотя найти компилятор, который бы это делал мне не удалось).
P.S. Warning, кстати, и gcc и clang выдают для обоих функций.
mayorovp
27.07.2015 21:49+1Я бы кидал предупреждение вообще на любую попытку указать длину массива в объявлении функции (если массив передается не по ссылке, конечно же). Первопричина всех подобных ошибок — это именно недопонимание конструкций вида
void something( char arr[100] ).
Orient
28.07.2015 06:44+1Контроль типов для конструкций вида
не обязывает в точности контролировать тип передаваемого значения параметра функции с точностью до количества элементов. Если нужно точноvoid f(char x[100])
, то следует использовать именно ссылкуchar x[100];
.void g(char (& y)[100])
Orient
28.07.2015 07:05Что-то некорректно написано?
mayorovp
28.07.2015 08:48-3Не понятна цель комментария.

khim
28.07.2015 16:06+2Почему непонятна? Она описывает способ описания точно такой же функции, но такой, которая будет вести себя разумно: sizeof возвращает то, что должен вернуть, всякие ARRAY_SIZE тоже работают, передать туда массив «не того размера» случайно нельзя (причём это нельзя сделать даже случайно если константа-размер-массива-изменится и вы забудете библиотеку перекомпилировать) и т.д. и т.п.
Очень удобная и полезная конструкция с несколькими недостатками: во-первых она несовместима с C, во-вторых выглядит она как-то… гхм… неаппетитно.
Самая большая проблема со всеми этими контрукциями — то, что всё это взрывает систему типов так, что хочется кому-то морду набить:
Не самое приятное место в C++, согласитесь.typedef struct { int a, b, c; } Str; typedef int Arr[100]; void FooStr(Str& s); // Структура передаётся по ссылке; все довольны. void BarStr(Str s); // Структура передаётся по значению; тоже все понятно. void FooArr(Arr& a); // Массив передаётся по ссылке; всё нормально. void BarArr(Arr a); // Массив передаётся... передаётся массив... по сссылке? // Причём без контроля размера? // WTF? Нет! *W* *T* *F* ???mayorovp
28.07.2015 18:45Я писал, что компилятор должен кидать предупреждение как только увидит массив с длиной в заголовке, не дожидаясь пока программист применит к нему оператор sizeof. Вы же теперь объясняете, что контроль типов ничего не обязывает. То есть вы считаете, что компилятор не должен кидать предупреждения? Если да — то у вас странные аргументы. Если же вы со мной согласны — то я не понимаю зачем вы спорите.

Krypt
28.07.2015 15:42Почему тогда работает этот код?:
static DDLogFlag LocalToDDLogLevelMapping[] = { DDLogFlagVerbose, DDLogFlagInfo, DDLogFlagWarning, DDLogFlagError };
if ((logLevel >= (sizeof(InternalToDDLogLevelMapping) / sizeof(DDLogLevel))))
Собственно это Objective-C, компилируется с помощью clang, но он обратно совместим с C by definition, и если я верно понял — это особенность именно языка, а не реализации. Хотелось бы решить для себя вопрос области применимости такого подхода.
DmitryMe Автор
28.07.2015 15:48Здесь sizeof() применяется к переменной, объявленной не как параметр функции, в этом случае sizeof() возвращает размер массива.

Krypt
28.07.2015 16:05Да, понял. Я довольно давно использую этот подход, не то, чтобы очень часто, но случается. Обычно для полей, объявленных как static с какими-то пресетами данных, как в этом примере.
Насколько я помогу судить, дело в потере информации о типе. В каких ещё ситуациях это может произойти?
khim
28.07.2015 16:31Таких случаев, собственно, два: передача массива как параметра функции и описание внешнего массива. Если вы описываете что-либо типа
то вы, тем самым, говорите компилятору: у меня тут массив, но какого он размера — я не знаю, знает только тот, кто его описал. Часто употребляется со строковыми константами.extern int a[];
Но опасен только случай с передачей параметров. Если попытаться сделать что-нибудь «нехорошее» с глобальной переменной, тип которой не до конца известен (скажемsizeofвызвать), то компилятор возмутится и компилировать программу откажется. А вот с параметрами функции всё наоборот — всё отлично скомпилируется, только работать не будет.
JIghtuse
Отлично! Полезное предупреждение. Спасибо за статью. Хоть и пользуюсь gcc-5.1 несколько недель, о таком предупреждении не знал. Нужно читать ChangeLog.