Search code examples
c++formatoverloadingvariadic-functions

unwanted implicit cast makes overload ambiguous


Consider the following code sample:

void my_print(bool is_red, const char* format, ...){
    va_list args;
    va_start(args, format);
    if(is_red) 
        print_red(format, args);
    else
        print_normal(format, args);
    va_end(args);
}

void my_print(const char* format, ...){
    va_list args;
    va_start(args, format);
    print_normal(format, args);
    va_end(args);
}


int main() {
    my_print((const char*)"Hello %s\n", "World");
    return 42;
}

This code is ambiguous to the compiler and it yields the error:

more than one instance of overloaded function "my_print" matches the argument list: function "my_print(bool is_red, const char *format, ...)" (declared at line 12) function "my_print(const char *format, ...)" (declared at line 22) argument types are: (const char *, const char [6])

From what I understand the parameters I passed can interpreted as either (const char*, ...) where the '...' is just 'const char*' or (bool, const char*, ...) where the '...' is empty.

I would expect the compiler to assume I want to call the one which does not require an implicit cast from 'bool' to 'const char*' (which I do), and hence call the second instance.

How can I clarify this ambiguity to the compiler without changing the syntax of the function call?


Solution

  • You could rename both overloads of my_print to functions named differently and introduce a template names my_print instead, which allows you to add a check, if the type of the first parameter is char const* in an if constexpr to resolve the ambiguity.

    void print_red(char const* format, va_list lst)
    {
        printf("red    :");
        vprintf(format, lst);
    }
    
    void print_normal(char const* format, va_list lst)
    {
        printf("normal :");
        vprintf(format, lst);
    }
    
    void my_print_with_color_impl(bool is_red, const char* format, ...) {
        va_list args;
        va_start(args, format);
        if (is_red)
            print_red(format, args);
        else
            print_normal(format, args);
        va_end(args);
    }
    
    void my_print_impl(const char* format, ...) {
        va_list args;
        va_start(args, format);
        print_normal(format, args);
        va_end(args);
    }
    
    template<class T, class ... Args>
    void my_print(T first, Args ... args)
    {
        if constexpr (std::is_same_v<std::decay_t<T>, char const*>)
        {
            my_print_impl(first, args...);
        }
        else
        {
            my_print_with_color_impl(first, args...);
        }
    }
    
    int main() {
        my_print("Hello %s\n", "World");
        return 42;
    }
    

    Alternatively pre C++17 you could create a template class for printing and partially specialize it based on the first parameter:

    template<class T, class...Args>
    struct Printer
    {
        void operator()(bool is_red, char const* format, ...) const
        {
            va_list args;
            va_start(args, format);
            if (is_red)
                print_red(format, args);
            else
                print_normal(format, args);
            va_end(args);
        }
    };
    
    template<class...Args>
    struct Printer<char const*, Args...>
    {
        void operator()(char const* format, ...) const
        {
            va_list args;
            va_start(args, format);
            print_normal(format, args);
            va_end(args);
        }
    };
    
    template<class T, class ... Args>
    void my_print(T first, Args ... args)
    {
        (Printer<typename std::decay<T>::type, Args...> {})(first, args...);
    }
    
    int main() {
        my_print("Hello %s\n", "World");
        my_print(1, "Hello colorful %s\n", "World");
        return 42;
    }