定义了opeator类型转换却无法static_cast?一种只能隐式转换不能显式转换的罕见情形

917 阅读3分钟

本文使用MSVC测试,GCC可能存在非标准行为,在某些情况下会允许病构代码通过编译

struct A {};
struct B {};
struct C
{
	C() {}
	C(A) {}
	C(B) {}
	void CC() {}
};
struct D
{
	operator A()
	{
		return A();
	}
	operator B()
	{
		return B();
	}
	operator C()
	{
		return C();
	}
};
void DCC(D d)
{
	static_cast<C>(d).CC();
}

DCC函数能通过编译吗?

乍一看,d的类型D分明就定义了operator C(),然后调用C的成员函数CC,怎么会不能通过编译呢?然而MSVC报错:

image.png

可以看到,编译器和我们对static_cast的理解是不同的。它并没有将D转换为C之后就结束了,而是还一定要寻找一个C的构造函数。这是因为,C++标准对static_cast描述为:

如果存在从表达式 到目标类型 的隐式转换序列,或者如果针对以表达式 对目标类型 类型的对象或引用所进行的直接初始化的重载决议找到至少一个可行函数,那么 static_cast<目标类型 >(表达式 ) 返回如同以 目标类型 Temp(表达式 ); 所初始化的一个虚构变量 Temp,它可能涉及隐式转换,对目标类型 的构造函数的调用,或对用户定义转换运算符的调用。对于非引用的目标类型,static_cast 纯右值表达式的结果对象是它直接初始化的对象。 (C++17 起)

由于此转换“如同”创建了一个虚构变量,所以构造函数的调用是必须的,无论是用户定义的构造函数还是编译器自动定义的构造函数。由于C没有直接接受D类型的构造函数,重载决议就必须在所有D经过operator转换之后可能适用的构造函数中进行选择——这些选择之间没有优先级高低之分,因此编译器无法做出选择,拒绝编译。由于C风格显式转换通常也会被解释为static_cast,可以说,显式转换总是需要执行构造函数的,但不一定需要执行operator。所以只要不能直接转换构造,就会导致多个operator-构造序列发生重载冲突。

隐式转换则不同。隐式转换不一定需要构造,因此构造函数和operator的地位是平等的。如果同时转换构造和operator,则会发生重载冲突。但是,隐式转换不允许先operator再构造这样的两次转换,所以在本例中反而不会发生重载冲突,而是直接应用唯一可行的operator转换,因为C没有直接接受D的构造函数,必须通过A或B的中介,而这是隐式转换所不允许的。以代码说明:

C DCC(D d)
{
	static_cast<C>(d).CC();
	(C)d;//病构,C风格显式转换会被解释为static_cast
	d.operator C();//良构,显式调用operator,不是类型转换,不涉及重载决议
	return C(d);//病构,显式调用构造函数,重载决议失败
	return d;//良构,隐式转换
	C c = d;//良构,隐式转换
}

当我们希望调用转换目标类型的成员函数时,通常采用的做法是显式转换或构造——因为多数情况下,能够隐式转换的类型都能显式转换构造。但在本例中,显式转换将发生重载冲突,我们只能采用等号初始化隐式转换一个新变量再调用,或者显式调用operator避免类型转换重载决议的问题。