Написание пользовательских [s] printf с использованием шаблона с переменным числом символов в C ++ для Arduino - PullRequest
2 голосов
/ 04 октября 2019

Как правило, я хотел бы реализовать класс SerialLog, содержащий коллекцию методов, которые форматируют строки с использованием API-стиля printf и выводят скомпилированное сообщение на последовательную консоль Arduino с помощью метода Serial.print(), содержащегося вбазовая arduino библиотека.

Такой код позволил бы намного более чистые вызовы журнала последовательной консоли в моем коде (ниже показано, как показаны необходимые вызовы вложенных функций базовой библиотеки Arduino c ++, тогда как последние два вызова показываютAPI, который я хотел бы реализовать):

# normal debug logging for Arduino using the provided functions
Serial.log(sprintf("A log message format string: %d/%f", 123, 456.78));

# the call I would like to use for string formatting
String message = SerialLog.sprintf("A log message format string: %d/%f", 123, 456.78);

# the call I would like to use to output a formatted string
SerialLog.printf("A log message format string: %d/%f", 123, 456.78);

Как видно из приведенных выше примеров, я намерен создать набор методов класса для последовательного вывода на консоль с аргументами, отражающими нормальный C ++printf function .

Я пытался реализовать такой printf -стиль API, используя простые вариационные определения, такие как myVariadicPrintfFunction(const char * format, ...), но такое определение функции, похоже, требует, чтобы все аргументыимеют тип const char *. Это не то поведение, которое я хочу. Таким образом, моя текущая реализация использует шаблоны для включения аргументов любого типа (хотя, очевидно, тип должен быть в конечном счете приемлемым для функции ядра printf C ++).

Моя реализация включает в себя следующие открытые методы в SerialLog класс:

  • SerialLog::sprint (String sprint(const char * format)): принимает аргумент const char *. Возвращает строку в виде объекта Arduino String.

  • SerialLog::sprintf (template <typename ...Args> String sprintf(const char * format, Args ...args)): принимает аргумент const char * в качестве строки формата и любойколичество дополнительных аргументов (различных типов), которые будут подставлены в строку формата. Возвращает строку как объект Arduino String.

  • SerialLog::print (SerialLog& print(const char * format)): То же, что и SerialLog::sprint для вывода строки в последовательный портконсоль использует Serial.print() вместо простого ее возврата.

  • SerialLog::printf (template <typename ...Args> SerialLog& printf(const char * format, Args ...args)): использует возвращаемое значение SerialLog::sprintf для вывода строкик последовательной консоли, используя Serial.print() вместо простого ее возврата.

Как и в случае нормальной функции C ++ printf , оба SerialLog::sprintf и SerialLog::printfдолжен принять строку формата в качестве первого аргумента, за которым следует любое количество допустимых аргументов любого приемлемого типа , которые используются в качестве значений замещения для предоставленной строки формата.

Например, формат"This %s contains %d substituted %s such as this float: %d." с дополнительными аргументами string (как char *), 4 (как int), "values" (как char *) и 123.45 (как * 1085)*) приведет к следующей скомпилированной строке: "This string contains 4 substituted values such as this float: 123.45.".

Мне не удалось выполнить описанное поведение, используя следующий код:

debug.h

#include <stdio.h>
#include <Arduino.h>

namespace Debug
{
    class SerialLog
    {
        public:

            String sprint(const char * format);

            template <typename ...Args>
            String sprintf(const char * format, Args ...args);

            SerialLog& print(const char * format);

            template <typename ...Args>
            SerialLog& printf(const char * format, Args ...args);

    } /* END class SerialLog */

} /* END namespace Debug */

debug.cpp

#include <debug.h>

namespace Debug
{
    String SerialLog::sprint(const char * format)
    {
        return String(format);
    }

    template <typename ...Args>
    String SerialLog::sprintf(const char * format, Args ...args)
    {
        char buffer[256];

        snprintf(buffer, 256, format, args...);

        return String(buffer);
    }

    SerialLog& SerialLog::print(const char * format)
    {
        Serial.print(format);

        return *this;
    }

    template <typename ...Args>
    SerialLog& SerialLog::printf(const char * format, Args ...args)
    {
        Serial.print(this->sprintf(format, args...));

        return *this;
    }

} /* END namespace Debug */

В это время возникают следующие ошибкиво время компиляции:

C:\Temp\ccz35B6U.ltrans0.ltrans.o: In function `setup':
c:\arduino-app/src/main.cpp:18: undefined reference to `String RT::Debug::SerialLog::sprintf<char const*>(char const*, char const*)'
c:\arduino-app/src/main.cpp:22: undefined reference to `RT::Debug::SerialLog& RT::Debug::SerialLog::printf<char const*>(char const*, char const*)'        
c:\arduino-app/src/main.cpp:26: undefined reference to `RT::Debug::SerialLog& RT::Debug::SerialLog::printf<char const*>(char const*, char const*)'
c:\arduino-app/src/main.cpp:29: undefined reference to `RT::Debug::SerialLog& RT::Debug::SerialLog::printf<char const*>(char const*, char const*)'        
c:\arduino-app/src/main.cpp:30: undefined reference to `RT::Debug::SerialLog& RT::Debug::SerialLog::printf<char const*, int, double>(char const*, char const*, int, double)'
collect2.exe: error: ld returned 1 exit status
*** [.pio\build\debug\firmware.elf] Error 1

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

Полный журнал сборки VSCode (с использованием расширения PlatformIO) можно найти в виде Gist по адресу gist.github.com / robfrawley / 7ccbdeffa064ee522a18512b77d7f6f9 . Кроме того, на всю кодовую базу проекта можно ссылаться по адресу github.com / src-run / raspetub-arduino-app , а соответствующие проекты для этого вопроса расположены по адресу lib / Debug / Debug.h и lib / Debug / Debug.cpp .

Наконец, хотя я владею многими другими языками, такими как Python, PHP, Ruby и другими, , это первый проект C ++ ! Я изучаю язык C ++ с помощью реализации этого приложения и знаю, что в базе кода существует много неоптимальных вариантов;различные аспекты этого приложения будут исправлены и улучшены по мере развития моих знаний C ++. Таким образом, Меня не особенно интересуют комментарии относительно недостатков в моей реализации или подробные мнения, объясняющие недостатки в моем понимании C ++ . Пожалуйста, продолжайте обсуждение с единственным вопросом, изложенным выше.

Спасибо, что нашли время, чтобы прочитать весь этот вопрос, и я очень признателен за любую предоставленную помощь!

1 Ответ

1 голос
/ 04 октября 2019

Не уверен (без полного примера это сложно), но я полагаю, проблема в том, что вы объявили только методы шаблона внутри debug.h и определили их внутри debug.cpp.

Общие положенияпредложение: в C ++ когда-либо объявляйте и определяющие вещи шаблона (классы, функции, методы, переменные) внутри заголовочных файлов

Дело в том, что в этом случае компилятор реализует определенный шаблонметод, когда это необходимо. Поэтому, если вы пишете в main.cpp

char const * bar = "bar"; 

RT::Debug::SerialLog::printf("foo format: %s %i %lf", bar, 0, 1.1);

, компилятор знает, что ему нужен RT::Debug::SerialLog::printf<char const*, int, double>, но не может его реализовать, потому что в main.cpp смотрите только содержимое файла debug.h, гдеШаблонный метод SerialLog::printf() объявлен , но не определен . Таким образом, компилятор не может реализовать char const *, int, double версию метода.

Я предлагаю изменить файлы следующим образом

--- debug.h

#include <stdio.h>
#include <Arduino.h>

namespace Debug
{
    class SerialLog
    {
        public:

            String sprint(const char * format);

            template <typename ...Args>
            String sprintf(const char * format, Args ...args)
            {
                char buffer[256];

                snprintf(buffer, 256, format, args...);

                return String(buffer);
            }

            SerialLog& print(const char * format);

            template <typename ...Args>
            SerialLog& printf(const char * format, Args ...args)
            {
                Serial.print(this->sprintf(format, args...));

                return *this;
            }

    } /* END class SerialLog */

} /* END namespace Debug */

--- debug.cpp

#include <debug.h>

namespace Debug
{
    String SerialLog::sprint(const char * format)
    {
        return String(format);
    }

    SerialLog& SerialLog::print(const char * format)
    {
        Serial.print(format);

        return *this;
    }

} /* END namespace Debug */

---- конечные файлы

Таким образом, если вы пишете в main.cpp

RT::Debug::SerialLog::printf("foo format: %s %i %lf", bar, 0, 1.1);

, компилятор знаеткоторый нуждается в RT::Debug::SerialLog::printf<char const*, int, double> и может реализовать его, потому что из debug.h можно увидеть определение из SerialLog::printf().

...