简体   繁体   中英

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

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?

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.

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.

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 .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM