If an argument with a specific type (which we will see which type in this post) needs an implicit conversion when passed to a variadic function, such behavior will trigger compiler’s warning message which is on by default for clang but not the others (GCC, and MSVC). It introduces a somewhat inconsistency in equal of handling across the compilers that we would need some awareness.
So this post is more or less like an exploration to the compilers’ behavior for this implicit conversion of arguments passed to variadic function; to see how we could trigger it, disable/enable such warning, reasoning behind, and solution.
We will explore this behavior with the following compilers. It tested on native Ubuntu 20.04 for GCC & Clang, and Windows 10 machine for MSVC.
Let’s say we have the following function
static void FreeLog(const char* fmt, ...)
{
va_list arglist;
va_start(arglist, fmt);
std::vprintf(fmt, arglist);
va_end(arglist);
}
You can find the reference of the full source code at the bottom of this code at Reference section, or you could gradually code it up while reading this.
This function is defined in global scope outside of the main()
function.
No need to worry too much about the code inside FreeLog()
. We just want to have some code that consumes what we would be sending in as arguments. The attention will weight more on compilation phase.
There are 3 test cases.
Now let’s consume it with the following code inside main()
function.
FreeLog("1: %f %s\n", 10.0, "Helloworld");
if we try to compile above code against all compilers listed above with compile options for highest warning verbosity but still practical as follows
clang++ -std=c++17 -Wall -Wextra -pedantic ClangImplicitConversion.cpp
g++ -std=c++17 -Wall -Wextra -pedantic ClangImplicitConversion.cpp
cl.exe /EHsc /std:c++17 /W4 ClangImplicitConversion.cpp
It should compile without any noticing and relevant warnings to our case. It will print the following output.
1: 10.000000 Hello world
Arguments passed in as exact type directly, no implicit, or type complications to worry. Then, all is fine!
Now, proceed to the next one.
Note:
printf
and its variants only acceptdouble
, although we would see"%f"
which pretty much signifiesfloat
but at the final it treats it asdouble
. So there’s no data conversion whatsoever there. Except if usescanf
and its variants in which"%f"
meansfloat
, and"%lf"
meansdouble
.
Now we get into a little bit more complicated by involving trivial type called Pod
.
struct Pod
{
double foo;
const char* bar;
operator const char*() const { return bar; }
};
Notice we have defined a user-defined conversion function so that we can just supply the object of type Pod
to any function that expects to receive const char*
. Also it is suitable for our test with implicit conversion.
Firstly validate and make sure such object declaration is really trivial. Make sure you include header <type_traits>
.
static_assert(std::is_trivial<Pod>::value, "Pod structure must be trivial type");
then we get into the meat of this test case
Pod st { 10.0, "Bar text" };
FreeLog("2: %f %s\n", st.foo, st);
Notice the second argument which we pass in st
mapping to %s
such that it will use our user-defined conversion function from Pod
to const char*
.
Compile with compile options as seen in Test case 1. All is fine again from all compilers.
Last test case, now we go with non-trivial type with the following declaration
template<class T, size_t SIZE>
struct MyStringWrapper
{
public:
typedef T value_type;
typedef const value_type* const_str;
public:
MyStringWrapper(const_str str)
{
std::strcpy(strBuf, str);
}
~MyStringWrapper() { }
operator const_str() const { return strBuf; }
private:
value_type strBuf[SIZE];
};
It is a minimal implementation of template string wrapper, just to return internal array of characters as a c-string pointer.
Not to be too much paying attention to template declaration, it would be similar case for this test case even with normal class declaration.
For a little bit of convenience, we will typedef
it.
typedef MyStringWrapper<char, 512> MyString;
Now start with our validation to make sure our understanding of such type is correct
static_assert(!std::is_trivial<MyString>::value, "MyString must be non-trivial type");
Then follow by the meat of this test case
MyString myStr("Hello world");
FreeLog("3: %s\n", myStr);
As usual, compile and run program with above compile options.
We will see the following result only from clang
ClangImplicitConversion.cpp:93:21: error: cannot pass object of non-trivial type 'MyString' (aka 'MyStringWrapper<char, 512>') through variadic function; call will abort at runtime [-Wnon-pod-varargs]
FreeLog("3: %s\n", myStr);
^
1 error generated.
But other compilers keep quiet about it.
The reason about this is stated in C++ standardese terms
From C++17 latest draft N4659 [expr.call 8.2.2/9] > Passing a potentially-evaluated argument of class type (Clause 12) having a non-trivial copy constructor, a non-trivial move constructor, or a non-trivial destructor, with no corresponding parameter, is conditionally-supported with implementation-defined semantics.
So it’s up to compiler’s specific implementation on how to deal with it. Thus we see different behavior across compilers we’ve tested so far.
Anyway, it would be a red herring to see this warning coming out from just only one compiler especially if we are using an in-house automated build system. It will definitely catch our eyes for this inconsistency behavior across the board.
Next question is how we can control this…?
There is a discussion (reported as bug) for non-POD type that supplied into variadic function that GCC itself didn’t warn about anything by default. Such report dated back in 2015!
We can opt-in turning on -Wconditionally-supported
. This particular warning option of conditionally-supported behavior covers multiple of things not just our case. It’s compiler implementation specific whether or not to support certain behaviors.
So change the compile options to
g++ -std=c++17 -Wall -Wextra -pedantic -Wconditionally-supported ClangImplicitConversion.cpp
Now we would see the following warning instead
ClangImplicitConversion.cpp: In function ‘int main()’:
ClangImplicitConversion.cpp:92:26: warning: passing objects of non-trivially-copyable type ‘MyString’ {aka ‘struct MyStringWrapper<char, 512>’} through ‘...’ is conditionally supported [-Wconditionally-supported]
92 | FreeLog("3: %s\n", myStr);
|
Notice the warning message saying non-trivially-copyable
in which we can do one step more by validating against std::is_trivially_copyable
but that might not be necessary (at least for our case) as a type that regarded as trivially copyable means that type would at least be trivial object.
So in case we want to be more pedantic about non-trivially-copyable
to check base with our understanding, we can add the following line after the line of std::is_trivial
.
static_assert(!std::is_trivially_copyable<MyString>::value, "MyString must be non-trivially-copyable type");
At very least, feel free to consult cppreferences, or consult
[basic.types], no.9
about trivially copyable types.
We noticed that it shows as
warning
noterror
like the case of clang. That’s because we didn’t tell compiler to treat warning as error. We can do that by supplying-Werror
.
I did some research, and the best I came across is from MSVC C++ team which decided to only implement checking against the standard printf/scanf
functions and not support user-defined function.
So we are at the mercy of the compiler’s ability. In case, you find a new announcement or update on this, please feel free to let me know.
To see the effect of warning for printf
function, we can add
std::printf("3: %s\n", myStr);
It will output a warning message like below
ClangImplicitConversion.cpp(95): warning C4477: 'printf' : format string '%s' requires an argument of type 'char *', but variadic argument 1 has type 'MyString'
As it’s turned on by default, then we would like to know how to turn it off.
We use -Wno-non-pod-varargs
to disable such warning (which is error by default). Ref
You can control it from the code by being explicit in data type casting. It will cover either trivial or non-trivial type case.
So changing the following line
MyString myStr("Hello world");
FreeLog("3: %s\n", myStr);
to
MyString myStr("Hello world");
FreeLog("3: %s\n", static_cast<const char*>(myStr));
provided that MyString
already have user-defined conversion function to const char*
. Then clang would no longer output warning message. Other compilers continue as normal.
Test case source code can be found on my Github, direct URL is at ClangImplicitConversion.cpp.
First published on February, 9, 2021
Written by Wasin Thonkaew
In case of reprinting, comments, suggestions
or to do anything with the article in which you are unsure of, please
write e-mail to wasin[add]wasin[dot]io
Copyright © 2019-2021 Wasin Thonkaew. All Rights Reserved.