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

Implicit conversion between two classes based on integral type

我有一个 class A 为整数类型提供构造函数和 class B 提供隐式转换的情况同一整数类型的运算符。但是,如果我使用 class B 的实例调用接受对 class A 的引用的函数,编译将失败。我本来期望 class B 隐式转换为 class A 的构造函数接受的类型。当然,如果我在 A 中添加一个接受 class 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
}

Is this behavior intended?

是的,是有意的。

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

标准说(强调我的):

[over.ics.user]

A user-defined conversion sequence consists of an initial standard conversion sequence followed by a user- defined conversion (15.3) followed by a second standard conversion sequence. ...


为了使用户定义类型 (a class) 可以隐式转换为另一种类型,必须有直接指向该类型的构造函数或转换运算符。无法通过中间类型进行隐式转换(从用户定义的类型到另一种类型)。

您可以改用显式转换。

隐式创建对象时,您只能进行一次用户定义的转换。由于 func 需要一个 A,您将有一个用户定义的转换将 B 转换为 std::uint8_t,然后另一个用户定义的转换将 std::uint8_t 转换为一个A。如果您希望它隐式发生,您需要的是 B 中的 operator AA 中采用 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

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

这是因为这样的 "long-distance" 转换会产生极其令人惊讶的结果。

如果您希望能够从任何可以转换为 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)) )
{}

或者您可以在要转换为 A.

时将 B 转换为 uint8_t

您可以在 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;相反,它只是阻止此模板在重载决议中被考虑。