Reputation: 361
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
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
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