Ray in NY
Ray in NY

Reputation: 361

User-defined conversions aren't applied to variadic function arguments? Why not?

I have a class that mainly wraps a std::variant with some minor additional functionality/meta-data.

For simplicity of use, I wanted to provide a user-defined conversion of this wrapper class to the underlying variant type, so functions like std::holds_alternative could be called directly on it.

What I've uncovered leaves me very confused about whether and when user-defined conversions will be applied. Here is simplified code.

#include <iostream>
#include <variant>

struct MyClass
{
    // "MyClass" is implicitly convertible to a variant<bool,int>
    operator std::variant <bool, int> ()
    {
        return std::variant<bool,int>(5);
    }
};

void thisFunctionTakesOneSpecificVariantType (std::variant<bool,int> v)
{
    std::cout << v.index();    
}

template <class... types>
void thisFunctionTakesAnyVariantType (std::variant<types...> v)
{
    std::cout << v.index();
}

int main ()
{
    MyClass mc;

     // 1. This compiles and runs as expected, 
     //    proving the user-defined conversion (MyClass -> variant<int,bool>) exists and works "sometimes"
     thisFunctionTakesOneSpecificVariantType (mc);

     // 2. This compiles and runs as expected,
     //    proving "thisFunctionTakesAnyVariantType" is well defined 
     thisFunctionTakesAnyVariantType (std::variant <bool, int> (5)); 

     // 3. But, combining 1 & 2, this fails to compile:
     /* fails */ thisFunctionTakesAnyVariantType (mc);  // error: no matching function for call to 'thisFunctionTakesAnyVariantType'

     // 4. This is what i really want to do, and it also fails to compile
     /* fails */ std::holds_alternative<int>(mc);       // error: no matching function for call to 'holds_alternative'

     // 5. An explicit conversion works for 3 and 4, but why not an implicit conversion?
     //      After all, the implicit conversion worked in #1
     thisFunctionTakesAnyVariantType ( static_cast<std::variant <bool, int>> (mc) );

   return EXIT_SUCCESS;
}

Why don't use cases 3 and 4 compile, while 1, 2, and 5 do?

In the error messages, it provides this note:

note: candidate template ignored: could not match 'variant<type-parameter-0-1...>' against 'MyClass'
    inline constexpr bool holds_alternative(const variant<_Types...>& __v)

Upvotes: 5

Views: 218

Answers (2)

songyuanyao
songyuanyao

Reputation: 172934

Why don't use cases 3 compile

Because implicit conversions are not considered in template argument deduction:

Type deduction does not consider implicit conversions (other than type adjustments listed above): that's the job for overload resolution, which happens later.

The conversion from MyClass to std::variant <bool, int> won't be considered then type deduction fails. As #5 showed you can apply explicit conversion before passing to thisFunctionTakesAnyVariantType.

Why don't use cases 4 compile

Same reason as #3. Note that even you specify some template arguments for the parameter pack template argument deduction still tries to deduce the following template arguments from the function argument. You can use exclude the function parameter from the deduction as

template <class... types>
void thisFunctionTakesAnyVariantType (std::type_identity_t<std::variant<types...>> v)

then you can call it as

thisFunctionTakesAnyVariantType<bool, int>(mc);

But note that this will make all the template argument deduction invalid (and #2 and 5 would fail), so it might be a bad idea.

BTW: std::type_identity is supported since C++20 even it's easy to implement one.

Upvotes: 2

bolov
bolov

Reputation: 75765

That's how the rules are laid out. Someone else with more knowledge then me might come and give you the exact rules at play here (template substitution and conversions considered), but at the end of the day it is what it is and you cannot change that.

In order for the conversion to be considered your class needs to inherit from std::variant<int, bool>. All of your examples will compile. I am however reluctant to recommend this approach as indeed composition seems the right design here.

What I would do is provide a conversion method. You lose the implicit aspect of it, but that maybe it's not such a bad idea, at least considering the alternative.

struct MyClass
{
    std::variant<bool, int> to_var() const
    {
        return {5};
    }
}
thisFunctionTakesAnyVariantType (mc.to_var());
std::holds_alternative<int>(mc.to_var());

You could leave the conversion operator for those situation where it works but consider if it doesn't just add confusion.

Upvotes: 2

Related Questions