Writing custom [s]printf using variadic template in C++ for Arduino - c++

Generally, I would like to implement a SerialLog class containing a collection of methods that format strings using a printf-style API and output the compiled message to the Arduino serial console using the Serial.print() method contained within the basic arduino library.
Such code would enable much cleaner serial console log invocations within my code (the below begins by showing the required nested function calls of the core Arduino c++ library, whereas the latter two calls show the API I would like to implement):
# 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);
As can be seen in the above examples, my intention is to create a set of class methods for serial console output with arguments that mirror the normal C++ printf function.
I've attempted to implement such a printf-style API using simple variadic definitions like myVariadicPrintfFunction(const char * format, ...) but such a function definition appears to require that all arguments are of type const char *. This is not the behavior I want. As such, my current implementation uses templates to enable arguments of any type (obviously the type must be ultimately acceptable to the C++ core printf function, though).
My implementation includes the following public methods within a SerialLog class:
SerialLog::sprint (String sprint(const char * format)): Accepts a const char * argument. Returns the string as the Arduino String object.
SerialLog::sprintf (template <typename ...Args> String sprintf(const char * format, Args ...args)): Accepts a const char * argument as the format string and any number of additional arguments (of various types) which will be substituted within the format string. Returns the string as the Arduino String object.
SerialLog::print (SerialLog& print(const char * format)): Same as SerialLog::sprint to output the string to the serial console using Serial.print() instead of simply returning it.
SerialLog::printf (template <typename ...Args> SerialLog& printf(const char * format, Args ...args)): Uses the return value of SerialLog::sprintf to output the string to the serial console using Serial.print() instead of simply returning it.
As with the normal C++ printf function, both SerialLog::sprintf and SerialLog::printf must accept a format string as the first argument followed by any number of acceptable argument of any acceptable type which are used as the substitution values for the provided format string.
For example, a format of "This %s contains %d substituted %s such as this float: %d." with additional arguments of string (as a char *), 4 (as an int), "values" (as a char *), and 123.45 (as a float) would result in the following compiled string: "This string contains 4 substituted values such as this float: 123.45.".
I have been unable to achieve the described behavior using the following code:
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 */
At this time, the follow errors occur during compilation:
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
Note: The above code is extracted from a larger Debug namespace and an expanded SerialLog class that contains additional methods, so the following error message line numbers will not correctly represent the example code shown.
The full VSCode build log (using the PlatformIO extension) can be located as a Gist at gist.github.com/robfrawley/7ccbdeffa064ee522a18512b77d7f6f9. Moreover, the entire project codebase can be referenced at github.com/src-run/raspetub-arduino-app, with the relevant projects for this question located at lib/Debug/Debug.h and lib/Debug/Debug.cpp.
Lastly, while I am proficient in many other languages like Python, PHP, Ruby, and others, this is the first C++ project! I am learning the C++ language through this application's implementation and am aware that many suboptimal choices exist within the codebase; different aspects of this application will be amended and improved as my knowledge of C++ evolves. As such, I am not particularly interested in comments regarding deficiencies in my implementation or verbose opinion pieces explaining the shortcomings in my understanding of C++. Please keep any discussion focused on the singular question outlined above.
Thanks for taking the time to read through this entire question and I greatly appreciate any assistance provided!

Not sure (without a full example it's difficult) but I suppose the problem is that you've declared only the template methods inside debug.h and you have defined them inside debug.cpp.
A general suggestion: in C++, ever declare and define template things (classes, functions, methods, variables) inside header files
The point is that, in this case, the compiler implement the specific template method when is needed. So if you write in main.cpp
char const * bar = "bar";
RT::Debug::SerialLog::printf("foo format: %s %i %lf", bar, 0, 1.1);
the compiler know that needs a RT::Debug::SerialLog::printf<char const*, int, double> but can't implement it because, in main.cpp, see only the content of debug.h, where the template method SerialLog::printf() is declared but not defined. So the compiler can't implement the char const *, int, double version of the method.
I suggest to change the files as follows
--- 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 */
---- end files
This way, if you write in main.cpp
RT::Debug::SerialLog::printf("foo format: %s %i %lf", bar, 0, 1.1);
the compiler know that needs a RT::Debug::SerialLog::printf<char const*, int, double> and can implement it because can see, from debug.h, the definition of SerialLog::printf().

Related

C++: safe usage of vsprintf

I have a largish code base that does log_msg("Here comes %s", "the text"), where log_message is a macro that adds function name and line numbers to the log message.
GCC/G++ warn about errors when the format string doesn't match the supplied arguments. Unfortunately sometimes the code calls log_msg(get_the_text()). The return value of get_the_text() is unknown at compile time, so if it contains some printf formatting sequences, the code will fall flat on its face.
What I'm looking for is a way to route the single argument usages through a different code path that doesn't interpret the formatting codes. I tried something like this hoping that the non-variadic case is more specific than the variadic one:
void log_the_message_implementation(const char *filename, const char *funcname, const char *msg);
void log_the_message_implementation(const char *filename, const char *funcname, const char *msg, ...);
I was hoping that the compiler would pick the single argument function when there are no variable args, but it complains about ambiguous calls.
Any ideas how to fix this without changing thousands of calls from
log_msg(get_the_text()) to log_msg("%s", get_the_text())?
Thanks to #SamVarshavchik this is what I came up with:
#include <iostream>
#include <cstdio>
#include <tuple>
template<typename ... Args>
void log(Args ... args) {
if (sizeof...(args) == 1) {
auto t = std::make_tuple(args...);
std::puts(std::get<0>(t));
} else {
std::printf(args...);
}
}
int
main() {
log("Test %d");
log("%s %d\n", "Test", 1);
log([]{ return "%s";}());
return 0;
}

How do I declare the main() entry point of a program without specifying all arguments in C++?

In C, I can do this to have an unspecified number of arguments in a function:
#include <elf.h>
#include <stddef.h>
#include <stdlib.h>
extern char **__environ;
int __libc_start_main
(
int (*main)(),
int argc,
char **argv
)
{
int ret;
Elf32_auxv_t *auxv;
size_t aux[38];
/* ... */
exit(main(argc, argv, __environ, aux));
}
However, when doing this in C++, the compiler emits this error:
test.c: In function ‘int __libc_start_main(int (*)(), int, char**)’:
test.c:21:45: error: too many arguments to function
exit(main(argc, argv, __environ, aux));
^
How do I do this in C++?
I understand that the C/C++ standards don't allow this, but I'm currently writing an implementation of the standard C library.
The short answer is: You don't.
In C++ all functions have a prototype; there is no such thing as an "unspecified number of arguments".
If you want to call main as main(argc, argv, __environ, aux), you need to declare it as int (*main)(int, char **, char **, void *) or similar.
Try either:
void foo(...);
or
template <typename ... ARGS> void foo(ARGS && ... args) { ... body }
First option is the same as void foo() (little known C language fact). First option requires some sort of additional argument (for example printf(char *, ...), where first argument allows function to detect, how to parse following arguments).
Second option requires you to commit to a function body somewhere in a header.

No matching function for call to 'EVP_DigestFinal' - Using openssl lib in C++

I have successfully integrated openssl dev library in main.cpp file. openssl/evp.h is currently included.
I can use EVP_DigestInit, EVP_DigestUpdate functions successfully without any error. But if I try to call EVP_DigestFinal, it gives following error:
No matching function for call to 'EVP_DigestFinal'
Whereas, the functions are defined in the same header file, evp.h. What should I do in order to use this function too? Thanks for help.
Related code:
EVP_MD_CTX ctx;
char ch[128];
int val = 128;
EVP_DigestFinal(&ctx, ch, val);
From what I find at OpenSSL.org, the signature is
int EVP_DigestFinal(EVP_MD_CTX *ctx, unsigned char *md, int *s);
To match that you need unsigned char for the second parameter and an address for the third parameter, something like
EVP_MD_CTX ctx;
unsigned char ch[128];
_______^
int val = 128;
EVP_DigestFinal(&ctx, ch, &val);
__________________________^

No idea how to use popt library

All:
Thanks for help.
I am new to C option parsing, for now, what I want is to use popt library to parsing the each argument and prnit them out.
#include <iostream>
#include <string>
#include <cstring>
#include <popt.h>
using namespace std;
int main(int argc, const char* argv[]){
char* dt1;
char* dt2;
struct poptOption {
const char * longName; /* may be NULL */
char shortName; /* may be ’\0’ */
int argInfo;
void * arg; /* depends on argInfo */
int val; /* 0 means don’t return, just update flag */
char * descrip; /* description for autohelp -- may be NULL */
char * argDescrip; /* argument description for autohelp */
};
struct poptOption optionsTable[]={
{"start",'s',POPT_ARG_STRING,dt1,'s',"The date format should like YYYY-MM-DD.",0},
{"end",'e',POPT_ARG_STRING,dt2,'e',"The date format should like YYYY-MM-DD.",0},
//~ POPT_AUTOHELP
//~ {NULL,0,0,NULL,0}
};
poptContext optCon;
optCon = poptGetContext (0, argc, argv, optionsTable, 0);
const char* portname = poptGetArg(optCon);
cout<<portname<<endl;
return 0;
}
When I compile it, I got error llike:
test.cpp: In function ‘int main(int, const char**)’
test.cpp:27: warning: deprecated conversion from string constant to ‘char*’
test.cpp:27: warning: deprecated conversion from string constant to ‘char*’
test.cpp:30: error: cannot convert ‘main(int, const char**)::poptOption*’ to ‘const poptOption*’ for argument ‘4’ to ‘poptContext_s* poptGetContext(const char*, int, const char**, const poptOption*, unsigned int)’
I don't think you should be defining the struct poptOption in your program. That struct should be defined for you in the popt include file. Try removing that struct definition.
Note, I think you also need to uncomment this line:
//~ {NULL,0,0,NULL,0}
The reason that this warning is being reported is a feature of the C language, but the mistake in the code is due to how you are attempting to use popt.
The types (char*) and (const char*) in C are different types. One is not really the other, and while C allows assignment from one type to another without blowing up, any decent C compiler will give you a warning. You can hide the warnings with a type cast, but it's generally a bad idea.
A C-style string is of the type (const char*), and you are assigning it to the field descrip in poptOption which is defined as a (char*). This raises a compiler warning because now, someone who reaches into that memory by following the reference from the optionsTable array could attempt to change the contents of the string. That's an odd thing to allow, considering the string is defined as a constant. That's why you get the warning.
The mistake in your code is that you are using popt incorrectly, with your own definition of the poptOption struct. If you look within the file that you include (popt.h) on line 55 you will see the poptOption struct, as it is defined by the popt authors. It is:
struct poptOption {
/*#observer#*/ /*#null#*/ const char * longName; /* may be NULL */
char shortName; /* may be '\0' */
int argInfo;
/*#shared#*/ /*#null#*/ void * arg; /* depends on argInfo */
int val; /* 0 means don't return, just update flag */
/*#shared#*/ /*#null#*/ const char * descrip; /* description for autohelp -- may be NULL */
/*#shared#*/ /*#null#*/ const char * argDescrip; /* argument description for autohelp */
};
or removing comments
struct poptOption {
const char * longName;
char shortName;
int argInfo;
void * arg;
int val;
const char * descrip;
const char * argDescrip;
};
and you clearly see that even the authors expected a (const char *), and not the (char *) you defined.

Google Test: "char-array initialized from wide string"

I have implemented type-parameterized tests (Sample #6) to apply the same test case to more than one class. It happens that when assigning a string to either a signed char[], unsigned char[], const signed char[] or const unsigned char[], I get:
../stackoverflow.cpp: In member function ‘void IosTest_DummyTest_Test<gtest_TypeParam_>::TestBody() [with gtest_TypeParam_ = std::basic_istream<char, std::char_traits<char> >]’:
../stackoverflow.cpp:34: instantiated from here
../stackoverflow.cpp:32: error: char-array initialized from wide string
What is more interesting is that when applying the test case to one type everything goes just fine, but when I add a second type it blows up. I could reproduce the error in the following code:
#include "gtest/gtest.h"
#include <iostream>
// Factory methods
template<class T> std::ios* CreateStream();
template<>
std::ios* CreateStream<std::istream>() {
return &std::cin;
}
template<>
std::ios* CreateStream<std::ostream>() {
return &std::cout;
}
// Fixture class
template<class T>
class IosTest: public ::testing::Test {
protected:
IosTest() : ios_(CreateStream<T>()) {}
virtual ~IosTest() {}
std::ios* const ios_;
};
using testing::Types;
typedef Types<std::istream, std::ostream> Implementations;
TYPED_TEST_CASE(IosTest, Implementations);
TYPED_TEST(IosTest, DummyTest) {
signed char c[] = ".";
this->ios_->fill(c[0]);
};
In the line typedef Types<std::istream, std::ostream> Implementations; is created a list of types called Implementations and in the following line, TYPED_TEST_CASE(IosTest, Implementations);, is defined that the test case IosTest will be applied to the typed defined in the Implementations list.
As I have already said, if I remove either std::istream or std::ostream from the Implementations list I can compile and run the tests without any warning (I am using the -Wall flag). Can anyone explain this phenomenon?
Is it is possible your gtest library was built with a different version compiler that you are compiling your app (stackoverflow.cpp) with? I recall seeing this error message related to a lib I had built with a newer version of gcc and trying to link it with an older version of gcc.
You can try building gtest from source. It comes with a script that extracts and fuses everything into a single header file and a single cpp file.
Look in your gtest installation for this python script:
gtest/scripts/fuse_gtest_files.py
There are instructions in the script for how to run it. You end up with two files:
gtest-all.cc
gtest.h
You only need to do this once and add it to your makefile. I do exactly this for distributing a Linux-based app to a customer.
It looks like GCC bug described here.
If you change signed char c[] = "."; to char c[] = "."; everything seems to work just fine.