Изначально требовалось, чтобы следующий код не выдавал ошибок, а в итоге получили гораздо более мощный инструмент, чем предполагали.
const char *pStr = PSTR("Hello"); // В этом месте ошибка.
// error: statement-expressions are not allowed outside functions nor in template-argument lists
int main() {…}
- для кода,
- для оперативной памяти и регистров.
Компилятор GCC использует двухбайтовый указатель, что предоставляет доступ к первым 64К кодовой памяти (остальная может быть использована только для инструкций) или ко всей ОЗУ.
Но узнать по указателю в какой памяти располагается переменная нет возможности. Из-за этого в библиотеке avr-gcc появились отдельные функции для работы с кодовой памятью и строками, которые в ней расположенны. Они маркируются суффиксом “_P” в конце имени функции. Например strcpy_P – аналог функции strcpy, принимающий указатель на строку в кодовой памяти.
К сожалению, компилятор не может проверить где будет использоваться переменная и разместить ее там, а следовательно программисту требуется позаботиться об этом самому. Соответственно, появилась необходимость маркировки переменных, расположенных в кодовой памяти ключевым словом PROGMEM, чтобы явно указать что мы намерены работать с кодом.
Это, однако, не отменяет необходимости программисту следить за правильностью пользования переменных.
Больше всего неудобств нам доставляли строки. Строковые литералы являются объектами, расположенными в оперативной памяти, а значит занимают и оперативную память, и кодовую (надо же откуда-то брать значения для инициализации). Опять же они не подходят для работы с функциями, работающими с кодовой памятью. Например:
int main() {
char dest[20];
strcpy_P(dest, "Hello world!");
}
Этот код приведет к неопределенным последствиям, так как будет брать данные из кодовой памяти, расположенной по тому же адресу, что и строка “Hello world!” в оперативной памяти.
Для этих случаев в библиотеке avr был предусмотрен макрос PSTR(текст), возвращающий указатель на строку, расположенную в кодовой памяти.
int main() {
char dest[20];
strcpy_P(dest, PSTR("Hello world!"));
}
Теперь этот код работает и даже не занимает оперативную память. Но стоит вынести этот макрос за пределы какой-либо функции, он перестает работать.
const char *pStr = PSTR("Hello"); // В этом месте ошибка.
// error: statement-expressions are not allowed outside functions nor in template-argument lists
int main() {…}
Приходилось писать примерно такой код:
extern const char PROGMEM caption1[];
const char caption1[] = "Hello";
const char *pStr = caption1;
Это надуманный пример, но представим, что вместо pStr у нас инициализируется какая-то пользовательская структура, ожидающая указатель на строку.
В первую очередь это было необходимо для инициализации структуры меню. Вся инициализация должна была проводиться статически, на этапе компиляции.
Поэтому мы стали искать надежный способ для получения указателя на строку в кодовой памяти. В этом нам помогли шаблонные классы. Для шаблонного класса можно создать статическую переменную, располагающуюся в кодовой памяти и получить указатель на нее.
template <char value>
struct ProgmemChar {
static const char PROGMEM v;
};
template <char value>
const char ProgmemChar<value>::v = value;
const char *pChar = &(ProgmemChar<'a'>::v);
Но строку не передашь параметром в шаблон. Поэтому мы решили разбить строку на символы. Как мы разбиваем строку на символы я покажу дальше, а пока покажу простой пример строки в кодовой памяти:
template <char ch1, char ch2, char ch3, char ch4, char ch5>
struct ProgmemString {
static const char PROGMEM v[5];
};
template <char ch1, char ch2, char ch3, char ch4, char ch5>
const char ProgmemString<ch1, ch2, ch3, ch4, ch5>::v[5] = {ch1, ch2, ch3, ch4, ch5};
const char *pStr = ProgmemString<'a', 'b', 'c', 'd', 0>::v;
Данный пример работает для строк, имеющих размер ровно 4 символа и завершающий 0 в конце. Причем строка ProgmemString<'a', 0, 0, 0, 0> тоже будет занимать 5 байт.
Для решения этой проблемы мы использовали частичную специализацию шаблонного класса, добавив в шаблон еще размер строки. Вот базовый шаблонный класс:
template<size_t S, char... L>struct _Pstr;
Теперь вернемся к проблеме разбиения строки на символы. Если честно, то для нас это до сих пор проблема, так как мы не смогли придумать пока ничего лучше, чем написать макрос, который N раз возьмет i-ый (от 0 до N-1) символ из исходной строки.
#define SPLIT_TO_CHAR_4(STR) STR[0], STR[1], STR[2], STR[3]
Этот макрос разбивает строку, в которой должно быть не меньше четырех символов, на символы. В данном случае N = 4.
Если подглядеть на код после препроцессора, то мы бы увидели следующий код:
"Hello world!"[0], "Hello world!"[1], "Hello world!"[2], "Hello world!"[3]
Согласен, что это длинный текст, но мы с этим смирились. Тем более, что сам компилятор уже выдает только четыре символа.
Более важной проблемой было взятие символа с большим индексом. Для большого N (а мы хотим чтобы все наши строки были короче N), обязательно будет случай, когда мы захотим взять символ за пределами строки, что приведет к ошибке компиляции.
Первым рабочим вариантом был следующий способ:
- Добавляем к исходной строке строку, состоящую из символа '\0' и имеющую длину N символов. Добавление осуществлялось так: #define ADD_STR(STR) STR "\0\0\…\0".
- Проводим операцию SPLIT_TO_CHAR над получившейся строкой.
Этот способ работает, но гарантированно увеличивает код после препроцессора на N*N символов. В итоге мы быстро получаем предел компилятора.
К счастью с приходом с++11 и constexpr функций у нас получилось избавиться от лишних символов, используя класс селектор символов. Для краткости он называется _CS (Char Selector).
struct _CS {
template<size_t n>
constexpr _CS(const char (&s)[n]) :s(s), l(n){}
constexpr char operator [](size_t i){return i < l ?s[i] :0;}
const char *s = 0;
const size_t l = 0;
};
Код этого класса я давненько подсмотрел на Хабре, но не могу сейчас найти где именно (спасибо тебе автор).
Код макроса разделения на символы стал проще:
#define SPLIT_TO_CHAR(STR) _CS(STR)[0], _CS(STR)[1], …, _CS(STR)[N-1]
Теперь осталось собрать все вместе:
// Базовый шаблон строки
template<size_t S, char... L>struct _PStr;
// Вспомогательные макросы, раскрывающие последовательность пронумерованных элементов. В примере я ограничился 10 элементами
#define ARGS01(P, S) P##00 S
#define ARGS02(P, S) ARGS01(P, S),P##01 S
#define ARGS03(P, S) ARGS02(P, S),P##02 S
#define ARGS04(P, S) ARGS03(P, S),P##03 S
#define ARGS05(P, S) ARGS04(P, S),P##04 S
#define ARGS06(P, S) ARGS05(P, S),P##05 S
#define ARGS07(P, S) ARGS06(P, S),P##06 S
#define ARGS08(P, S) ARGS07(P, S),P##07 S
#define ARGS09(P, S) ARGS08(P, S),P##08 S
#define ARGS0A(P, S) ARGS09(P, S),P##09 S
// Специализации класса для определенной длины строки (от 0 до 10 символов). Строка гарантированно будет завершена 0.
template<char... L>struct _PStr<0x00, L...>{static const char PROGMEM v[];};
template<char... L>const char _PStr<0x00, L...>::v[] = {0};
template<ARGS01(char _,), char... L>struct _PStr<0x01, ARGS01(_,), L...>{static const char PROGMEM v[];};
template<ARGS01(char _,), char... L>const char _PStr<0x01, ARGS01(_,), L...>::v[] = {ARGS01(_,), 0};
template<ARGS02(char _,), char... L>struct _PStr<0x02, ARGS02(_,), L...>{static const char PROGMEM v[];};
template<ARGS02(char _,), char... L>const char _PStr<0x02, ARGS02(_,), L...>::v[] = {ARGS02(_,), 0};
template<ARGS03(char _,), char... L>struct _PStr<0x03, ARGS03(_,), L...>{static const char PROGMEM v[];};
template<ARGS03(char _,), char... L>const char _PStr<0x03, ARGS03(_,), L...>::v[] = {ARGS03(_,), 0};
template<ARGS04(char _,), char... L>struct _PStr<0x04, ARGS04(_,), L...>{static const char PROGMEM v[];};
template<ARGS04(char _,), char... L>const char _PStr<0x04, ARGS04(_,), L...>::v[] = {ARGS04(_,), 0};
template<ARGS05(char _,), char... L>struct _PStr<0x05, ARGS05(_,), L...>{static const char PROGMEM v[];};
template<ARGS05(char _,), char... L>const char _PStr<0x05, ARGS05(_,), L...>::v[] = {ARGS05(_,), 0};
template<ARGS06(char _,), char... L>struct _PStr<0x06, ARGS06(_,), L...>{static const char PROGMEM v[];};
template<ARGS06(char _,), char... L>const char _PStr<0x06, ARGS06(_,), L...>::v[] = {ARGS06(_,), 0};
template<ARGS07(char _,), char... L>struct _PStr<0x07, ARGS07(_,), L...>{static const char PROGMEM v[];};
template<ARGS07(char _,), char... L>const char _PStr<0x07, ARGS07(_,), L...>::v[] = {ARGS07(_,), 0};
template<ARGS08(char _,), char... L>struct _PStr<0x08, ARGS08(_,), L...>{static const char PROGMEM v[];};
template<ARGS08(char _,), char... L>const char _PStr<0x08, ARGS08(_,), L...>::v[] = {ARGS08(_,), 0};
template<ARGS09(char _,), char... L>struct _PStr<0x09, ARGS09(_,), L...>{static const char PROGMEM v[];};
template<ARGS09(char _,), char... L>const char _PStr<0x09, ARGS09(_,), L...>::v[] = {ARGS09(_,), 0};
template<ARGS0A(char _,), char... L>struct _PStr<0x0A, ARGS0A(_,), L...>{static const char PROGMEM v[];};
template<ARGS0A(char _,), char... L>const char _PStr<0x0A, ARGS0A(_,), L...>::v[] = {ARGS0A(_,), 0};
// Селектор символа
struct _CS {
template<size_t n>
constexpr _CS(const char (&s)[n]) :s(s), l(n){}
constexpr char operator [](size_t i){return i < l ?s[i] :0;}
const char *s = 0;
const size_t l = 0;
};
// Вспомогательный макрос для экранирования запятых
#define STR_UNION(...) __VA_ARGS__
// Главный макрос, возвращающий указатель на строку, расположенную в кодовой памяти. SPS = StaticProgramString.
#define SPS(T) STR_UNION(_PStr<_CS(T).l - 1, ARGS0A(_CS(T)[0x, ])>::v)
Разберем по элементам главный макрос:
- _Pstr<size_t S, char… L>::v – указатель на строку длиной S и содержащую символы L,
- _CS(T).l – 1 — размер исходной строки без нуля в конце,
- ARGS0A(_CS(T)[0x, ]) — макрос, забирающий первые 10 символов из исходной строки.
Для каждой строки будет выбрана своя специализация шаблона, подходящая по длине строки.
Подводя итоги я хотел бы сказать, что с помощью этого макроса нам удалось реализовать не только получение указателя на строку в коде, независимо от того где этот макрос применяется, но и еще два явных преимущества перед PSTR:
- Для каждой уникальной строки, созданной с помощью SPS будет создан только один экземпляр строки, ведь статическое поля шаблона создается только один раз для всего проекта. Конечно, современные компиляторы могут оптимизировать использование строк, но только в рамках компиляции одного файла cpp.
- Строка создается с глобально доступным именем, что необходимо для использования в качестве параметра шаблона.
template <class T, const char *name>
struct NamedType {
T value;
static const char *getName() {
return name;
}
};
NamedType<int, SPS("Параметр")> var1 = {3};
Эти шаблонные классы позволили нам собирать метаданные о переменных в проекте, что позволило нам упростить разработку на порядок, одновременно улучшив пользовательский интерфейс и гибкость настройки. Но это уже другая история.
Комментарии (8)
LampTester
06.10.2016 12:20+1Гм. Может быть я не проник в суть статьи или отстал от жизни, но я все-таки не совсем понял, чем не устроил вариант
extern const char PROGMEM caption1[]; const char caption1[] = "Hello"; const char *pStr = caption1;
Собственно, я всегда примерно так и делал. Более того, это рекомендуемый в документации на avr-libc способ решения подобных проблем. Собственно все еще проще:
const char foo[] PROGMEM = "Hello, world!"; ... strcpy_P(dest,foo);
и, собственно, все.ko1un
06.10.2016 12:32+1Согласен, этот способ работает, но требуется написать две строки вместо одной.
strcpy_P(dest,SPS("Hello world!"));
Плюс сама строка располагается непосредственно в месте ее использования. Ваш пример сложно будет читать, если эти две строки не будут помещаться в экран.
Плюс не требуется придумывать имена для переменных, хранящих строку, что является лишним трудом.
Короче, лень — двигатель прогресса.
Кстати, рекомендуемый в документации способ, не работал одно время. Компилятор ругался, что __attribute__((__progmem__)) не может быть использован при создании переменной, а только в объявлении. Это было давно, но именно оттуда у меня привычка писать отдельно объявление, а потом инициализацию.LampTester
06.10.2016 13:10Понял.
Плюс сама строка располагается непосредственно в месте ее использования.
В сущности, разделение объявления строки и места ее использования можно считать фичей. Такой подход позволяет вынести сообщения в отдельный файл, и при необходимости локализации править только его. С именами особой проблемы тоже нет:
const char init_error_message[] PROGMEM = "Initialization failed."; const char init_OK_message[] PROGMEM = "Hardware initialized."; ...
Но, конечно, тут на вкус и цвет.
x893
06.10.2016 12:49+1Взял библиотеку уже готовую — например adafruit fona.
Там повсеместно используется конструкция
имя_функции(F("какой-то текст"));
«какой-то текст» располагается во флэш.
Реализацию не сложно посмотреть.
Хотя может и не понял глубины мысли в этом посте.LynXzp
06.10.2016 13:42Тоже не понял глубину мысли. Все строки у меня PROGMEM или PSTR, т.е. в flash памяти кода. И все две функции работы со строками немного изменены и это «понимают». Ну нельзя просто так взять конкретный символ и что? pgm_read_byte( (char*)(&(str))+i ) — вот макрос для взятия символа. Ну разве что макросы — зло, а двухстраничные темплейты — добро.
ko1un
06.10.2016 21:00Макрос F() — это аналог PSTR.
Весь смысл в том, что эти макросы не работают вне функций. А нам нужно было инициализировать глобальные переменные в кодовой памяти указателями на строку.
static const char *pStr = PSTR("Hello"); // error: statement-expressions are not allowed outside functions nor in template-argument lists
Естественно, мы делали так же, как указал LampTester, но нам было лень постоянно так делать и мы нашли такой способ.
Eivind
Для GCC вы можете делать как-то так:
ko1un
С этим способом создания пользовательских литералов я не был знаком. Почитал вашу статью. Спасибо огромное.
Это, действительно, должно упростить раскрытие символов в макросе SPLIT_TO_CHAR.
Сейчас будем пробовать. Только компилятор обновим, а то наш еще не хочет такую строку кушать.