Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implicit conversion between two classes based on integral type

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
}
like image 439
Phidelux Avatar asked Mar 06 '23 11:03

Phidelux


2 Answers

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.

like image 86
Yakk - Adam Nevraumont Avatar answered Mar 23 '23 00:03

Yakk - Adam Nevraumont


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
like image 39
NathanOliver Avatar answered Mar 23 '23 00:03

NathanOliver