1

I have some Arduino C++11 code that I'm trying to improve: trying to make a printf-like function treat String specially so I don't have to call c_str() myself everywhere I use it. Basically for any builtin type like int float bool etc, I just want to pass the arg as-is, and for String, pass return the c_str(). Hit some snags so I tried this in some of the online compilers available. Starting point is this, using std::string instead of String:

#include <string>

class SerialOut {
public:
    template<class ...Ts>
    static void error(const char* msg, Ts... args) {
        printf(msg, args...);
    }
};

int main() {
    std::string greeting("hi");
    SerialOut::error("Message %d %s\n", 1, greeting.c_str());
}

So I tried creating a function template that just returns the value it gets, with a specialization for std::string:

#include <string>

template <typename T, typename R=T> R raw(T& x) {return x;}
template <> const char* raw<>(std::string& x) {return x.c_str();}

class SerialOut {
public:
    template<class ...Ts>
    static void error(const char* msg, Ts... args) {
        printf(msg, raw(args)...);
    }
};

int main() {
    std::string greeting("hi");
    SerialOut::error("Message %d %s\n", 1, greeting);
}

I get a compilation error when I run this in https://repl.it/languages/cpp11:

clang version 7.0.0-3~ubuntu0.18.04.1 (tags/RELEASE_700/final)
 clang++-7 -pthread -std=c++11 -o main main.cpp
main.cpp:10:25: error: cannot pass object of non-trivial type
      'std::__cxx11::basic_string<char>' through variadic function; call will abort at
      runtime [-Wnon-pod-varargs]
            printf(msg, raw(args)...);
                        ^
main.cpp:16:20: note: in instantiation of function template specialization
      'SerialOut::error<int, std::__cxx11::basic_string<char> >' requested here
        SerialOut::error("Message %d %s\n", 1, greeting);
                   ^
1 error generated.
compiler exit status 1

With https://www.onlinegdb.com/online_c++_compiler there is no error but the raw() specialization is not selected, so the output for greeting is garbage.

In Arduino IDE I get a slightly different error (after replacing std::string by String, of course):

sketch\mqtt.cpp.o: In function `char const* raw<String, char const*>(String&)':

sketch/utils.h:15: multiple definition of `char const* raw<String, char const*>(String&)'

sketch\Thermistor.cpp.o:sketch/utils.h:15: first defined here

sketch\sketch.ino.cpp.o: In function `char const* raw<String, char const*>(String&)':

sketch/utils.h:15: multiple definition of `char const* raw<String, char const*>(String&)'

sketch\Thermistor.cpp.o:sketch/utils.h:15: first defined here

I tried several variations on the raw() functions, to no avail. I figure I'm just missing a subtlety or it's just not possible to do this in C++11.

Update: I found Variadic Macro: cannot pass objects of non-trivially-copyable type through '...', one of the answers solves the above in C++14 (basically use decltype(auto) and overload instead of specialization). I added a slight variation on it that works also in C++11, and with "inline" it also works in Arduino C++ (without "inline" on the overload, the above message about multiple definitions -- turns out this is a linker message so it does compile, I guess the Arduino variant doesn't inline "obviously inlined" functions as other compilers).

Oliver
  • 27,510
  • 9
  • 72
  • 103
  • 1
    You've specialized `raw` but the call `raw(greeting)` calls `raw` – Igor Tandetnik Mar 15 '20 at 16:44
  • Thanks @IgorTandetnik but this is not an explanation, see my answer below which uses an overload. The compiler is able to choose the overload, but not the specialization. Note that I also tried a one-parameter specialization. – Oliver Mar 15 '20 at 17:11
  • Just overload the template with a regular function. – Igor R. Mar 15 '20 at 17:15
  • Specializations don't participate in overload resolution; only the primary template does. If it's chosen, then the compiles checks whether it should instantiate the primary template or use one of the specializations. `raw(greeting)` matches the primary template, with `T` deduced as `std::string`, and `R` defaulted to be the same as `T` (it cannot be deduced). These template parameters then don't match the specialization, so the primary template is instantiated. No overload resolution takes place, as there's only one candidate. – Igor Tandetnik Mar 15 '20 at 17:15
  • @IgorTandetnik got it, thanks! I did not remember that "Specializations don't participate in overload resolution; only the primary template does", that's the key. Actually can you move your comment to your answer, I'd feel better +1'ing it with an explanation. – Oliver Mar 15 '20 at 17:21

2 Answers2

0

Something along these lines, perhaps:

template <typename T>
struct SerialHelper {
    static T raw(T val) { return val; }
};

template <>
struct SerialHelper<std::string> {
    static const char* raw(const std::string& val) { return val.c_str(); }
};


class SerialOut {
public:
    template<class ...Ts>
    static void error(const char* msg, Ts... args) {
        printf(msg, SerialHelper<Ts>::raw(args)...);
    }
};

Demo

Igor Tandetnik
  • 50,461
  • 4
  • 56
  • 85
0

Based on Variadic Macro: cannot pass objects of non-trivially-copyable type through '...' I got it to work with this very simple change, which works in C++11 and Arduino C++:

#include <string>

template <typename T> T raw(const T& x) {return x;}
inline const char* raw(const String& x) {return x.c_str();}

class SerialOut {
public:
    template<class ...Ts>
    static void error(const char* msg, Ts... args) {
        printf(msg, raw(args)...);
    }
};

int main() {
    std::string greeting("hi");
    SerialOut::error("Message %d %s\n", 1, greeting);
}

Thanks to @IgorTandetnik comment, it is clear why.

Oliver
  • 27,510
  • 9
  • 72
  • 103