3

I am trying to write a macro for logging mechanism. I wrote a variadic macro but it does not work with std::string. The code looks like the following:

#include <stdio.h>
#include <string>


#define LOG_NOTE(m, ...) printf(m, ##__VA_ARGS__)

int main()
{
    std::string foo = "random string";
    int bar = 5;
    LOG_NOTE("%s %d %s", "Hello World", bar, foo);

    return 0;
}

If I would call the macro like following, I would not get any error.

LOG_NOTE("%s %d %s", "Hello World", bar, "random string");

Compiler Output:

In function 'int main()': 5:49: error: cannot pass objects of non-trivially-copyable type 'std::string {aka class std::basic_string}' through '...' 11:5: note: in expansion of macro 'LOG_NOTE'

eneski
  • 1,575
  • 17
  • 40

4 Answers4

6

The issue here is not the variadic macro, but the call to printf. Have a look at the documentation: the format specifier "%s" corresponds to char*, not std::string. printf can only handle primitive builtin types. You can change you invocation to

LOG_NOTE("%s %d %s", "Hello World", bar, foo.c_str());

to fix this.

lubgr
  • 37,368
  • 3
  • 66
  • 117
5

I wrote a variadic macro

Don't. Use a variadic template function.

The actual problem you have is that you're trying to pass a C++ object (std::string) through a C API (printf). This is not possible.

You'd need some mechanism for conversion, for example:

#include <stdio.h>
#include <string>

template<class T>
decltype(auto) convert_for_log_note(T const& x)
{
    return x;
}

decltype(auto) convert_for_log_note(std::string const& x)
{
    return x.c_str();
}


template<class...Args> 
void LOG_NOTE(const char* format, Args&&...args)
{
    printf(format, convert_for_log_note(args)...);
}

int main()
{
    std::string foo = "random string";
    int bar = 5;
    LOG_NOTE("%s %d %s\n", "Hello World", bar, foo);

    return 0;
}

Example output:

Hello World 5 random string

http://coliru.stacked-crooked.com/a/beb3431114833860

Update:

For C++11 you'll need to spell out the return types by hand:

#include <stdio.h>
#include <string>

template<class T>
T const& convert_for_log_note(T const& x)
{
    return x;
}

const char* convert_for_log_note(std::string const& x)
{
    return x.c_str();
}


template<class...Args> 
void LOG_NOTE(const char* format, Args&&...args)
{
    printf(format, convert_for_log_note(args)...);
}

int main()
{
    std::string foo = "random string";
    int bar = 5;
    LOG_NOTE("%s %d %s\n", "Hello World", bar, foo);

    return 0;
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • In all fairness, logging macro's are a well-known exemption from the "no macro's" rule, for three reasons: `__FILE__`, `__LINE__` and the ability to turn them into do-nothing statements in release builds. You can't eliminate side effects from function call arguments. – MSalters Aug 15 '18 at 11:09
  • @MSalters as long as the c++ committee behave themselves, we'll soon have std::source_location, which along with `if constexpr`, will eliminate the need for macros altogether. Note that the argument side-effects are not an issue as long as you pass the arguments by universal reference. Obviously passing LOG("%s%", mything.to_string().c_str()); does invoke side-effects and should obviously be avoided. Better to provide an ADL overload for `convert_for_log_note(MyThing const&)` type. – Richard Hodges Aug 15 '18 at 14:41
  • This is exactly what I'm looking for, but it does not seem to work in C++11 at least not with GNU GCC v7.1.1 using `g++ -std=c++11`. It does work however in C++14. Anyone know how to modify this to make it work in C++11? – Oliver Mar 15 '20 at 16:23
  • 1
    @Oliver I've added an update which will compile for c++11. https://godbolt.org/z/M5GmyD – Richard Hodges Mar 16 '20 at 19:19
  • This is super cool. One thing I ran into was that the ability to use `decltype(auto)` as the return type of the `std::string` specialization of the `convert_for_log_note` template depends on how it is first used in the code. (Something along the lines of https://stackoverflow.com/a/43514762/430067.) I just changed it to `const char*` because I did not see any reason for the generic return type. – Shibumi May 07 '20 at 18:55
4

You cannot pass object to printf, so you have currently to use

LOG_NOTE("%s %d %s", "Hello World", bar, foo.c_str());

If you don't need formatting, and just write every argument separated with space, you might simply use variadic template instead of MACRO:

template <typename ... Ts>
void LOG_NOTE(const Ts&...args)
{
    const char* sep = "";
    (((std::cout << sep << args), sep = " "), ...); // C++17 folding expression
    // C++11/C++14 version are more verbose:
    // int dummy[] = {0, ((std::cout << sep << args), (sep = " "), 0)...};
    // static_cast<void>(dummy); // avoid warning for unused variable
}

int main()
{
    std::string foo = "random string";
    int bar = 5;
    LOG_NOTE("Hello World", bar, foo);
}

Demo

Jarod42
  • 203,559
  • 14
  • 181
  • 302
0

I could not get @richardhodges nice solution to work in any of the C++11 compilers I tried. However, the following works with gcc -std=c++11:

#include <stdio.h>
#include <string>

template<class T>
T convert_for_log_note(T const& x)
{
    return x;
}

inline const char* convert_for_log_note(std::string const& x)
{
    return x.c_str();
}


template<class...Args> 
void LOG_NOTE(const char* format, Args&&...args)
{
    printf(format, convert_for_log_note(args)...);
}

int main()
{
    std::string foo = "random string";
    int bar = 5;
    LOG_NOTE("%s %d %s\n", "Hello World", bar, foo);

    return 0;
}

The inline keyword is necessary with the above solution for the Arduino C++ compiler, whereas other g++ compilers do not require it (the ones I have tried, anyways). Without this keyword, the Arduino code compiles, but the linker complains about multiple definitions.

Oliver
  • 27,510
  • 9
  • 72
  • 103