Хорошие новости для пользователей 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)


  1. JIghtuse
    27.07.2015 13:14
    +3

    Отлично! Полезное предупреждение. Спасибо за статью. Хоть и пользуюсь gcc-5.1 несколько недель, о таком предупреждении не знал. Нужно читать ChangeLog.


  1. grossws
    27.07.2015 13:43
    +3

    void something( char encryptionKey[9000] )
    {
       // WHATEVER, PROFIT
    
      // this call is broken
      SecureZeroMemory( encryptionKey, sizeof(encryptionKey)); // erase they key
    }

    здесь разработчики хотели перезаписать нулями какие-то данные, но из-за ошибки будет перезаписан только первый байт
    а не первые 4/8 байт по размеру указателя?


    1. DmitryMe Автор
      27.07.2015 14:09
      +2

      Да, вы правы. Спасибо.


    1. grafmishurov
      27.07.2015 15:25

      По размеру слова на таргете. А ошибка с передачей размера не отдельным аргументом — действительно детская ошибка, не знал что в С++ она распространена, думал, что они со своим STL уже и забыли про массивы.


      1. khim
        27.07.2015 16:21
        +1

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


        1. mapron
          27.07.2015 17:17

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


          1. khim
            27.07.2015 18:21
            +2

            Вектору действительно много-много лет, но это ни разу не замена старому доброму массиву. Написать что-нибудь типа

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

            В общем — да, потихоньку «сырые» массивы из C++ изживаются, но до полной замены ещё ой как далеко.


        1. mapron
          27.07.2015 17:20
          +1

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


          1. khim
            27.07.2015 18:04
            +2

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


            1. burjui
              28.07.2015 00:16
              +2

              Мне кажется, вы перегибаете палку, делая поспешные выводы о квалификации и/или личных предпочтениях оппонента. Ваш гипотетический массив из пары чисел (даже пары сотен) можно передать и вектором (ссылкой) с данными в куче, если функция вызывается редко, и у вас не жёсткий real-time. Профессионал должен быть в состоянии отличить микроскопический overhead от подлинного транжирства (вызова той же самой функции в цикле с N>1000 итераций, например).

              Общее правило: когда пишешь код, используй мозг, а когда код уже написан — подключай профайлер.


              1. 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++ — это очень тяжёлый, сложный язык, работать с ним сложно. И единственное оправдание многим решениям, которые усложняют работу с ним — это его эффективность. Если же от неё отказываетесь, то зачем вообще весь огород городить?


  1. NeoCode
    27.07.2015 14:57
    +1

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


  1. fshp
    27.07.2015 15:59

    что в коде на C++ рекомендуется использовать следующий фокус (отсюда) с шаблонной функцией:
    Что только не придумают, лишь бы не использовать boost (boost::size).


    1. NeoCode
      27.07.2015 16:11

      Я занимаюсь проектированием собственного языка программирования, и могу сказать, что в языке кроме sizeof() должен быть оператор cardof() — для определения количества элементов любой агрегатной сущности (не только массива, но и структуры и т.п.). В С++ тоже не мешало бы такое ввести, только увы — не введут… так и будут использовать всякие хаки на шаблонах, которые в одном месте компилируются а в другом — нет.


      1. khim
        27.07.2015 16:25
        +1

        А не расскажите в какое место в С++ можно будет засунуть cardof() для структур? Интересно же.

        P.S. Или вы предлагаете ввести полный reflection? Тут да, тут возможны разговоры. Но один cardof я не понимаю куда пристроить от слова совсем…


        1. JIghtuse
          27.07.2015 17:11
          +1

          Фраза о reflection напомнила об этом ролике. Он хоть и затасканный, но в этот раз слова удачно подобрали.

          Hitler on C++17


        1. NeoCode
          27.07.2015 17:16

          Естественно полная рефлексия:) Ну и в метапрограмминге пригодился бы.


    1. Bas1l
      27.07.2015 16:23
      -2

      <да начнется холивар> А я вот с некоторых пор стараюсь избегать буст, потому я хочу, чтоб код был самодостаточным (компилировался сразу после загрузки из системы контроля версий)—то есть буст надо класть в код—но даже минимальные плюшки из него, типа boost::array (хотя он уже не актуален), обычно тащат за собой мегабайты кода. Впрочем, я не уверен, сколько тащит boost::size.


      1. dbanet
        27.07.2015 16:31
        +3

        Зачем класть в репу буст?
        Достаточно декларировать зависимость и использовать нормальную систему сборки (i.e. CMake).


        1. phprus
          28.07.2015 18:44

          > Зачем класть в репу буст?
          Иногда приходится носить буст с собой, например, из-за патчей, а не использовать готовые пакеты из репозиториев операционной системы.

          А CMake вполне может отслеживать зависимости от кастомного буста и, при необходимости, его (пере)компилировать.


      1. fshp
        27.07.2015 17:01
        +2

        Какие мегабайты кода? Вот меня всегда удивлял такой подход.
        1) Размер бинарика при использовании std::array не отличается от использования boost::array. Я не в курсе, сколько процентов кода было скопипащено, но они эквивалентны.
        2) >90% boost`а — это header-only библиотеки. Они не требуют никакой линковки и дополнительных зависимостей.
        3) boost модульный. Что бы использовать a, вам не нужно b.


        1. Bas1l
          28.07.2015 16:27

          1. Я не про бинарник, а про исходный код.
          2, 3. Я знаю. Но если с помощью boost bcp вытащить только то, что нужно, к примеру, для boost array, то получается 2.9 Мб исходного кода (проект начинался еще до C++11).


      1. fshp
        27.07.2015 17:03
        +1

        компилировался сразу после загрузки из системы контроля версий

        А компилятор вы в репозиторий кладёте?


  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. Но в одном случае это ошибка компиляции, а в другом — чистая компиляция, которая ведет к возрату почти всегда неправильного значения.


    1. khim
      27.07.2015 21:32
      +1

      Согласно стандарту, невозврат значения во всех ветках функции — undefined behavior.
      Undefined behaviour — это вообще не о том. Вы имеете право иметь и функцию, которая иногда не возвращает значение и функцию, которая никогда его не возвращает в программе — и это будет валидная программа на C++.

      Компилирование кода:
      int sample(int& i) { i = 0; }
      заканчивается с ошибкой компиляции (Оракловский C++ компилятор 2006 года на Солярисе) типа «ошибка, функция не возвращает значение.»
      Ну, то, что компилятор по неизвестной причине бузит — это дело десятое. Строго говоря никто не запрещает подобную функцию вам в программе иметь. Использовать её будет нельзя — это да. Warning'ом всё должно бы ограничиться.

      bool sample(int& i) { if (i) return false; }
      компилируется вообще без ошибок и даже без предупреждений, хотя ошибка та же.
      Как это? Тут совсем другая история. Тут функцию вполне себе можно использовать без того, чтобы вызвать undefined behavior. Более того, если вспомнить правила игры, то можно понять, что это — очень даже полезная функция! Её вызовом (если она inline и описана в заголовке) вы говорите, что переменная i — никогда не может быть равна нулю. Компилятор может это с пользой использовать (хотя найти компилятор, который бы это делал мне не удалось).

      P.S. Warning, кстати, и gcc и clang выдают для обоих функций.


  1. mayorovp
    27.07.2015 21:49
    +1

    Я бы кидал предупреждение вообще на любую попытку указать длину массива в объявлении функции (если массив передается не по ссылке, конечно же). Первопричина всех подобных ошибок — это именно недопонимание конструкций вида void something( char arr[100] ).


    1. Orient
      28.07.2015 06:44
      +1

      Контроль типов для конструкций вида

      void f(char x[100])
      не обязывает в точности контролировать тип передаваемого значения параметра функции с точностью до количества элементов. Если нужно точно
      char x[100];
      , то следует использовать именно ссылку
      void g(char (& y)[100])
      .


      1. Orient
        28.07.2015 07:05

        Что-то некорректно написано?


        1. mayorovp
          28.07.2015 08:48
          -3

          Не понятна цель комментария.


          1. khim
            28.07.2015 16:06
            +2

            Почему непонятна? Она описывает способ описания точно такой же функции, но такой, которая будет вести себя разумно: sizeof возвращает то, что должен вернуть, всякие ARRAY_SIZE тоже работают, передать туда массив «не того размера» случайно нельзя (причём это нельзя сделать даже случайно если константа-размер-массива-изменится и вы забудете библиотеку перекомпилировать) и т.д. и т.п.

            Очень удобная и полезная конструкция с несколькими недостатками: во-первых она несовместима с 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* ???
            
            Не самое приятное место в C++, согласитесь.


            1. mayorovp
              28.07.2015 18:45

              Я писал, что компилятор должен кидать предупреждение как только увидит массив с длиной в заголовке, не дожидаясь пока программист применит к нему оператор sizeof. Вы же теперь объясняете, что контроль типов ничего не обязывает. То есть вы считаете, что компилятор не должен кидать предупреждения? Если да — то у вас странные аргументы. Если же вы со мной согласны — то я не понимаю зачем вы спорите.


  1. 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, и если я верно понял — это особенность именно языка, а не реализации. Хотелось бы решить для себя вопрос области применимости такого подхода.


    1. DmitryMe Автор
      28.07.2015 15:48

      Здесь sizeof() применяется к переменной, объявленной не как параметр функции, в этом случае sizeof() возвращает размер массива.


      1. Krypt
        28.07.2015 16:05

        Да, понял. Я довольно давно использую этот подход, не то, чтобы очень часто, но случается. Обычно для полей, объявленных как static с какими-то пресетами данных, как в этом примере.

        Насколько я помогу судить, дело в потере информации о типе. В каких ещё ситуациях это может произойти?


        1. khim
          28.07.2015 16:31

          Таких случаев, собственно, два: передача массива как параметра функции и описание внешнего массива. Если вы описываете что-либо типа

          extern int a[];
          то вы, тем самым, говорите компилятору: у меня тут массив, но какого он размера — я не знаю, знает только тот, кто его описал. Часто употребляется со строковыми константами.

          Но опасен только случай с передачей параметров. Если попытаться сделать что-нибудь «нехорошее» с глобальной переменной, тип которой не до конца известен (скажем sizeof вызвать), то компилятор возмутится и компилировать программу откажется. А вот с параметрами функции всё наоборот — всё отлично скомпилируется, только работать не будет.