显式转换函数、直接初始化和转换构造函数

3

在后标准草案n3376中,将显式转换函数用于用户定义类型的示例列举于12.3.2:2节中:

class Y { };
struct Z {
  explicit operator Y() const;
};
void h(Z z) {
  Y y1(z); // OK: direct-initialization
}

根据12.3.2节的规定,显式转换函数仅被视为直接初始化的用户定义转换;然而,这似乎允许以下情况:
struct Y { Y(int); };
struct Z {
  explicit operator int() const;
};
void h(Z z) {
  Y y1(z); // direct-initialization
}

这似乎与标准的意图相冲突,事实上,它被gcc-4.7.1拒绝:

source.cpp: In function 'void h(Z)':
source.cpp:4:9: error: no matching function for call to 'Y::Y(Z&)'
source.cpp:4:9: note: candidates are:
source.cpp:1:12: note: Y::Y(int)
source.cpp:1:12: note:   no known conversion for argument 1 from 'Z' to 'int'
source.cpp:1:8: note: constexpr Y::Y(const Y&)
source.cpp:1:8: note:   no known conversion for argument 1 from 'Z' to 'const Y&'
source.cpp:1:8: note: constexpr Y::Y(Y&&)
source.cpp:1:8: note:   no known conversion for argument 1 from 'Z' to 'Y&&'

gcc是否正确地拒绝了通过int将Z转换为Y的转换,还是标准确实允许这种用法?我考虑了所提到的直接初始化的上下文。根据8.5:16中对于类类型的直接初始化的定义,构造函数以初始化表达式作为其参数被调用,因此通过隐式转换序列(13.3.3.1)将其转换为参数类型。由于隐式转换序列是一种隐式转换(4:3),因此模拟复制初始化(8.5:14)而不是直接初始化,12.3.2:2中的语言必须指的是整个表达式。还要注意的是,这不违反12.3:4(多个用户定义的转换);同样的编译器在去掉explicit后仍然可以处理相同的代码(Clang和Comeau也是如此)。
struct Y { Y(int); };
struct Z { operator int(); };
void h(Z z) {
  Y y1(z); // direct-initialization
}

我认为Jesse Good在13.3.1.4:1中已经指出了operator Yoperator int之间的区别,但是还有第三种情况仍然让我担忧:

struct X {};
struct Y { Y(const X &); };
struct Z {
  explicit operator X() const;
};
void h(Z z) {
  Y y1(z); // direct-initialization via class-type X
}

对于Y的构造函数的单个const X&参数绑定到临时X的初始化,按照13.3.1.4:1进行直接初始化上下文,其中TX,而SZ。我认为此条款是错误的,应该更改为:

13.3.1.4类通过用户定义的转换进行复制初始化[over.match.copy]

1 - [...]当初始化一个临时变量以绑定到作为其第一个参数的可能带有cv-qualified T的引用的构造函数时,在"cv2 T"类型对象的直接初始化上下文中使用单个参数调用时,同时还会考虑显式转换函数。[...]

为了避免混淆,我认为还应修改12.3.2:2:

12.3.2转换函数[class.conv.fct]

2 - 转换函数可以是显式的(7.1.2),在这种情况下,它仅在某些上下文(13.3.1.4、13.3.1.5、13.3.1.6)中被视为直接初始化(8.5)的用户定义转换。[...]

对以上内容有什么评论吗?


1
如果你认为措辞可以改进,我实际上会建议参加其中一个C++讨论组。 - Luc Danton
@LucDanton 我想说我没有时间参加讨论组,但显然不是这样。 - ecatmur
3个回答

4
根据8.5和13.3.1.3,将考虑Y的构造函数,并通过重载决议选择最佳构造函数。在这种情况下,相关的构造函数是Y(int)以及复制和移动构造函数。在重载解析的过程中,13.3.2可行函数[over.match.viable]指定如下:
“第二,对于F成为一个可行函数,必须存在一种隐式转换序列(13.3.3.1),将该参数转换为调用F的相应参数。[...]”
对于所有这些构造函数,从ZintY的任何风味都没有这样的转换。让我们调查一下标准在13.3.3.1隐式转换序列[over.best.ics]中对隐式转换序列的定义:
“1个隐式转换序列是一系列用于将函数调用中的参数转换为所调用函数的相应参数类型的转换。转换序列是一个隐式转换,如第4条中所定义的,这意味着它受一个表达式初始化对象或引用的规则的限制(8.5、8.5.3)。”
如果我们交叉引用第4条文档,则可以了解隐式转换是以复制初始化的方式定义的(即T t=e;,其中Tintez):
“(§4.3)如果对于某个虚构的临时变量t(8.5),声明T t=e;是良好形式化的,则表达式e可以隐式转换为类型T。”
因此,我认为12.3.2:2不适用于this初始化,这发生在直接初始化的更大上下文中。否则,将与最新段落相矛盾。

13.3.1.5 是关于非类类型对象的初始化,但这里 y1Y 类型,属于类类型,所以我认为不适用。同样地,在你提到的 8.5:16 下面的项目符号是指目标是类类型的情况下的“否则”情形。 - ecatmur
1
@ecatmur 当你失败时,再试一次。现在怎么样? - Luc Danton
总的来说,我认为弄清楚标准实际上要求什么有点令人眼花缭乱,因为第8条款中的段落引用了第13条款中的段落,而这些段落又引用了第12条款中的段落,最后再回到第8条款。然而,我认为12.3.2:2完全是多余的,可以改为非规范性的,具有相同的效果。跟踪和交叉参考是有用的,但是在涉及“显式”的影响时,8.5和13.3.*才是关键。 - Luc Danton
无疑,12.3.2:2是一个转移注意力的问题,或仅适用于非类转换运算符(即第13.3.1.5条)。Jesse Good发现了关于构造函数第一个参数的复制初始化的重要条款,我认为这应该解决了大部分问题。 - ecatmur
@ecatmur 在这段代码片段中没有任何类类型被复制初始化。但我不认为我理解了,“构造函数第一个参数的复制初始化”这句话,我无法完全解析它。 - Luc Danton
显示剩余3条评论

3
正如Luc Danton的回答中所述,隐式转换是以复制初始化的方式定义的。因此,如果我们查看13.3.1.4:1[通过用户定义的转换进行类的复制初始化]:
引用: 当初始化表达式的类型为“cv S”的类类型时,“S的非显式转换函数”及其基类将被考虑。当初始化一个临时对象以绑定到以可能带有cv限定符T作为其第一个参数的引用的构造函数的第一个参数,并在直接初始化的上下文中使用单个参数调用该构造函数时,也会考虑显式转换函数。那些没有在S中隐藏并产生其cv未限定版本为“T类型或其派生类”的类型的候选函数。返回“X的引用”的转换函数返回类型为X的左值或右值引用,具体取决于引用类型,因此被认为是选择候选函数的过程中的X。
如果我理解正确,第一个例子有效是因为转换函数产生了一个Y,因此是一个候选函数,正如引文中第二个强调部分所指出的那样;然而,在你的第二个例子中,候选函数集为空,因为没有将Y转换的转换函数,也没有非显式转换函数,正如引文中第一个强调部分所指出的那样。
关于第三种情况:
在找到defect report 1087之后,似乎清楚的意图是允许在直接初始化cv2 T对象时使用复制、移动和模板构造函数,就像你提到的那样。然而,(在阅读后),似乎由于缺陷报告导致措辞变得模糊,不涵盖你提出的第三种情况。

这里的复制初始化是为 Y 的构造函数的第一个参数而设计的;语言上说“[...]显式转换函数也被考虑在内”是允许第一种情况(其中 S 是 Z,T 是 const Y &)的关键。在第二种情况下,这个条款不适用,因为正在初始化的对象是 Y 构造函数的 int 参数,所以适用于 13.3.1.5。但是这里有一个问题;如果 T 是另一个类 X 呢? - ecatmur
@ecatmur:请参阅缺陷报告1087。他们改变了措辞以允许移动构造函数和模板构造函数,但同时也使其更加模糊,并且它并不涵盖您提出的情况。 - Jesse Good
尽管我再次阅读了一遍,这句话:“假设“cv1 T”是正在初始化的对象类型”,似乎排除了你的第三种情况。 - Jesse Good
不幸的是,“正在初始化的对象”是Y构造函数的“绑定到第一个参数的临时变量”,在第三种情况下仍然是X。您绝对正确,缺陷报告1087就是这种歧义产生的地方。 - ecatmur
@ecatmur:啊,我再读这段话几次之后,我同意它没有涵盖你提出的情况。你介意提交一个缺陷报告吗? - Jesse Good
谢谢,我已经在comp.std.c++上发布了DR。 - ecatmur

2

我并非语言专家,但是标准的措辞让我认为将转换运算符标记为explicit需要您明确指定转换类型(例如int)作为对象y1初始化的一部分。使用代码Y y1(z),似乎您正在依赖隐式转换,因为您为变量y1指定的类型是Y

因此,在这种情况下,我期望正确使用显式转换运算符:

Y y1( int(z) ); 

或者,由于您实际上正在指定一个转换,最好

Y y1( static_cast<int> (z) ); 

你已经使我相信了标准的意图;由于我们在进行语言法律方面的讨论,我们只需要弄清楚如何根据其意图阅读标准。 - ecatmur

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接