我有这段代码:
class MyString
{
public:
operator const char*() const {
return nullptr;
}
};
class YourString
{
public:
YourString() {}
YourString(const char* ptr) {
(void)ptr;
}
YourString& operator=(const char* ptr)
{
return *this;
}
};
int main()
{
MyString mys;
YourString yoursWorks;
yoursWorks = mys;
YourString yoursAlsoWorks(mys);
YourString yoursBreaks = mys;
}
MSVC毫无疑问地接受它。 Clang-CL不接受它:
$ "C:\Program Files\LLVM\msbuild-bin\CL.exe" ..\string_conversion.cpp
..\string_conversion.cpp(32,13): error: no viable conversion from 'MyString' to 'YourString'
YourString yoursBreaks = mys;
^ ~~~
..\string_conversion.cpp(10,7): note: candidate constructor (the implicit copy constructor) not viable: no known conversion from 'MyString' to
'const YourString &' for 1st argument
class YourString
^
..\string_conversion.cpp(10,7): note: candidate constructor (the implicit move constructor) not viable: no known conversion from 'MyString' to
'YourString &&' for 1st argument
class YourString
^
..\string_conversion.cpp(14,2): note: candidate constructor not viable: no known conversion from 'MyString' to 'const char *' for 1st argument
YourString(const char* ptr) {
^
..\string_conversion.cpp(5,2): note: candidate function
operator const char*() const {
^
1 error generated.
GCC也不是:
$ g++.exe -std=gnu++14 ..\string_conversion.cpp
..\string_conversion.cpp: In function 'int main()':
..\string_conversion.cpp:33:27: error: conversion from 'MyString' to non-scalar type 'YourString' requested
YourString yoursBreaks = mys;
^
据我所知,只允许进行一次用户定义的转换。
然而,MSVC是否有理由对待这条线
YourString yoursBreaks = mys;
as
YourString yoursBreaks(mys);
并接受它?转换编译器是允许的吗?根据什么规则允许/不允许?是否有类似的规则?
更新:使用MSVC时,/Za
标志会导致代码无法被接受。
$ "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin\x86_amd64\CL.exe" /Za ..\string_conversion.cpp
string_conversion.cpp
..\string_conversion.cpp(33): error C2440: 'initializing': cannot convert from 'MyString' to 'YourString'
..\string_conversion.cpp(33): note: No user-defined-conversion operator available that can perform this conversion, or the operator cannot be called
答案 0 :(得分:10)
tldr;代码格式不正确,MSVC接受它是错误的。复制初始化与直接初始化不同。外行解释是yoursBreaks
的初始化将涉及两个用户定义的转换(MyString --> const char* --> YourString
),而直接初始化涉及一个用户定义的转换(MyString --> const char*
),并且您被允许最多一个用户定义的转换。强制执行该规则的标准解释是[over.best.ics]不允许在通过转换构造函数从不相关的类类型复制初始化类类型的上下文中进行用户定义的转换。 / p>
达标!做什么:
YourString yoursBreaks = mys;
意思?任何时候我们声明一个变量,那就是某种初始化。在这种情况下,根据[dcl.init]:
初始化发生在{em>大括号或等于初始化或条件(6.4)的
=
形式,以及参数传递,函数返回,抛出异常(15.1),处理异常(15.3)和聚合成员初始化(8.6.1)称为 复制初始化 。
复制初始化是T var = expr;
形式的任何内容。尽管出现=
,但它永远不会调用operator=
。我们总是经历构造函数或转换函数。
具体来说,这种情况:
如果目标类型是(可能是cv限定的)类类型:
- 如果初始化表达式是prvalue,并且源类型的cv-nonqualified版本相同 作为目的地的阶级,[...]
- 否则,如果初始化是直接初始化,或者它是复制初始化的地方 cv-源类型的非限定版本与该类的类相同,或者是类的派生类 目的地,[...]
- 否则(即,对于剩余的复制初始化情况),用户定义的转换序列 可以从源类型转换为目标类型或(当使用转换函数时) 如13.3.1.4中所述列举其派生类,并通过选择最佳类 超载分辨率(13.3)。如果转换无法完成或不明确,则初始化为 不良形成。
我们陷入了最后一颗子弹。让我们跳到13.3.1.4:
- T的转换构造函数(12.3.1)是候选函数 - 当初始化表达式的类型是类类型“ cv
S
”时,将考虑S
及其基类的非显式转换函数。初始化临时时绑定到第一个参数 一个构造函数,其中参数的类型为“引用可能 cv -qualifiedT
”,并且在直接初始化类型对象的上下文中使用单个参数调用构造函数“ cv2T
”, 还考虑了显式转换函数。 那些未隐藏在S
中并且生成其cv非限定版本与T
类型相同或者是其派生类的类型的候选函数。返回“引用X
”的转换函数返回左值或x值,具体取决于类型 因此,X
类型的引用被视为在选择候选函数的过程中产生X
。
第一个要点为我们提供了YourString
的转换构造函数,它们是:
YourString(const char* );
第二颗子弹没有给我们任何东西。 MyString
没有返回YourString
的转换函数或从中派生的类类型。
所以,好的。我们有一个候选构造函数。它可行吗? [over.match]通过以下方式检查可靠性:
然后根据将每个参数与每个可行函数的相应参数匹配所需的隐式转换序列(13.3.3.1)来选择最佳可行函数。
,在[over.best.ics]中:
格式良好的隐式转换序列是以下形式之一:
- 标准转换序列(13.3.3.1.1),
- 用户定义的转换序列(13.3.3.1.2)或
- 省略号转换序列(13.3.3.1.3)。但是,如果目标是
- 构造函数的第一个参数或
- 用户定义的转换函数的隐式对象参数和构造函数或用户定义的转换函数是候选者 - 13.3.1.3,当参数是类复制初始化的第二步中的临时参数时,
- 13.3.1.4 ,13.3.1.5或13.3.1.6(在所有情况下),或
- 13.3.1.7的第二阶段[...]
不考虑用户定义的转换序列。 [注意:这些规则会阻止多个用户定义 在重载决策期间应用转换,从而避免无限递归。 -end note] [例如:struct Y { Y(int); }; struct A { operator int(); }; Y y1 = A(); // error: A::operator int() is not a candidate struct X { }; struct B { operator X(); }; B b; X x({b}); // error: B::operator X() is not a candidate
-end example]
因此即使存在从MyString
到const char*
的转换序列,在这种情况下也不会考虑,因此此构造函数不可行。
由于我们没有其他候选构造函数,因此调用格式不正确。
另一行:
YourString yoursAlsoWorks(mys);
称为直接初始化。我们在之前引用的[dcl.init]块中调用了三个子弹点,其全部内容如下:
列举适用的构造函数(13.3.1.3), 并且通过重载决策(13.3)选择最好的一个。调用所选的构造函数来初始化对象,初始化表达式或表达式列表作为其参数。如果没有构造函数适用,或者重载决策不明确,则初始化是不正确的。
其中13.3.1.3表示构造函数是从:
枚举的用于直接初始化 或者不在复制初始化的上下文中的默认初始化,候选函数是被初始化的对象的类的所有构造函数。
那些构造函数是:
YourString(const char* ) // yours
YourString(YourString const& ) // implicit
YourString(YourString&& ) // implicit
为了检查后两个函数的可行性,我们从复制初始化上下文(根据上面的内容失败)重新执行重载解析。但是对于您的YourString(const char*)
,它很简单,有一个可行的转换函数,从MyString
到const char*
,所以它已被使用。
请注意,此处只有一次转化:MyString --> const char*
。一次转换很好。
答案 1 :(得分:1)
让我们看一下隐式转换的规则found here。有趣的是这个:
只要在不接受该类型的上下文中使用某种类型T1的表达式,但接受某些其他类型T2,就会执行隐式转换;特别是:[...]初始化T2 [...]
类型的新对象时
和
用户定义的转换包含零个或一个非显式单参数构造函数或非显式转换函数调用
案例1
YourString yoursWorks;
yoursWorks = mys;
在第一种情况下,我们需要一个非显式转换函数调用。 YourString::operator=
预计const char*
并获得MyString
。 MyString
为此转换提供了非显式转换函数。
案例2
YourString yoursAlsoWorks(mys);
在第二种情况下,我们再次需要一个非显式转换函数调用。 YourString::YourString
期望const char*
并获得MyString。 MyString
为此转换提供了非显式转换函数。
案例3
YourString yoursBreaks = mys;
第三种情况不同,因为它不是出现的分配副本。与第二种情况相反,yoursBreaks
尚未初始化。您无法在尚未构造的对象上调用赋值运算符operator=
。它实际上是通过复制构造进行的赋值。要将mys
分配给yoursBreaks
,我们需要两者非显式转化函数调用(将mys
转换为const char*
和然后< / em>非显式单参数构造函数(用YourString
构造const char *
。隐式转换只允许一个或另一个。
答案 2 :(得分:0)
首先
YourString yoursWorks;
yoursWorks = mys;
不等于
YourString yoursAlsoWorks(mys);
或
YourString yoursBreaks = (const char*) mys;
第一种方法使用构造函数
YourString() {}
后面是MyString的转换运算符和YourString 赋值运算符。
第二种方法使用构造函数
YourString(const char* ptr) {(void)ptr;}
和MyString的转换运算符。
(这可以通过向构造函数添加trace语句来证明。)
然后,当最后一个语句中缺少(const char *)强制转换时, MSVC将假定它应该隐式添加。 虽然这看起来是一种合理的方法,但它与描述相冲突 在Stroustrup的书“The C ++ Programming Language”第4版:
18.4类型转换 ... 显式,即,转换仅在直接初始化中执行,即,作为不使用的初始化器 a =。 ...