0

I would like to generate a version of printf that automatically converts its std::string parameters to their c_str() values. (I find the printf syntax much cleaner and simpler than C++ streams).

I want to use something SIMPLE. Short and simple is the only design goal. Boost has something like this but it is much too complex. I do NOT care about efficiency or avoiding copies at all.

Below is a simple example that almost works with one parameter. There are two problems with the code:

(1) How do I extend it to an arbitrary number (or say at least 3) parameters? I know I can use variadic templates, but I don't understand how to use them here.

(2) How can I keep compiler warnings (under clang with -Wall) when there is a type mismatch between the format parameter const char* and the actual object to be printed?

Edit: Part (2) has been nearly solved. I add a __attribute__((format printf,1,2)) just before the myprint(). This does the typechecking but requires that myprint be variadic.

Sample code:

#include <stdio.h>
#include <string>
using std::string;
template <typename T>
void myprint(const char* format, T arg){
  printf(format, arg);
}

template <>
void myprint(const char* format, string arg){
  printf(format,(arg+" STRING ").c_str());
}

int main(){
  string x("foo");
  myprint ("The value of 1 is: %s\n", "simple"); //works
  myprint ("The value of 2 is: %s\n", x); // works
  myprint ("The value of 2 is: %d\n", x); // fails - no warning!
  printf ("The value of 2 is: %d\n", x.c_str()); // works - warning
  return 0;
}
kdog
  • 1,583
  • 16
  • 28
  • `printf()` gives undefined behaviour if the format string specifier does not match the corresponding argument (e.g. specifying `%d` when `const char *` is supplied). Your third call of `myprint()` does the same - specifying `%d` when the corresponding argument is a `std::string`, which your code translates into a call of `printf()` with a `%d` format and a `const char *`. A compiler can't give similar warnings in templated code until the template is instantiated. – Peter Feb 27 '18 at 01:08
  • 1
    In any event, with google, you will be able to find examples of a type-safe `printf()` that uses variadic templates in C++. – Peter Feb 27 '18 at 01:09
  • @Peter - His complaint is the warning is NOT appearing once the call is wrapped. I suspect certain warnings are turned off for template code...? (but I don't know clang well enough for that to be anything but a wild guess) – zzxyz Feb 27 '18 at 01:14
  • Compiler warnings with `printf()` rely on the compiler scanning the format string, and comparing specifiers with types of arguments. The compiler can't for that with `printf()` for an arbitrary format string (e.g. one generated at run time) and won't tend to do that for arbitrary functions that accept a `const char *` either without use of compiler-specific extensions. – Peter Feb 27 '18 at 01:25
  • @Peter the format string is not generated at run time. The format string is generated at compile time. So the compiler should be able to warn here, not sure why it isn't. – kdog Feb 27 '18 at 01:28
  • 1
    @kdog: the only format string available at compile-time is inside your `main()` function. Where you are actually calling `printf()`, the format string is passed as a variable whose value is not known until runtime, so the compiler can't validate the `printf()` call. However, you can have the compiler validate your `mystring()` function calls in `main()`, where you are passing in string literals for the format strings. Mark `myprintf()` with the [`format` attribute](https://clang.llvm.org/docs/AttributeReference.html#format-gnu-format) so the compiler knows you want that behavior. – Remy Lebeau Feb 27 '18 at 01:33
  • Check out boost::format and http://fmtlib.net/ I think fmtlib is super duper. – Jive Dadson Feb 27 '18 at 02:19
  • @JiveDadson "fmtlib" is super duper? Could you please explain that in more detail? – John Aug 22 '21 at 11:50

2 Answers2

0

The problem is you are using C-style printf specifiers without regard to what kind of data you are passing to them. You can't pass a std::string or a char char* to the %d specifier. This is exactly the type of mistake that C++ streams avoid.

If you want your compiler to validate your myprint() function's printf-style parameters at compile-time, you need to mark the function accordingly. GCC and Clang have a format attribute for that purpose (see the clang documentation about that).

Otherwise, to really use printf() in a type-safe manner, you should make your myprint() function use C++11 variadic template parameters, and let the template parameters dictate what specifiers and data you pass to printf() for each argument. There are plenty of "safe" printf implementations using variadic templates available online if you look around.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Doesn't have to be type safe but why can't it give the same warning that printf() does? That is, printf() gives a warning when there is a type mismatch outside a template. Why can't it warn inside a template? – kdog Feb 27 '18 at 00:53
  • Is there some simple, short way for the template itself to check that its format string is type safe, assuming say the format string just uses `%d`, `%s`, `%p` and does not use `%` otherwise? – kdog Feb 27 '18 at 01:03
  • perhaps with constexpr string introspection. Not a small job though. – Richard Hodges Feb 27 '18 at 01:05
  • @kdog: "*printf() gives a warning when there is a type mismatch outside a template*" - that is a compiler-specific extension. Not all C++ compilers validate printf style parameters at compile-time. "*Is there some simple, short way for the template itself to check that its format string is type safe*" - see https://stackoverflow.com/a/17672820/65863 – Remy Lebeau Feb 27 '18 at 01:14
  • I only care about warnings in clang. But the code itself should of course compile outside clang. (clang is the development machine but deployment is to many other platforms) – kdog Feb 27 '18 at 01:16
  • @kdog: if you want the compiler to validate printf-style parameters in your own function, you have to mark the function accordiingly. GCC and Clang have attributes for that purpose ([clang documentation](https://clang.llvm.org/docs/AttributeReference.html#format-gnu-format)). – Remy Lebeau Feb 27 '18 at 01:18
  • @kdog: also see: [Compile-Time and Runtime-Safe Replacement for “printf”](http://blogs.microsoft.co.il/sasha/2015/01/29/compile-time-runtime-safe-replacement-printf/) (though it is for VC++, but can probably be adapted for other C++11 compilers) – Remy Lebeau Feb 27 '18 at 01:21
  • @RemyLebeau How can I get this warning to appear for templated code, specifically? – kdog Feb 27 '18 at 01:21
  • @kdog: Marking a function that has printf-style parameters for compile-time validation has nothing to do with templates. – Remy Lebeau Feb 27 '18 at 01:22
  • @RemyLebeau I'm not sure if we are on the same page here. The warnings only fail to appear for an instantiated templated function. How specifically do I get the warning in this case? – kdog Feb 27 '18 at 01:24
  • @kdog: given the example you have shown in your question, there is no way to get the warning on the `printf()` because it is wrapped inside another function that passes in a format string determined at runtime, so there is no chance for compile-time checking on the `printf()` itself. That has nothing to do with templates. However, you can mark your main `myprintf()` function with the `format` attribute, and then the compiler can validate the calls to that function in your example `main()`. – Remy Lebeau Feb 27 '18 at 01:31
  • @RemyLebeau You are the second person here to say the format string is "passed in at runtime". This isn't how the template works. The template generates a `printf()` at compile time. That `printf()` has a normal compile time format string. Nothing is happening at run time that is relevant. – kdog Feb 27 '18 at 01:35
  • @kdog: You clearly don't understand how functions work, templated or otherwise. `template void myprint(const char* format, T arg){ printf(format, arg); }` - that is calling `printf()` at runtime, with a `format` variable whose value is only determined at runtime. The compiler doesn't know where `format` gets its value from, that information is not available at compile-time. So the compiler can't validate the `printf` call like you are expecting it to. Maybe it could if `myprintf()` were marked as `inline`, but even that is not a guarantee... – Remy Lebeau Feb 27 '18 at 01:39
  • @kdog: Just because a function is templated does not mean its inner code is automatically injected inline at the call site where the templated function is used. You seem to think it is. It is not. A templated function is still a function, with everything that goes with that (ie, parameter values are determined at runtime, not at compile-time). – Remy Lebeau Feb 27 '18 at 01:42
  • @RemyLebeau Now I see the issue, I think you're right. But then there must be some way then for the template to inject the constant format string at compile time on the one hand, or for the Clang __ATTRIBUTE__ feature to work here. Isn't there some `constexpr` or template magic that would do this? – kdog Feb 27 '18 at 01:46
  • @RemyLebeau I think I got the warning to work with attribute! Thanks!! – kdog Feb 27 '18 at 01:54
  • @RemyLebeau Oops, I misread the compiler message. The compiler does warn correctly on the template when `__attribute__((format(printf,1,2))` is added before `myprint` but it also gives an error that `myprint` must be variadic...but I think this can remedied without much difficulty. It's the variadic template part now I have to think about... – kdog Feb 27 '18 at 02:04
0

Here an idea:

namespace io {
template <class X>
using is_string = typename std::enable_if<std::is_same<X, std::string>::value>::type;

template <class X>
using is_not_string = typename std::enable_if<!std::is_same<X, std::string>::value>::type;

template <class T, class = is_not_string<T>>
constexpr T forward_or_transform(T t) {
    return std::forward<T>(t); 
}

template <class T, class = is_string<T>>
constexpr const char* forward_or_transform(T t) { 
    return t.c_str(); 
}

template <class ...Ti>
int printf(const std::string& format, Ti...t) {
    return std::printf(format.c_str(), forward_or_transform(t)...);
}
}

auto s = std::string("const std::string&");
io::printf("[%s]-[%s]-[%s]-[%d]\n", std::string("std::string&&"), "const char *", s, 42);

Print: [std::string&&]-[const char *]-[const std::string&]-[42]

user1823890
  • 704
  • 8
  • 7