Reputation: 9622
I have 2 versions of the same variadic function, however one works the other doesn't. I suspect the reason is because one is using a primitive type where the other uses std::string.
void test1(int f_num, ...)
{
va_list file_names;
va_start(file_names, f_num);
for(int i=0; i<f_num; i++)
{
string name = string(va_arg(file_names, char*));
cout << name << endl;
}
va_end(file_names);
}
void test2(int f_num, ...)
{
va_list file_names;
va_start(file_names, f_num);
for(int i=0; i<f_num; i++)
{
string name = va_arg(file_names, string);
cout << name << endl;
}
va_end(file_names);
}
int main()
{
test1(3, "Hallo", "you", "people");
test2(3, "Hallo", "you", "people");
}
The above results in the following output:
Hallo
you
people
terminate called after throwing an instance of 'std::bad_alloc'
what(): std::bad_alloc
Aborted (core dumped)
The first function thus works but the second doesn't. Am I correct to assume that it's because the variadic macro doesn't handle non-primitive types? Can you make it handle non-porimitive types?
Upvotes: 2
Views: 1136
Reputation: 70472
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-definedAs 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].
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.
Upvotes: 3
Reputation: 2177
If you call test2 as such, it should work:
test2(3, std::string("Hallo"), std::string("you"), std::string("people"));
What's happening here is that when you pass "Hallo" you're passing a char*, not an std::string, so when you try to retrieve the char* by giving it a type of std::string with va_arg, it fails since the object isn't an std::string.
Also, in some compilers, it seems that you can't pass types that aren't trivially destructable, when I tried this in cpp.sh it gave the error
In function 'void test2(int, ...)':
27:23: error: cannot receive objects of non-trivially-copyable type 'std::string {aka class std::basic_string<char>}' through '...';
In function 'int main()':
36:51: error: cannot pass objects of non-trivially-copyable type 'std::string {aka class std::basic_string<char>}' through '...'
Although it seems this isn't part of the standard as I couldn't find anywhere that required this, so I assume it's just a limitation of the compiler they're using. If anyone knows the details about this, I'll edit the answer with the correct information.
Like Henri Menke said, it seems the problem is that cpp.sh is using an old version that isn't c++11 compliant, so if you use a compiler that is at least c++11 compliant, you can use std::string within variadic arguments.
It appears this is an implementation detail, as miradulo said, so it will depend on your compiler, and not the standard version you're using.
And also, like Henri Menke mentioned, if you use
using std::string_literals;
And then put an 's' at the end of the normal string as such:
test2(3, "Hallo"s, "you"s, "people"s);
It transforms the char* to a std::string so you can use them like this to call test2, this is due to string literals.
Upvotes: -1
Reputation: 179981
The answer to the first part is that you're mostly correct. There are restrictions on the use of class types, in particular if they have a non-trivial copy constructor (which std::string
has). It might work on some implementations and not on others. Formally, that's conditionally-supported with implementation-defined semantics.
The second part is a bit harder, but also a bit simpler. These varargs are old C, and not typesafe. C++ does have typesafe variadics, via templates:
template<typename... T>
void test1(T... args)
{
std::cout << ... << args << std::endl;
}
The problem is that you need to know how to unpack those args...
- they're called parameter packs. There are a few different ways to unpack them; this particular form is called a fold expression.
Upvotes: 2