Модификаторите const и restrict

Нека разгледаме конкретна задача. Да се напише функция, която приема като аргументи масив от низове a, число pos и низ str. На позиция pos в a трябва да се изкопира str. Ето една директна реализация на тази функция:

void put_at_pos (char** a, int pos, char* str) {
    int i;
    for (i=0; str[i] != '\0'; i++)
        a[pos][i] = str[i];
    a[pos][i] = '\0';
}
        

Желаем компилаторът да оптимизира автоматично тази функция по следния начин:

void put_at_pos (char** a, int pos, char* str) {
    char* p = a[pos];
    char* q = str;
    for (; *q != '\0'; p++, q++)
        *p = *q;
    *p = '\0';
}
        

За съжаление компилаторът няма да успее да оптимизира този код автоматично.

Най-напред да видим защо за нас втората дефиниция на put_at_pos е еквивалентна на първата. На всяка итерация на цикъла, указателите p и q се увеличават с единица. Това означава, че след i итерации те ще имат следните стойности:

p е равен на a[pos] + i

q е равен на str + i

Следователно

*p е равно на *(a[pos] + i), т.е. на a[pos][i]

*q е равно на *(str + i), т.е. на str[i].

Компилаторите са в състояние да направят тези разсъждения. Какъв тогава е проблемът? Ами това, че от никъде не става ясно, че масивът a не се засича с някой от низовете, които той съдържа. Или по-точно от никъде не следва, че за някое i указателят a[pos]+i няма да се окаже равен на a+pos. Ако това наистина се случи, когато i има тази стойност a[pos] ще се промени и значи няма да бъде константа.

За да се реши този проблем, трябва да покажем на компилаторът, че няма никаква връзка между указателите a+pos и a[pos]. Вече знаем един начин, който при конкретния случай ще свърши работа — да използваме локална променлива:

void put_at_pos (char** a, int pos, char* str) {
    int i;
    char* b = a[pos];
    for (i=0; str[i] != '\0'; i++)
        b[i] = str[i];
    a[pos][i] = '\0';
}
        

Едва ли обаче подобно използване на локална променлива е уместно. По-неопитните програмисти изобщо не биха не биха разбрали смисъла от това, а по-опитните ще решат, че с това целим изразът a[pos] да се пресметне еднократно, а не при всяка итерация на цикъла. Съвсем малко ще са онези, които ще разберат истинското ни намерение.

В заглавието на този раздел се говори за const и restrict. Именно тези модификатори ни дават правилния начин за справяне с проблема. Благодарение на тях може да не се интересуваме какви са възможностите на съвременните компилатори да оптимизират програмите и какво им пречи да правят това. Просто трябва да мислим кога е уместно да използваме тези модификатори.

И const, и restrict се използват съгласно еднакви синтактични правила. Ще започнем с модификатора const.

Основното предназначение на модификатора const съвсем не е това, да се помага на компилатора да оптимизира. Един от резултатите при използването му обаче е именно този. Няма да обясняваме тук това подробно, само ще кажем, че е добре да използваме този модификатор винаги, когато може.

Ето някои примери за използване на const:

char const * p дефинира указател, който не е константа. Символите, към които той сочи обаче, не могат да се променят.

char * const p дефинира указател, който е неизменяем (поради това разбира се трябва да му бъде посочена стойност). Символите, към които сочи обаче не са неизменяеми.

char const * const p дефинира неизменяем указател към неизменяеми символи.

Виждаме, че модификаторът const винаги се отнася към „нещото“ пред него. По изключение ако const стои на първа позиция, той се отнася за „нещото“ след него. Така например const char * p е еквивалентно на char const * p.

Ето и един по-сложен пример. Нека p е указател към указател q, а q е указател към сивмол x. Тогава:

след char const **p ще бъде неизменяем x;

след char * const *p ще бъде неизменяем q;

след char ** const p ще бъде неизменяем p.

От време на време стандартът ISO C на езика Си претърпява ревизии. Една такава ревизия имаше през 1999 г. и в резултат на нея в езикът се появи нова ключова дума — модификаторът restrict. Тъй като стандартът Си-99 е сравнително скорошен, далеч не всички компилатори го поддържат, но в близките 2–3 години по-важните компилатори трябва напълно да поддържат този стандарт[9].

Да предположим, че става въпрос за функция със следния прототип:

void g (int *a; int *b)
        

При използване на Си-99 можем да запишем този прототип по следния начин:

void g (int * restrict a; int * restrict b)
        

Така показваме, че указателите a и b нямат нищо общо по между си.

Както вече казахме, ключовата дума restrict следва същите синтактични правила при използване, както и const. Тя обаче се отнася винаги за указател. Използваме restrict при декларирането на даден указател p, ако този указател няма нищо общо с останалите указатели в програмата. Тук под „нищо общо“ разбираме следното: за кой да е друг указател q, който е видим във функцията, стойността на p е различна от стойността на q, а разликата p-q никога не е била пресмятана.

Вече можем да завършим обсъждането на функцията put_at_pos. Тя трябва да бъде дефинирана по следния начин:

void put_at_pos (char** restrict a, int pos, char* str) {
    int i;
    for (i=0; str[i] != '\0'; i++)
        a[pos][i] = str[i];
    a[pos][i] = '\0';
}
        

Забележете, че указателят str не е обявен като restrict, защото той може и да има връзка с някой от низовете в масива a.

Поздсказка

С помощта на следните предпроцесорни дефиниции можем използваме restrict, без да губим възможността да компилираме програмата и с по-стари компилатори, които все още не поддържат restrict:

#if !defined __STDC_VERSION__ || __STDC_VERSION__ < 199901L
#ifdef __GNUC__
#define restrict __restrict__ /* за ГНУ-компилатора */
#else
#define restrict /* това изтрива restrict */
#endif
#endif
          

Преди години, когато не всички компилатори поддържаха const, аналогични предпроцесорни инструкции се използваха и за този модификатор.

Забележка

Засега ключовата дума restrict е част от стандарта на Си, но не и от стандарта а Си++. ГНУ-компилаторът на Си++ обаче поддържа алтернативна ключова дума __restrict__.



[9] ГНУ-компилаторът поддържа частично този стандарт, ако му подадем опцията -std=c99.