Reputation: 1724
I've this code that plays with variadic functions:
#include <cstdarg>
#include <iostream>
template <typename T>
void bar(va_list vl) {
std::cout << va_arg(vl, T) << std::endl;
}
void foo(int n, ...) {
va_list vl;
va_start(vl, n);
while (n--) {
bar<int>(vl);
}
va_end(vl);
}
int main() {
foo(3, 1, 2, 3);
return 0;
}
Unfortunately, the output of this code is platform dependent. In GCC 7 I get:
1
2
3
while in MSVC 2015 is
1
1
1
I'm tring to write code to get the GCC output, but I suppose I'm doing something wrong and I've no idea where. Which is the correct output, if any?
Both compilers are set at maximum warning level, but they are not printing any warning.
In MSVC, being va_list
an alias to char*
, the output makes sense. In GCC it is a builtin type and I've no idea what happens there.
Edit:
If I change the definition of bar
to void bar(va_list& vl)
then the outputs are the same. Is it legal to pass a va_list
by reference?
Upvotes: 0
Views: 1197
Reputation: 1
The issue with the original code (which will happen only on some platforms, depending on the particular implementation of va_list
) is
that when va_arg
is performed on vl
here:
template <typename T>
void bar(va_list vl) {
std::cout << va_arg(vl, T) << std::endl;
}
the changes are not reflected in 'vl' here:
void foo(int n, ...) {
va_list vl;
va_start(vl, n);
while (n--) {
bar<int>(vl);
}
va_end(vl);
}
There are even more issues on some platforms with passing va_list as pointer (C/C++) or reference(for C++).
The workaround that works well is to wrap va_list into a struct:
/** @brief the cross-platform way to pass va_list back and forth in functions */
struct va_list_container {
va_list args;
};
and pass the pointer to that struct to bar.
I've got the following result with the original code on Macos:
make 1
c++ 1.cpp -o 1
Workspace % ./1
1
1
1
but when I made the following changes:
Workspace % cat 1.cpp
#include <cstdarg>
#include <iostream>
struct va_list_container {
va_list args;
};
template <typename T>
void bar(struct va_list_container *p_vlc) {
std::cout << va_arg(p_vlc->args, T) << std::endl;
}
void foo(int n, ...) {
struct va_list_container vlc;
va_start(vlc.args, n);
while (n--) {
bar<int>(&vlc);
}
va_end(vlc.args);
}
int main() {
foo(3, 1, 2, 3);
return 0;
}
Workspace % make 1
c++ 1.cpp -o 1
Workspace % ./1
1
2
3
PS: The option with reference which also has the same output:
#include <cstdarg>
#include <iostream>
struct va_list_container {
va_list args;
};
template <typename T>
void bar(struct va_list_container & vlc) {
std::cout << va_arg(vlc.args, T) << std::endl;
}
void foo(int n, ...) {
struct va_list_container vlc;
va_start(vlc.args, n);
while (n--) {
bar<int>(vlc);
}
va_end(vlc.args);
}
int main() {
foo(3, 1, 2, 3);
return 0;
}
Upvotes: 0
Reputation: 38161
You should use modern C++11 approach. Using old fashion C variadic function is to dangerous.
You could use C++ template parameter pack, but in your case simple std::initializer_list just do the job and it is quite handy:
void foo(std::initializer_list<int> items) {
for (auto item : items) {
std::cout << item << std::endl;
}
}
int main() {
foo({1, 2, 3});
return 0;
}
Upvotes: 0
Reputation: 141768
Your program invokes undefined behavior.
The semantics of C standard library in C++ is the same as in C and the content of cstdarg should be the same as stdarg.h
in C. C++ standard is big, so I'll use C standard here, which is easier for me to read. From C99 7.15.3, annotation mine:
.... The object ap [of type va_list, my annotation] may be passed as an argument to another function; if that function invokes the va_arg macro with parameter ap, the value of ap in the calling function is indeterminate and shall be passed to the va_end macro prior to any further reference to ap.
If you pass va_list
object to another function and in that function you call va_arg
, you have to call va_end
in the same function.
According to the footnote 221 from C99 you can pass a pointer to va_list
.
Upvotes: 4