I have the situation where I have a class A
, that provides a constructor for an integral type, and a class B
that provides a implicit conversion operator for the same integral type. However, if I call a function accepting a reference to class A
with an instance of class B
, the compilation fails. I would have expected an implicit conversion of class B
to the type accepted by the constructor of class A
. Of course, if I add a constructor to A
accepting class B
, everything is fine. Is this behavior intended? Please checkout the example below.
#include <iostream>
class B
{
public:
B() = default;
B(const std::uint8_t &val) : mVal(val) {}
std::uint8_t get() const { return mVal; }
operator std::uint8_t() const { return mVal; }
private:
std::uint8_t mVal;
};
class A
{
public:
A() = default;
A(const std::uint8_t &val) : mVal(val) {}
// No problem if this exists
// A(const B &b) : mVal(b.get()) {}
std::uint8_t get() const { return mVal; }
private:
std::uint8_t mVal;
};
void func(const A &a)
{
std::cout << static_cast<int>(a.get()) << std::endl;
}
int main(int, char*[])
{
std::uint8_t val = 0xCE;
A a(val);
B b(val);
func(val); // fine
func(a); // fine
func(b); // error
}
There is a rule in C++ that no implicit conversion will use two user-defined conversions.
This is because such "long-distance" conversions can result in extremely surprising results.
If you want to be able to convert from anything that can convert to a uint8_t
you can do:
template<class IntLike,
std::enable_if_t<std::is_convertible_v<IntLike, std::uint8_t>, bool> =true,
std::enable_if_t<!std::is_same_v<A, std::decay_t<IntLike>>, bool> =true
>
A( IntLike&& intlike ):A( static_cast<std::uint8_t>(std::forward<IntLike>(intlike)) )
{}
or you could cast your B
to an uint8_t
at the point you want to convert to an A
.
You can do a similar thing in B
where you create a magical template<class T, /*SFINAE magic*/> operator T
that converts to anything that can be constructed by an uint8_t
.
This obscure code:
std::enable_if_t<std::is_convertible_v<IntLike, std::uint8_t>, bool> =true,
std::enable_if_t<!std::is_same_v<A, std::decay_t<IntLike>>, bool> =true
exists to make sure that the overload is only used if the type we are converting from has the properties we want.
The first enable_if
clause states that we only want things that can convert to uint8_t
. The second states we don't want this constructor to be used for the type A
itself, even if it passes the first.
Whenever you create a forwarding reference implicit constructor for a type, that second clause is pretty much needed or you get some other surprising issues.
The technique used is called SFINAE or Substitution Failure Is Not An Error. When a type IntType
is deduced and those tests fail, there is substitution failure in those clauses. Usually this would cause an error, but when evaluating template overloads it is not an error because SFINAE; instead, it just blocks this template from being considered in overload resolution.
You are only allowed one user defined conversion when implicitly creating a object. Since func
needs an A
you would have a user defined conversion to turn B
into a std::uint8_t
and then another user defined conversion to turn that std::uint8_t
into an A
. What you would need is a operator A
in B
or a constructor in A
that takes a B
if you want it to happen implicitly. Otherwise you can just explicitly cast so you only need a single implicit one like
func(static_cast<std::uint8_t>(b)); // force it to a uint8_t
// or
func({b}); // make b the direct initializer for an A which will implicitly cast
// or
func(A{b}); same as #2 above but explicitly sating it
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With