Правильность популярного выражения для функции копирования строк в языке Си { while (*dest++ = *src++) }

Начал читать учебник "Программирование: Введение в профессию" Том №2 "Системы и сети" автора А.В. Столярова, в котором рассматривается язык программирования Си. В теме про указатели и строковые массивы (стр.96) есть небольшой пример реализации процедуры копирования из одной строки в другую. Собственно так выглядит код (на стр.94 есть еще один пример):

    void stringCopy(char* dest, const char* src)
    {
        while ((*dest++ = *src++));    
    }

Автор настаивает, что в коде есть ошибка. Причем сам пример часто мелькает в интернете в качестве "суперкороткого решения", и автор относится к этому отрицательно. Сам же автор конкретной ошибки не указывает, однако просит разобраться (для себя), лишь намекая на побочный эффект, который легко не заметить. Вот что автор пишет. введите сюда описание изображения Вот так выглядит правильный пример кода автора учебника:

     while (*src)
     {
         *dest = *src;
         dest++;
         src++;
     }
     *dest = '\0'

Собственно я искал хоть какое-то упоминание о ошибке в этом коде, но никто даже не интересовался, кроме как принципом работы кода. В общем если разобраться в приоритетах операций - все становится на свои места (я так сначала думал с разгону):

1.Оказывается что постфиксный инкремент/декремент имеют отличную от префиксных аналогов реализацию. Что точно уж объясняет работу ошибочного кода. Я проверил он работает 100%. https://ravesli.com/urok-40-inkrement-dekrement-pobochnye-effekty/

#include <stdlib.h>
void stringCopy(char* dest, const char* src)
{
    while ((*dest++ = *src++));// почему-то неправильная запись с ошибкой (хотя все работает)
    /*
    Правильная запись
    while (*src)
    {
        *dest = *src;
        dest++;
        src++;
    }
    *dest = '\0';
    */
}
int main()
{
    char* src = malloc(15);
    char* dest = malloc(15);
    *(src + 0) = '\0';
    *(src + 1) = 'e';
    *(src + 2) = 'a';
    *(src + 3) = 'r';
    *(src + 4) = '!';
//дальше не продолжал инициализацию - Visual Studio туда помещает 
//мусор без нулей
    
    stringCopy(dest, src);
//вот тут я поставил точку останова в дебаггере - мои указатели 
//ссылаются на нулевые строки (значит копирование успешно прошло при нулевой строке и значит что ошибка не в операторе постфиксного инкремента)
    free(src);
    free(dest);
}

2.Наконец вроде бы разобрался с понятием "леводопустимых" выражений (стр 87). Получается, что уникальность реализации постфиксных инкремента и декремента имеет прямую связь с этим понятием "леводопустимости"? Или это отдельно? Чтобы уже разобраться совсем совсем.

В итоге не понятно, есть ли там ошибка или нету. Получается что проблема в наглядности этого кода и лучше писать все красиво по полочкам, ибо никогда не знаешь что стукнет в голову компилятору или сложно анализировать код? А ошибка придумана - чтобы сломать мозг тому кто будет ее искать и привести его к этому выводу?

Upd: ошибки там нет. Ошибка была в примере на стр. 94 (по видимости автора ошибкой является то, что длина высчитывается с учетом знака окончания строки, хотя это даже не правило). Просто мне показалось из-за стиля его заметок в виде крестика, что там тоже должна быть ошибка. Просто автор пытался показать, что так делать не стоит. Компактность кода в этом случае приводит к сложности понимания происходящего.

Upd: продолжив дальнейшее изучение книги я наткнулся на стр.100-103 параграф "Точки следования (sequence points). У меня конечно нет столько опыта, хотелось бы услышать имеет ли место в данной ситуации то, о чем там говорится. Ну и так как никто не ответил о "леводопустимых выражениях" - может и о них кто-то скажет в данной ситуации.


Ответы (1 шт):

Автор решения: Stanislav Volodarskiy

Давайте отрефакторим:

void stringCopy(char* dest, const char* src)
{
    while ((*dest++ = *src++));    
}

a. Убираем лишние скобки ставим новые чтобы документировать порядок применения операторов:

void stringCopy(char* dest, const char* src)
{
    while (*(dest++) = *(src++));    
}

b. Разбираем цикл while:

void stringCopy(char* dest, const char* src)
{
    while (1) {    
        if (!(*(dest++) = *(src++)))
            break;
    }    
}

c. Разбираем условие в if:

void stringCopy(char* dest, const char* src)
{
    while (1) {
        char c = (*(dest++) = *(src++));    
        if (!c)
            break;
    }    
}

d. Выносим инкременты из присваивания:

void stringCopy(char* dest, const char* src)
{
    while (1) {
        char c = (*dest = *src);
        dest++;
        src++;    
        if (!c)
            break;
    }    
}

e. Заносим инкременты в if:

void stringCopy(char* dest, const char* src)
{
    while (1) {
        char c = (*dest = *src);
        if (!c) {
            dest++;
            src++;    
            break;
        } else {
            dest++;
            src++;    
        }
    }    
}

f. Инкременты перед break не меняют смысл программы. Убираем их:

void stringCopy(char* dest, const char* src)
{
    while (1) {
        char c = (*dest = *src);
        if (!c) {
            break;
        } else {
            dest++;
            src++;    
        }
    }    
}

g. Меняем условие в if:

void stringCopy(char* dest, const char* src)
{
    while (1) {
        *dest = *src;
        if (!(*src)) {
            break;
        } else {
            dest++;
            src++;    
        }
    }    
}

h. Заносим присваивание в if:

void stringCopy(char* dest, const char* src)
{
    while (1) {
        if (!(*src)) {
            *dest = *src;
            break;
        } else {
            *dest = *src;
            dest++;
            src++;    
        }
    }    
}

i. Уточняем условие в if и присваивание перед break:

void stringCopy(char* dest, const char* src)
{
    while (1) {
        if (*src == '\0') {
            *dest = '\0';
            break;
        } else {
            *dest = *src;
            dest++;
            src++;    
        }
    }    
}

j. Присваивание перед break выносим из цикла:

void stringCopy(char* dest, const char* src)
{
    while (1) {
        if (*src == '\0') {
            break;
        } else {
            *dest = *src;
            dest++;
            src++;    
        }
    }    
    *dest = '\0';
}

k. Убираем if:

void stringCopy(char* dest, const char* src)
{
    while (*src != '\0') {
        *dest = *src;
        dest++;
        src++;    
    }    
    *dest = '\0';
}

l. Меняем условие while:

void stringCopy(char* dest, const char* src)
{
    while (*src) {
        *dest = *src;
        dest++;
        src++;    
    }    
    *dest = '\0';
}

Получили правильный с точки зрения автора учебника вариант. Каждый шаг сохраняет семантику программы. Если вы сомневаетесь в каком-то шаге, комментируйте, разберёмся.

Автор может упомянуть что в конце работы оригинальной процедуры указатели src и dest указывают на элементы за концами строк. Язык C специально разрешает указателям указывать на элемент следующий за концом массива. Этот указатель нельзя разыменовывать, но этого и не происходит.

→ Ссылка