Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overload resolution between va_list and ellipsis differs based on the literal value. Why?

Tags:

c++

I have two overloaded functions where the only difference is the last argument, in one case being a va_list and in the other case being an ellipsis. I noticed the selected overload is different depending if this last argument is an integer literal 0 or 1.

I made a simplified working example as follows, where I am experimenting with various inputs and found out some other weird scenarios:

#include <iostream>
#include <cstdarg>

void Func(va_list /*arg*/)
{
    std::cout << "void Fun(va_list);" << std::endl;
}

void Func(...)
{
    std::cout << "void Fun(...);" << std::endl;
}

int main()
{
    std::cout << "Func(0);"  << " >> ";
    Func(0);    
    std::cout << "Func(1);"  << " >> ";
    Func(1);
    std::cout << "Func(-1);" << " >> ";
    Func(-1);

    std::cout << "Func('0');"   << " >> ";
    Func('0');    
    std::cout << "Func('\\0');" << " >> ";
    Func('\0');
    
    std::cout << "Func(\"\");"      << " >> ";
    Func("");    
    std::cout << "Func(\"Dummy\");" << " >> ";
    Func("Dummy");
    std::cout << "Func(nullptr);"   << " >> ";
    Func(nullptr);
    
    std::cout << "Func(0*1);"      << " >> ";
    Func(0*1);
    std::cout << "Func(0.0);"      << " >> ";
    Func(0.0);
    
    std::cout <<"int i = 0; Func(i);" << " >> ";
    int i = 0;
    Func(i);
    std::cout <<"i = 1; Func(i);" << " >> ";
    i = 1;
    Func(i);
    
    static const int j = 0, k = 1;
    std::cout << "static const int j = 0; Func(j);" << " >> ";
    Func(j);
    std::cout << "static const int k = 1; Func(k);" << " >> ";
    Func(k);    
}

Compiling this code in cppshell with C++14 gives the following output:

Func(0); >> void Func(va_list);
Func(1); >> void Func(...);
Func(-1); >> void Func(...);
Func('0'); >> void Func(...);
Func('\0'); >> void Func(va_list);
Func(""); >> void Func(...);
Func("Dummy"); >> void Func(...);
Func(nullptr); >> void Func(va_list);
Func(0*1); >> void Func(va_list);
Func(0.0); >> void Func(...);
int i = 0; Func(i); >> void Func(...);
i = 1; Func(i); >> void Func(...);
static const int j = 0; Func(j); >> void Func(...);
static const int k = 1; Func(k); >> void Func(...);

As you can see, some calls resolve to the va_list overload, while most resolve to the ellipsis overload.

I believe this weirdness may come from the fact that va_list is often a char*. In Visual C++, I find it defined in vadefs.h as typedef char* va_list;. However I still do not understand how the overload resolution is working on most cases here.

  • How does overload resolution work between va_list and ellipsis?
  • How could i make a call to Func(0) resolve to the ellipsis overload?
like image 549
GuiBeal Avatar asked Jun 01 '21 17:06

GuiBeal


1 Answers

The ... overload will only be selected if there is no implicit conversion sequence to va_list. In other words, the ellipsis always has the lowest possible priority during overload resolution.

Since the definition of va_list is unspecified, in general the results you observe will not be portable: Func(x) could call either the va_list overload or the ellipsis overload depending on the implementation. The one exception is that Func(x) will always call the va_list overload when x itself has type va_list (possibly const-qualified).

If, in fact, va_list is char* on your system, then a null pointer conversion can occur: in other words, 0 can be implicitly converted to the null pointer of type char*. No other integer value can be implicitly converted to a pointer value.

As of C++14, even '\0' should not be implicitly convertible to a pointer value. The null pointer conversion only applies to an integer literal with value zero. (Although '\0' has integer type, it is not an integer literal.) This particular version of Visual Studio is out of date.

As for your question "How could i make a call to Func(0) resolve to the ellipsis overload?", well, you can't change the rules of overload resolution. What you can do is change the ellipsis overload to a template:

template <class T>
void Func(T x);

This guarantees that the va_list overload will only be called when the argument is va_list (possibly const-qualified) and in all other cases, the template will be called since it will give an exact match. Yet, a problem still remains: you may have some char* variable, which you think of as not being a va_list, and calling Func with it as an argument could call the va_list overload since va_list is char* (on your system). There's no way to prevent this. Maybe you shouldn't be overloading Func at all. You can have two separate functions instead.

like image 147
Brian Bi Avatar answered Nov 11 '22 07:11

Brian Bi