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.