基于整数类型的两个类之间的隐式转换

时间:2018-09-11 13:59:41

标签: c++ operator-overloading implicit-conversion

我遇到的情况是,我有一个类A为一个整数类型提供构造函数,而一个类B为同一个整数类型提供了隐式转换运算符。但是,如果我调用一个接受带有类A实例的对类B的引用的函数,则编译将失败。我本来希望将类B隐式转换为类A的构造函数接受的类型。当然,如果我向A添加一个接受类B的构造函数,一切都很好。这是故意行为吗?请查看下面的示例。

#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
}

3 个答案:

答案 0 :(得分:2)

  

此行为是故意的吗?

是的,这是有意的。

隐式转换序列最多可以有一个用户定义的转换(构造函数或转换函数)。

标准说(强调我的意思):

  

[over.ics.user]

     

用户定义的转换顺序由初始标准转换顺序组成,后跟 a 用户-   定义的转换(15.3),然后是第二个标准转换序列。 ...


为使用户定义的类型(一个类)可以隐式转换为另一种类型,必须直接有一个构造函数或转换操作符可以转换为该类型。不能通过中间类型进行隐式转换(从用户定义的类型到另一种)。

您可以改用显式转换。

答案 1 :(得分:2)

在隐式创建对象时,只允许一个用户定义的转换。由于func需要A,因此您需要进行用户定义的转换,以将B转换为std::uint8_t,然后进行另一个用户定义的转换,以将std::uint8_t转换为A。您需要的是operator A中的BA中的构造函数,如果您希望它隐式发生,则需要一个B。否则,您可以明确地进行强制转换,因此只需要一个像

这样的隐式变量即可。
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

答案 2 :(得分:2)

C ++中有一条规则,即任何隐式转换都不会使用两个用户定义的转换。

这是因为这样的“长途”转换可能会导致非常令人惊讶的结果。

如果您希望能够从任何可以转换为uint8_t的内容中进行转换,则可以执行以下操作:

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)) )
{}

或者您可以在要转换为B的位置将uint8_t转换为A

您可以在B中做类似的事情,在此过程中,您可以创建一个神奇的template<class T, /*SFINAE magic*/> operator T并转换为uint8_t可以构造的任何内容。

此模糊代码:

  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

存在以确保仅在我们要从其转换的类型具有所需属性的情况下使用重载。

第一个enable_if子句指出,我们只希望可以转换为uint8_t的事物。第二种状态我们不希望此构造函数用于类型A本身,即使它通过了第一种。

无论何时为类型创建转发引用隐式构造函数,都非常需要第二个子句,否则会遇到其他令人惊讶的问题。

所使用的技术称为SFINAE或“替代失败不是错误”。当推导类型IntType并且那些测试失败时,这些子句中存在替换失败。通常,这将导致错误,但是在评估模板过载时,这不是错误,因为SFINAE;相反,它只是阻止此模板在超载解决方案中被考虑。