После прочтения статьи Самая опасная функция в мире С/С++ я счёл полезным углубиться во зло, таящееся в тёмном погребе memset, и написать дополнение, чтобы шире раскрыть суть проблемы.

В языке Си повсеместно используется memset(), таящий в себе множество ловушек. Выдержка из C++ Reference:
void * memset ( void * ptr, int value, size_t num );
Fill block of memory
Sets the first num bytes of the block of memory pointed by ptr to the specified value (interpreted as an unsigned char).
Parameters
ptr — Pointer to the block of memory to fill.
value — Value to be set. The value is passed as an int, but the function fills the block of memory using the unsigned char conversion of this value.
num — Number of bytes to be set to the value. size_t is an unsigned integral type.
Return Value
ptr is returned.

Как уже неоднократно подмечено, есть множество граблей, на которые наступают даже опытные разработчики. Из описанного в статье Andrey2008 краткое обобщение типичных ошибок:

№1. Пытаясь вычислить размер массива, либо структуры, не используйте sizeof() для указателей на массив/структуру, он вернёт вам размер указателя 4 или 8 байт, вместо размера массива/структуры.

№2. Третий аргумент memset() принимает на вход количество байт, а не количество элементов, не учитывая тип данных. Добавлю ещё, например, тип int может занимать как 4, так и 8 байт, в зависимости от архитектуры. На этот случай следует использовать sizeof(int).

№3. Не путайте местами аргументы. Правильная последовательность это указатель, значение, длина в байтах.

№4. Не используйте memset при работе с объектами класса.

Но это лишь вершина айсберга.

Альтернатива memset


memset — это низкоуровневая функция, обязывающая разработчика принимать во внимание все особенности архитектуры компьютера и её использование должно быть обосновано. Давайте для начала рассмотрим альтернативу = {0}, вместо memset, говорят это позволяет инициализировать массив или строку на этапе компиляции, что должно повышать быстродействие программы, в отличии от memset (также ZeroMemory), инициализирующих данные во время исполнения. Я решил это проверить.

void doInitialize()
{
   char p0[25] = {0} ;           // установит все 25 символов в 0
   char p1[25] = "" ;            // установит все 25 символов в 0

   wchar_t p2[25] = {0} ;        // установит 25 символов в 0
   wchar_t p3[25] = L"" ;        // установит все 25 символов в 0

   short        p4[62] = {0}     // установит 62 значения в 0
   int          p5[37] = {-1} ;  // установит значение первого элемента в -1
   unsigned int p6[10] = {89} ;  // установит значение первого элемента 89
}

C99 [$6.7.8/21]
If there are fewer initializers in a brace-enclosed list than there are elements or members of an aggregate, or fewer characters in a string literal used to initialize an array of known size than there are elements in the array, the remainder of the aggregate shall be initialized implicitly the same as objects that have static storage duration.

Заодно такая инициализация снимает проблемы №1, №2, №3 с путаницей параметров и размеров буфера. То есть второй и третий аргумент местами мы не перепутаем, размер передавать не надо. Давайте же посмотрим как такой код преобразуют компиляторы. Все компиляторы сразу проверить я не могу, под рукой оказались gcc входящий в android-ndk-r10c, а также gcc в убунту 14.04.

gcc -v
1) gcc version 4.9 20140827 (prerelease) (GCC)
2) gcc version 4.8.2 (Ubuntu 4.8.2-19ubuntu1)

Давайте посмотрим как ведёт себя компилятор на таком куске кода:

void empty_string(){
    int i;
    char p1[25] = {0};
    printf("\np1: ");
    for (i = 0; i < 25; i++)
        printf("%x,",p1[i]);
}

Итак, без оптимизации (-O0) инициализация массива компилируется в такой ассемблерный код (просматриваем бинарники с помощью objdump):

gcc -O0, ELF 32-bit, ARM, EABI5
    83d8:       e3a03000        mov     r3, #0
    83dc:       e50b3024        str     r3, [fp, #-36]  ; 0x24
    83e0:       e24b3020        sub     r3, fp, #32
    83e4:       e3a02000        mov     r2, #0
    83e8:       e5832000        str     r2, [r3]
    83ec:       e2833004        add     r3, r3, #4
    83f0:       e3a02000        mov     r2, #0
    83f4:       e5832000        str     r2, [r3]
    83f8:       e2833004        add     r3, r3, #4
    83fc:       e3a02000        mov     r2, #0
    8400:       e5832000        str     r2, [r3]
    8404:       e2833004        add     r3, r3, #4
    8408:       e3a02000        mov     r2, #0
    840c:       e5832000        str     r2, [r3]
    8410:       e2833004        add     r3, r3, #4
    8414:       e3a02000        mov     r2, #0
    8418:       e5832000        str     r2, [r3]
    841c:       e2833004        add     r3, r3, #4
    8420:       e3a02000        mov     r2, #0
    8424:       e5c32000        strb    r2, [r3]
    8428:       e2833001        add     r3, r3, #1


gcc -O0, ELF 64-bit, x86-64
  400700:       48 c7 45 d0 00 00 00 00    movq   $0x0,-0x30(%rbp)
  400708:       48 c7 45 d8 00 00 00 00    movq   $0x0,-0x28(%rbp)
  400710:       48 c7 45 e0 00 00 00 00    movq   $0x0,-0x20(%rbp)
  400718:       c6 45 e8 00                movb   $0x0,-0x18(%rbp)


Как и ожидалось, без оптимизации мы получаем run-time код, который будет кушать O(n) процессорного времени (где n длина буфера). Что же сделает компилятор с оптимизацией (-O3) можем видеть ниже.

gcc -O3, 32-bit, ARM

000083ac <empty_string>:
    83ac:       e59f002c        ldr     r0, [pc, #44]   ; 83e0 <empty_string+0x34>
    83b0:       e92d4038        push    {r3, r4, r5, lr}
    83b4:       e08f0000        add     r0, pc, r0
    83b8:       ebffffb2        bl      8288 <printf@plt>
    83bc:       e59f5020        ldr     r5, [pc, #32]   ; 83e4 <empty_string+0x38>
    83c0:       e3a04019        mov     r4, #25
    83c4:       e08f5005        add     r5, pc, r5
    83c8:       e1a00005        mov     r0, r5
    83cc:       e3a01000        mov     r1, #0
    83d0:       ebffffac        bl      8288 <printf@plt>
    83d4:       e2544001        subs    r4, r4, #1
    83d8:       1afffffa        bne     83c8 <empty_string+0x1c>
    83dc:       e8bd8038        pop     {r3, r4, r5, pc}
gcc -O3, 64-bit, x86-64
00000000004006d0 <empty_string>:
  4006d0:       53                      push   %rbx
  4006d1:       be a4 08 40 00          mov    $0x4008a4,%esi
  4006d6:       bf 01 00 00 00          mov    $0x1,%edi
  4006db:       31 c0                   xor    %eax,%eax
  4006dd:       bb 32 00 00 00          mov    $0x32,%ebx
  4006e2:       e8 d9 fd ff ff          callq  4004c0 <__printf_chk@plt>
  4006e7:       66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)
  4006ee:       00 00 
  4006f0:       31 d2                   xor    %edx,%edx
  4006f2:       31 c0                   xor    %eax,%eax
  4006f4:       be aa 08 40 00          mov    $0x4008aa,%esi
  4006f9:       bf 01 00 00 00          mov    $0x1,%edi
  4006fe:       e8 bd fd ff ff          callq  4004c0 <__printf_chk@plt>
  400703:       83 eb 01                sub    $0x1,%ebx
  400706:       75 e8                   jne    4006f0 <empty_string+0x20>
  400708:       5b                      pop    %rbx
  400709:       c3                      retq   


Видим, что кусок кода с обнулением в run-time просто пропал, мы получили обещанную производительность O(1), давайте разберёмся откуда же свои значения берёт printf? Нас интересует вот этот кусочек:

83bc:           ldr     r5, [pc, #32]
83c0:           mov     r4, #25     ;// В r4 записываем количество циклов for, это наш счётчик цикла
83c4:           add     r5, pc, r5  ;// В r5 записываем текст "%x," как константу, в памяти она хранится как 002c7825
83c8:           mov     r0, r5      ;// r5 неизменно передаётся в r0 на каждой итерации цикла, это первый параметр printf()
83cc:           mov     r1, #0      ;// записываем константу 0 (вместо фактического p1[i]) как второй параметр printf()
83d0:           bl      8288 <printf@plt>
83d4:           subs    r4, r4, #1  ;// Отнимаем единицу в счётчике цикла
83d8:           bne     83c8 <empty_string+0x1c>  ;// Если не дошли до 0, то переходим на начало цикла 83c8

То есть компилятор просто выкинул массив, а вместо его значений использует 0, как заложенную на этапе компиляции константу. Хорошо, но что же происходит если мы будем использовать memset? Давайте посмотрим несколько кусочков objdump-а, например, под ARM:

Без оптимизации -O0:

    83d8:       e24b3024        sub     r3, fp, #36     ; 0x24
    83dc:       e1a00003        mov     r0, r3
    83e0:       e3a01000        mov     r1, #0
    83e4:       e3a02019        mov     r2, #25
    83e8:       ebffffa3        bl      827c <memset@plt>

С оптимизацией -O3:

    83c0:       e58d3004        str     r3, [sp, #4]
    83c4:       e58d3008        str     r3, [sp, #8]
    83c8:       e58d300c        str     r3, [sp, #12]
    83cc:       e58d3010        str     r3, [sp, #16]
    83d0:       e58d3014        str     r3, [sp, #20]
    83d4:       e58d3018        str     r3, [sp, #24]
    83d8:       e5cd301c        strb    r3, [sp, #28]

x86-64
Без оптимизации -O0:
  400816:       ba 19 00 00 00          mov    $0x19,%edx
  40081b:       be 00 00 00 00          mov    $0x0,%esi
  400820:       48 89 c7                mov    %rax,%rdi
  400823:       e8 a8 fc ff ff          callq  4004d0 <memset@plt>

С оптимизацией -O3:
  4007f4:       48 c7 04 24 00 00 00 00    movq   $0x0,(%rsp)
  4007fc:       48 c7 44 24 08 00 00 00    movq   $0x0,0x8(%rsp)
  400805:       48 c7 44 24 10 00 00 00    movq   $0x0,0x10(%rsp)
  40080e:       c6 44 24 18 00             movb   $0x0,0x18(%rsp)


То есть оптимизация просто убирает вызов memset, вставляя его inline. При таких раскладах memset будет всегда работать за O(n) времени, а вот инициализация с помощью = {0} при оптимизации работает за константу, в нашем случае и вовсе не отнимая тактов процессора, нагло выбрасывая сам факт существования массива и подменяя все его элементы нулями. Но давайте посмотрим, всегда ли это так и что будет если мы запишем ненулевое значение после инициализации? Тестовая функция примет вот такой вид:

void empty_string(){
    int i;
    char p1[25] = {0};
    p1[0] = 65;
    printf("\np1: ");
    for (i = 0; i < 25; i++)
        printf("%x,",p1[i]);
}

После компиляции получаем уже знакомый блок кода:

    8404:       e3a02041        mov     r2, #65 ; 0x41
    8408:       e08f0000        add     r0, pc, r0
    840c:       e58d3004        str     r3, [sp, #4]
    8410:       e58d3008        str     r3, [sp, #8]
    8414:       e58d300c        str     r3, [sp, #12]
    8418:       e58d3010        str     r3, [sp, #16]
    841c:       e58d3014        str     r3, [sp, #20]
    8420:       e58d3018        str     r3, [sp, #24]
    8424:       e5cd301c        strb    r3, [sp, #28]
    8428:       e5cd2004        strb    r2, [sp, #4]

x86-64
  4006f8:       48 c7 04 24 00 00 00    movq   $0x0,(%rsp)
  4006ff:       00 
  400700:       48 c7 44 24 08 00 00    movq   $0x0,0x8(%rsp)
  400707:       00 00 
  400709:       48 c7 44 24 10 00 00    movq   $0x0,0x10(%rsp)
  400710:       00 00 
  400712:       c6 44 24 18 00          movb   $0x0,0x18(%rsp)
  400717:       c6 04 24 41             movb   $0x41,(%rsp)


И выглядит это так, как будто компилятор нам вставил оптимизированную версию memset. А давайте посмотрим что будет, если размер массива значительно вырастет? Скажем, не 25 байт, а 25 килобайт!

    83fc:       e24ddc61        sub     sp, sp, #24832  ; 0x6100
    8400:       e24dd0a8        sub     sp, sp, #168    ; 0xa8
    8404:       e3a01000        mov     r1, #0
    8408:       e59f2054        ldr     r2, [pc, #84]   ; 8464 <empty_string+0x6c>
    840c:       e1a0000d        mov     r0, sp
    8410:       ebffff99        bl      827c <memset@plt>

x86-64
  400720:       55                      push   %rbp
  400721:       ba a8 61 00 00          mov    $0x61a8,%edx
  400726:       31 f6                   xor    %esi,%esi
  400728:       53                      push   %rbx
  400729:       48 81 ec b8 61 00 00    sub    $0x61b8,%rsp
  400730:       48 89 e7                mov    %rsp,%rdi
  400733:       48 8d ac 24 a8 61 00    lea    0x61a8(%rsp),%rbp
  40073a:       00 
  40073b:       48 89 e3                mov    %rsp,%rbx
  40073e:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
  400745:       00 00 
  400747:       48 89 84 24 a8 61 00    mov    %rax,0x61a8(%rsp)
  40074e:       00 
  40074f:       31 c0                   xor    %eax,%eax
  400751:       e8 8a fd ff ff          callq  4004e0 <memset@plt>
  400756:       be 54 09 40 00          mov    $0x400954,%esi
  40075b:       bf 01 00 00 00          mov    $0x1,%edi
  400760:       31 c0                   xor    %eax,%eax
  400762:       c6 04 24 41             movb   $0x41,(%rsp)
  400766:       e8 a5 fd ff ff          callq  400510 <__printf_chk@plt>
  40076b:       0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
  400770:       0f be 13                movsbl (%rbx),%edx
  400773:       31 c0                   xor    %eax,%eax
  400775:       be 5a 09 40 00          mov    $0x40095a,%esi
  40077a:       bf 01 00 00 00          mov    $0x1,%edi
  40077f:       48 83 c3 01             add    $0x1,%rbx
  400783:       e8 88 fd ff ff          callq  400510 <__printf_chk@plt>
  400788:       48 39 eb                cmp    %rbp,%rbx
  40078b:       75 e3                   jne    400770 <empty1_string+0x50>
  40078d:       48 8b 84 24 a8 61 00    mov    0x61a8(%rsp),%rax
  400794:       00 
  400795:       64 48 33 04 25 28 00    xor    %fs:0x28,%rax
  40079c:       00 00 
  40079e:       75 0a                   jne    4007aa <empty1_string+0x8a>
  4007a0:       48 81 c4 b8 61 00 00    add    $0x61b8,%rsp
  4007a7:       5b                      pop    %rbx
  4007a8:       5d                      pop    %rbp
  4007a9:       c3                      retq


Надо же!

Строка = {0} переходит на сторону тьмы, memset ликует!

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

Инициализация строки


Также не будет лишним рассмотреть вариант инициализации массива = "". В языке Си используются нуль-терминированные строки, то есть первый символ с байтовым значением 0x00 означает конец строки. Поэтому для инициализации строки нет смысла обнулять все элементы, достаточно лишь обнулить первый. Вот некоторые способы инициализировать пустую строку:

void doInitializeCString()
{
   char p0[25] = {0} ;           // установит все символы в 0
   char p1[25] = "" ;            // установит все символы в 0
   char p2[25] ;
   p2[0] = 0 ;                   // установит первый символ в 0
   char p3[25] ;
   memset(p3, 0, sizeof(p3)) ;   // установит 25 символов в 0
   char p4[25] ;
   strcpy(p4, "") ;              // установит первый символ в 0
   char *p5 = (char *) calloc(25, sizeof(char)) ;  // установит все символы в 0
}

Самый надёжный способ, как будет работать инициализация через = "" снова разобрать objdump. Без оптимизации ничего особенного мы не увидим, там всё аналогично = {0}, рассмотрим сразу с опцией -O3. Итак компилируем под ARM:
вот такую функцию
void empty_string(){
    int i;
    char p1[25] = "";
    printf("\np1: ");
    for (i = 0; i < 25; i++)
        printf("%x,",p1[i]);
}


И, внезапно, получаем обнуление всех элементов массива.

    83c0:       e58d3004        str     r3, [sp, #4]
    83c4:       e58d3008        str     r3, [sp, #8]
    83c8:       e58d300c        str     r3, [sp, #12]
    83cc:       e58d3010        str     r3, [sp, #16]
    83d0:       e58d3014        str     r3, [sp, #20]
    83d4:       e58d3018        str     r3, [sp, #24]
    83d8:       e5cd301c        strb    r3, [sp, #28]

x86-64
  400768:       48 c7 04 24 00 00 00 00    movq   $0x0,(%rsp)
  400770:       48 c7 44 24 08 00 00 00    movq   $0x0,0x8(%rsp)
  400779:       48 c7 44 24 10 00 00 00    movq   $0x0,0x10(%rsp)
  400782:       c6 44 24 18 00          movb   $0x0,0x18(%rsp)


Та ну ладно! Зачем в нуль-терминированной строке обнулять все неиспользованные символы?! Достаточно же обнулить один единственный байт. Хм, а если там будет 25 тысяч байт, что оно сделает? А вот что:

    8474:       e24ddc61        sub     sp, sp, #24832  ; 0x6100
    8478:       e24dd0a8        sub     sp, sp, #168    ; 0xa8
    847c:       e3a0c000        mov     ip, #0
    8480:       e28d3f6a        add     r3, sp, #424    ; 0x1a8
    8484:       e1a0100c        mov     r1, ip
    8488:       e59f204c        ldr     r2, [pc, #76]   ; 84dc <empty_string+0x6c>
    848c:       e28d0004        add     r0, sp, #4
    8490:       e503c1a8        str     ip, [r3, #-424] ; 0x1a8
    8494:       ebffff78        bl      827c <memset@plt>

x86-64
00000000004007b0 <empty_string>:
  4007b0:       55                      push   %rbp
  4007b1:       ba a0 61 00 00          mov    $0x61a0,%edx
  4007b6:       31 f6                   xor    %esi,%esi
  4007b8:       53                      push   %rbx
  4007b9:       48 81 ec b8 61 00 00    sub    $0x61b8,%rsp
  4007c0:       48 8d 7c 24 08          lea    0x8(%rsp),%rdi
  4007c5:       48 8d ac 24 a8 61 00    lea    0x61a8(%rsp),%rbp
  4007cc:       00 
  4007cd:       48 c7 04 24 00 00 00    movq   $0x0,(%rsp)
  4007d4:       00 
  4007d5:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
  4007dc:       00 00 
  4007de:       48 89 84 24 a8 61 00    mov    %rax,0x61a8(%rsp)
  4007e5:       00 
  4007e6:       31 c0                   xor    %eax,%eax
  4007e8:       48 89 e3                mov    %rsp,%rbx
  4007eb:       e8 f0 fc ff ff          callq  4004e0 <memset@plt>
  4007f0:       be 54 09 40 00          mov    $0x400954,%esi
  4007f5:       bf 01 00 00 00          mov    $0x1,%edi
  4007fa:       31 c0                   xor    %eax,%eax
  4007fc:       e8 0f fd ff ff          callq  400510 <__printf_chk@plt>
  400801:       0f 1f 80 00 00 00 00    nopl   0x0(%rax)
  400808:       0f be 13                movsbl (%rbx),%edx
  40080b:       31 c0                   xor    %eax,%eax
  40080d:       be 5a 09 40 00          mov    $0x40095a,%esi
  400812:       bf 01 00 00 00          mov    $0x1,%edi
  400817:       48 83 c3 01             add    $0x1,%rbx
  40081b:       e8 f0 fc ff ff          callq  400510 <__printf_chk@plt>
  400820:       48 39 eb                cmp    %rbp,%rbx
  400823:       75 e3                   jne    400808 <empty_string+0x58>
  400825:       48 8b 84 24 a8 61 00    mov    0x61a8(%rsp),%rax
  40082c:       00 
  40082d:       64 48 33 04 25 28 00    xor    %fs:0x28,%rax
  400834:       00 00 
  400836:       75 0a                   jne    400842 <empty_string+0x92>
  400838:       48 81 c4 b8 61 00 00    add    $0x61b8,%rsp
  40083f:       5b                      pop    %rbx
  400840:       5d                      pop    %rbp
  400841:       c3                      retq


Похоже, тёмный memset преследует нас. Если вы всё ещё хотите сражаться против тьмы, то стоит упомянуть какие ещё ловушки вас поджидают.



memset может инициализировать числа неправильными значениями


Если вы хотите заполнить массив целых чисел ненулевыми значениями, ознакомьтесь с побайтовым заполнением данных.

void doInitializeToMistakenValues()
{
   char           pChar[25] ;
   unsigned char  pUChar[25] ;
   short          pShort[25] ;
   unsigned short pUShort[25] ;
   int            pInt[25] ;
   unsigned int   pUInt[25] ;

   // Значения 2-байтовых и 4-байтовых элементов будут отличны от единицы
   memset(pChar,   1,  sizeof(pChar)) ;   // 1
   memset(pUChar,  1,  sizeof(pUChar)) ;  // 1
   memset(pShort,  1,  sizeof(pShort)) ;  // 257
   memset(pUShort, 1,  sizeof(pUShort)) ; // 257
   memset(pInt,    1,  sizeof(pInt)) ;    // 16843009
   memset(pUInt,   1,  sizeof(pUInt)) ;   // 16843009

   // Значения unsigned массивов заполнится байтами 0xFF
   memset(pChar,   -1, sizeof(pChar)) ;   // -1
   memset(pUChar,  -1, sizeof(pUChar)) ;  // 255
   memset(pShort,  -1, sizeof(pShort)) ;  // -1
   memset(pUShort, -1, sizeof(pUShort)) ; // 65535
   memset(pInt,    -1, sizeof(pInt)) ;    // -1
   memset(pUInt,   -1, sizeof(pUInt)) ;   // 4294967295
}

Рассмотрим как это получается. Вот имеем скажем массив int, передаём вторым параметром единицу, что происходит?

А вот что:

0x01010101 — в шестнадцатеричной записи каждый байт будет заполнен единицей, а правильное значение
0x00000001 будет невозможно задать функцией memset. Но на самом деле это не баг, это фича.

Вот только незнание этих фич приводит к непредсказуемым ошибкам.

memset может установить невалидное значение


Если в элементы double установить байты -1, мы получим значение Not-A-Number (NaN), а в последствии последующих вычислений, каждая операция со значением NaN будет возращать NaN, таким образом нарушая всю цепочку вычислений.

Таким же образом устанавливать -1 в тип bool некорректно и он формально не будет ни true, ни false. Хотя в большинстве случаев он будет вести себя как true. В большинстве случаев…

И последнее, memset предназначен только для работы с простыми структурами данных. Никогда не используйте memset с управляемыми структурами данных, эта функция предназначена только для низкоуровневых операций.


В статье использованы материалы memset is evil.

Также читайте про уязвимости функции printf
.

Комментарии (23)


  1. SHVV
    07.12.2015 18:04
    +1

    Таким же образом устанавливать -1 в тип bool некорректно и он формально не будет ни true, ни false. Хотя в большинстве случаев он будет вести себя как true. В большинстве случаев…

    Ммм… А подробнее можно? С числами то всё понятно. А вот bool до «недавнего» времени вообще не существовал и инициализировать то что использовалось вместо него можно было вообще чем угодно.


    1. a_batyr
      07.12.2015 18:46
      +1

      Например, если неверно задать количество заполняемых байт

      #include <stdbool.h>
      void wrong_bool_memset(){
          bool b;
          memset(&b, -1, 2);
          printf("%d\n", (int)b);
          printf("%d\n", (int)!b);
          if(!b)printf("Checked\n");
      }
      Вывод:
      255
      254
      Checked


      1. SHVV
        07.12.2015 19:46

        Интересно, какие подводные камни могут быть, если заполнение было полным, то есть, если memset(&b, -1, sizeof(bool));


        1. khim
          08.12.2015 04:11
          +2

          Да каким угодно.

          $ cat test.cc
          int foo(bool x) {
            if (x)
              return 0;
            else
              return 42;
          }
          
          int bar(bool x) {
            return (!!x) * 2;
          }
          

          $ gcc -O3 -S test.cc -o-
          	.file	"test.cc"
          	.text
          	.p2align 4,,15
          	.globl	_Z3foob
          	.type	_Z3foob, @function
          _Z3foob:
          .LFB0:
          	.cfi_startproc
          	cmpb	$1, %dil
          	sbbl	%eax, %eax
          	andl	$42, %eax
          	ret
          	.cfi_endproc
          .LFE0:
          	.size	_Z3foob, .-_Z3foob
          	.p2align 4,,15
          	.globl	_Z3barb
          	.type	_Z3barb, @function
          _Z3barb:
          .LFB1:
          	.cfi_startproc
          	movzbl	%dil, %eax
          	addl	%eax, %eax
          	ret
          	.cfi_endproc
          .LFE1:
          	.size	_Z3barb, .-_Z3barb
          	.ident	"GCC: (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4"
          	.section	.note.GNU-stack,"",@progbits
          

          В первом случае 1 — это true, всё остальное false, во втором вместо 0 и 2 получаем половину «таблицы менделеева». Вам достаточно?

          Бороться с «прокисшим» (sour) boolом — эта та ещё задачка. Некоторые аж в ассемблер залазят, чтобы точно всё сработало.


          1. pwl
            08.12.2015 08:46

            По моему мнению, если уж и вылезет баг втором случае, то вина того, кто использовал неявное приведения bool к int, ничуть не меньше, чем на том, кто его заполнил странным значением.

            И кроме того, следуя логики статьи, не является ли ваш пример основанием для того, чтобы отказаться от использования bool? (ну, чтоб в ассемблер не лазить лишний раз)


            1. khim
              08.12.2015 11:08

              вина того, кто использовал неявное приведения bool к int, ничуть не меньше, чем на том, кто его заполнил странным значением.
              С чего вдруг? Integral conversion вполне однозначно говорят: bool преобразованный к intу даёт гарантированно либо 0, либо 1.

              И кроме того, следуя логики статьи, не является ли ваш пример основанием для того, чтобы отказаться от использования bool?
              Нет. Тут нужно просто запомнить, что bool кажется типом похожим на int или char, но на самом деле нужно относиться к нему уважительно — так, как вы относитесь к объектам. Если объект с виртуальной функций обнулить memsetом — тоже ведь беда будет.


    1. khim
      08.12.2015 04:13

      А вот bool до «недавнего» времени вообще не существовал и инициализировать то что использовалось вместо него можно было вообще чем угодно.
      Вообще-то _Bool появился в C99 15 лет назад (а в C++ присутствовал с самого начала).


      1. SHVV
        08.12.2015 07:43
        +2

        У нас довольно старый проект и в коде можно найти 5 разных булов, которые ещё и могут быть не равны друг другу.


  1. dbagaev
    07.12.2015 18:20
    +13

    В мире С++ есть еще одна альтернатива memset, а именно типобезопасные алгоритмы std::fill() и std::fill_n(). Интересно было бы еще и их рассмотреть.


    1. fareloz
      08.12.2015 12:31
      +2

      Была уже статья на хабре, правда немного другого формата:
      Кто быстрее: memset, bzero или std::fill


  1. pwl
    08.12.2015 03:19
    +3

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

    Не путайте местами аргументы.

    Я вот не пойму, это стеб или серьезно?
    Следующей по опасности, видимо, будет операция деления: Если путать местами делимое и делитель — такая ерунда получается. Но хуже того, даже если ничего не напутать, частное помноженное на делитель не всегда равно делимому!

    memset может установить невалидное значение

    Если в элементы double установить байты -1, мы получим значение Not-A-Number (NaN)

    И с каких пор у нас NaN невалидное значение? Создатели стандарта старались, выделяли специальноые значения для него, придумывали signaling и quiet NaN, как именно они должны себя вести в разных операциях, а тут, бах, невалидно и все тут!
    Я бы еще понял еслиб речь шла о денормализованных значениях…

    И это еще почти никто (@a_batyr как-то неуверенно это сделал) не упомянул какой кошмар начнется если memset-ом начинать заполнять случайные области на стеке!


    1. khim
      08.12.2015 04:23
      +5

      Я вот не пойму, это стеб или серьезно?
      Это абсолютно серьёзно. Количество проектов, в которых перепутаны аргументы memset'а просто-таки не поддаётся перечислению.

      Следующей по опасности, видимо, будет операция деления: Если путать местами делимое и делитель — такая ерунда получается.
      Именно в этом и проблема: если вы перепутаете местами делимое и делитель, то ваша программа нормально работать не будет. А если перепутать аргументы memset'а местами — то будет. До поры, до времени. В 90% случаев последний аргумент memsetа — нуль, если его передать вторым, то уже неважно что будет в третьем, просто обнуления не произойдёт. Сразу после запуска программы, когда вы получаете от системы «чистенькую» память — всё работает, а если немножко поработать — начинает глючить по странному. И воспроизводимости никакой нет. Очень неприятная ошибка.

      И это еще почти никто (@a_batyr как-то неуверенно это сделал) не упомянул какой кошмар начнется если memset-ом начинать заполнять случайные области на стеке!
      Действительно. А дело всё в том, что тут речь идёт не о каких-то «фантазиях на тему», а про наиболее неприятные ошибки, к которым приводит memset. Если вы начинаете заполнять «случайные области на стеке», то ваша программа долго не проживёт, вы эту ошибку заметите и исправите. Всё. Неприятно, но не смертельно. А вот ошибки, которые могут сидеть в коде годами и проходить при этом разные тесты — это действительно неприятно. Структура memset'а такова, что это происходит довольно-таки часто: либо ничего не обнуляется, либо обнуляется слишком мало — заметить это сложно на практике (когда обнуляется слишком много — это как раз обычно сразу замечают).


      1. 0xd34df00d
        14.12.2015 22:20

        Именно в этом и проблема: если вы перепутаете местами делимое и делитель, то ваша программа нормально работать не будет.

        Или будет, на программистский взгляд.

        Я так в формуле для некоторого параметра лазерного резонатора делил на длину резонатора, а не умножал. Только когда к физикам пришёл с получившимися коэффициентами регрессии для модели резонатора, они сказали, что фигня какая-то, формулы стоит проверить.


  1. pwl
    08.12.2015 04:33
    +5

    В 90% случаев последний аргумент memsetа — нуль

    Ну что-ж… Если их путают даже при обсуждении их путанья, тогда да, верю что проблема существует! :)

    Хотя в коде такого ни разу в жизни не видел. Вот разнообразных ошибок с sizeof-ами — сколко угодно…

    (пардон промахнулся. это ответ на сообщение khim )


    1. khim
      08.12.2015 04:38

      Зависит от специфика проекта. Ошибки с sizeof'ами — могут быть незамечены и при работе со стеком, и с динамической памятью, перепутанный же порядок при работе со стеком обычно замечают (там остаётся какой-нибудь мусор, который себя быстро проявляет), а при работе с динамической памятью — нет (вначале вся память заполнена нулями, так что пока программа немножко не поработает неправильный вызов не приводит к проблемам).


    1. Andrey2008
      08.12.2015 12:21
      -1

      На тему перепутанного 2 и 3 аргумента: www.viva64.com/en/examples/V575


  1. middle
    08.12.2015 09:09
    +1

    Вы в серьёз рассуждаете о том, что переменную на стеке можно заполнить нулями «во время компиляции»? 8-O Или это «такая шутка белых, в которую черные не врубаются»?


    1. a_batyr
      08.12.2015 11:25
      +2

      заполнить нулями «во время компиляции»?
      Как видите, если массив заполнен только нулями и используется только для чтения, то выходит как бы да.
      такая шутка белых, в которую черные не врубаются
      Проверить и подёргать всё же любопытно.
      в серьёз
      Рыдаю.


  1. vladon
    08.12.2015 12:57

    Вывод один: не используйте memset в программах на C++.


    1. Wyrd
      10.12.2015 00:00

      Мне кажется можно сделать еще более глубокий вывод: не используйте функции С при программирование на С++. Это РАЗНЫЕ языки, с разными подходами.


      1. mbait
        12.12.2015 10:08
        +1

        Ах, если бы… На деле в этом невероятно сложно объяснить. Оба языка требуют большой дисциплины и глубоких знаний, но работать приходится с разными людьми. Например, недавно на одном из совещаний выяснилось, что коллега не знает, что в Си есть указатели на функции. При этом он перешел к нам из Амазона, писал там много на С++. Не могу же я выйти и сказать команде «вы знаете, его бы я вообще не подпускал к коду». Да и не стал бы я так делать. А в результате получаются статьи о том, какой злой memset.


        1. a_batyr
          14.12.2015 12:56

          Он не злой, он просто на стороне тьмы. Лично мне достаточно печенюшек, чтобы оставаться на стороне тьмы :)


  1. mbait
    12.12.2015 10:00
    +1

    В 99% проектов, с которыми я работал, никто не знал про оператор sizeof <expression>, и все писали sizeof(type). А правильно будет: type* ptr = malloc(sizeof *ptr). Если привыкнуть опускать скобки, то компилятор не будет давать описаться и написать sizeof type. К сожалению, защиты от malloc(sizeof ptr), когда нужно было написать malloc(sizeof *ptr), я не знаю.