Passing Non-Pod Type to Variadic Function Is Undefined Behavior

Passing NON-POD type to Variadic function is undefined behavior?

It's specified in C++11 5.2.2/7:

Passing a potentially-evaluated argument of class type having a non-trivial copy constructor, a non-trivial move contructor, or a non-trivial destructor, with no corresponding
parameter, is conditionally-supported with implementation-defined semantics.

So it's up to each compiler whether to support it or not; portable code can't rely on any implementation defined behaviour. In older standards, it was simply undefined.

c++ non-POD warning for passing a string?

The explanation is quite simple: only PODs (Plain Old Data structures) can be passed as an argument to a variadic function (not a variadic function template though, just a simple variadic function using the ellipses).

std::string is not a POD, but you can do:

printf("%s% 38s\n", "Filename:", filename.c_str());
// ^^^^^^^^

The c_str() member function returns a const char* to the encapsulated C string.

Is it possible to handle non-primitive types in a variadic function?

va_arg decodes the va_list

You cannot use va_arg to convert the passed in variable argument parameter in a single go. The syntax of va_arg would be to decode the variable argument to match the type that was passed in. For example, if you pass in an int value, you must decode it as an int with va_arg. You cause undefined behavior if you try to decode it as anything else (like, as a double).

Since you passed in a string literal, the type should be const char *.

const char *arg = va_arg(file_names, const char *);

va_arg on non-trivial classes is implementation-defined

As MSalter's answer clearly explains, even if you really passed in a std::string to a function expecting variable arguments, it is not necessarily going to work because the semantics are implementation-defined.

When there is no parameter for a given argument, the argument is passed in such a way that the receiving function can obtain the value of the argument by invoking va_arg (18.10). … Passing a potentially-evaluated argument of class type (Clause 9) having a non- trivial copy constructor, a non-trivial move contructor, or a non-trivial destructor, with no corresponding parameter, is conditionally-supported with implementation-defined semantics. …

C++.11 §[expr.call] ¶7

Note: (18.10) defines va_arg, and (Clause 9) is §[class].

Just use a variadic template

You can use C++11's variadic template argument feature to achieve the effect you want in a type safe way. Assume you actually want to do a little more than print each argument, the usual pattern is to recursively traverse the parameter pack, unpacking one parameter at a time.

void test3 (int f_num, std::string file_name) {
std::cout << f_num << ':' << file_name << std::endl;
}

template <typename... T>
void test3 (int f_num, std::string file_name, T...rest) {
test3(f_num, file_name);
test3(f_num, rest...);
}

Thus, iteration is achieved by recursively calling the function, reducing the parameter pack by one parameter on the next recursive call.

clang error: cannot pass object of non-trivial type 'std::vectorlong' through variadic method; call will abort at runtime [-Wnon-pod-varargs]

Why not also pass the function as a template parameter:

#include <iostream>
#include <string>
#include <vector>

bool save(int a, std::vector<long> arr)
{
std::cout << a << '\n';
std::cout << " hello \n";
return true;
}

template <typename T, typename Callable, typename... Args>
bool sum_super_cool(T v, Callable ins_func, Args&&... args)
{
std::cout << "pre\n";
return ins_func(std::forward<Args>(args)...);
}

int main(int argc, char** argv) {
std::vector<long> arr;
arr.push_back(123);
sum_super_cool(1, save, 2, arr);

return 0;
}

This works fine without any reinterpret_casting voodoo. That should almost always be a giant red flag!

Why does the spec prohibit passing class types to variable-argument C++ functions?

The calling convention does specify who does the low-level stack dance, but it doesn't say who's responsible for "high-level" C++ bookkeeping. At least on Windows, a function that accepts an object by value is responsible for calling its destructor, even though it is not responsible for the storage space. For instance, if you build this:

#include <stdio.h>

struct Foo {
Foo() { puts("created"); }
Foo(const Foo&) { puts("copied"); }
~Foo() { puts("destroyed"); }
};

void __cdecl x(Foo f) { }

int main() {
Foo f;
x(f);
return 0;
}

you get:

x:
mov qword ptr [rsp+8],rcx
sub rsp,28h
mov rcx,qword ptr [rsp+30h]
call module!Foo::~Foo (00000001`400027e0)
add rsp,28h
ret

main:
sub rsp,48h
mov qword ptr [rsp+38h],0FFFFFFFFFFFFFFFEh
lea rcx,[rsp+20h]
call module!Foo::Foo (00000001`400027b0) # default ctor
nop
lea rax,[rsp+21h]
mov qword ptr [rsp+28h],rax
lea rdx,[rsp+20h]
mov rcx,qword ptr [rsp+28h]
call module!Foo::Foo (00000001`40002780) # copy ctor
mov qword ptr [rsp+30h],rax
mov rcx,qword ptr [rsp+30h]
call module!x (00000001`40002810)
mov dword ptr [rsp+24h],0
lea rcx,[rsp+20h]
call module!Foo::~Foo (00000001`400027e0)
mov eax,dword ptr [rsp+24h]
add rsp,48h
ret

Notice how main constructs two Foo objects but destroys only one; x takes care of the other one. That obviously wouldn't work if the object was passed as a vararg.


EDIT: Another problem with passing objects to functions with variadic parameters is that in its current form, regardless of the calling convention, the "right thing" requires two copies, whereas normal parameter passing requires just one. Unless C++ extended C variadic functions by making it possible to pass and/or accept references to objects (which is extremely unlikely to ever happen, given that C++ solves the same problem in a type-safe way using variadic templates), the caller needs to make one copy of the object, and va_arg only allows the callee to get a copy of that copy.

Microsoft's CL tries to get away with one bitwise copy and one full copy construction of that bitwise copy at the va_arg site, but it can have nasty consequences. Consider this example:

struct foo {
char* ptr;

foo(const char* ptr) { this->ptr = _strdup(ptr); }
foo(const foo& that) { ptr = _strdup(that.ptr); }
~foo() { free(ptr); }

void setPtr(const char* ptr) {
free(this->ptr);
this->ptr = _strdup(ptr);
}
};

void variadic(foo& a, ...)
{
a.setPtr("bar");

va_list list;
va_start(list, a);
foo b = va_arg(list, foo);
va_end(list);

printf("%s %s\n", a.ptr, b.ptr);
}

int main() {
foo f = "foo";
variadic(f, f);
}

On my machine, this prints "bar bar", even though it would print "foo bar" if I had a non-variadic function whose second parameter accepted another foo by copy. This is because a bitwise copy of f happens in main at the call site of variadic, but the copy constructor is only invoked when va_arg is called. Between the two, a.setPtr invalidates the original f.ptr value, which is however still present in the bitwise copy, and by pure coincidence _strdup returns that same pointer (albeit with a new string inside). Another outcome of the same code could be a crash in _strdup.

Note that this design works great for POD types; it only falls apart when constructors and destructors need side effects.

The original point that calling conventions and parameter passing mechanisms don't necessarily support non-trivial construction and destruction of objects still stands: this is exactly what happens here.


EDIT: answer originally said that the construction and destruction behavior was specific to cdecl; it is not. (Thanks Cody!)

type-safety by using the ellipsis notation

You may only pass "plain old data" (POD) types as variadic arguments. These are basic types (including pointers), and simple aggregates of other POD types; anything with non-trivial constructors, destructor, base classes or virtual functions are not POD. Passing a non-POD type gives undefined behaviour.

If you really want to use variadic functions in conjunction with complex types, you will have to pass them by pointer.

UPDATE: The C++0x draft relaxes "undefined behaviour" to "conditionally-supported, with implementation-defined semantics". I'm assuming this means you'll either get correct runtime behaviour (using the copy constructor/destructor where necessary), or a compile error, but never incorrect runtime behaviour from a conforming implementation.

Why does MSVC give me 'wrong' results?

You are passing a vector::iterator as a printf argument, where an integer is expected. This gives undefined behaviour. (Incidentally, so does passing a pointer as the second argument; but this is likely to give the result you expect on a 32-bit platform). printf is not type-checked, which is why you should usually use C++-style I/O instead.

GCC's implementation is just a wrapper around a pointer, so you accidentally get the result you expect; MSVC's implementation seems to be a larger type - certainly, in a debug build, iterators are quite large on that platform in order to support run-time validity checks.

I get no warnings/errors

That's because you didn't enable them. On gcc, with -Wall (or just -Wformat), I get:

test.cpp:16:72: warning: format ‘%X’ expects argument of type ‘unsigned int’, but argument 2 has type ‘__gnu_cxx::__normal_iterator<Person*, std::vector<Person> >’ [-Wformat]
test.cpp:16:72: warning: format ‘%X’ expects argument of type ‘unsigned int’, but argument 3 has type ‘Person*’ [-Wformat]

telling me exactly what's wrong.

Get var args from a varadic function with struct parameters in C

Passing a non-POD type as a variadic parameter is non-portable. I believe it will work (with warnings) in MSVC, while clang will outright refuse to compile it. In other cases it may compile (hopefully with warnings), but not execute in the way you might expect.

You can instead pass the variadic parameters as pointers then in the function:

// Get copy
my_struct input = *va_arg( vl, my_struct* ) ;

or

// Get reference
my_struct* input = va_arg( vl, my_struct* ) ;

The call might then look like:

my_struct a ;
my_struct b ;
...
my_func( 2, str, &a, &b ) ;

error: cannot pass object of non-trivial type 'std::string' and more errors

curl_easy_setopt is a C function, where variadic actually means the <cstdarg>'s ... parameter. It accepts only trivial types, which std::string is not (i.e., it cannot be copied with memcpy, and instead, a non-trivial copy-constructor is involved); otherwise the behavior is undefined or only conditionally supported. In order to pass a string value, use its const char* representation obtained with c_str():

curl_easy_setopt(curl, CURLOPT_PASSWORD, apikey.c_str());
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());


Related Topics



Leave a reply



Submit