我理解关键字explicit
在有可能产生歧义的情况下使用时的价值,就像我在这里和这里看到的例子一样。
我理解为防止将基本类型隐式转换为对象类型,这是有道理的。
struct point {explicit point(int x, int y = 0);
};
point p2 = 10; // Error: would be OK without explicit
我想了解的是,当我使用自定义数据类型作为构造函数参数时,explicit
带来了什么价值?
struct X {X(int x);}; // a sample custom datatype I am referring to.
struct pointA { pointA(X x);}; // here this looks to me same as
struct pointB {explicit pointB(X x);}; // like here this
int main() {
pointA pa = 10; // fails as expected
return 0;
}
explicit
的重点与参数列表无关,它与正在构造的类型有关,尤其是在有副作用的情况下。 例如,考虑std::ofstream
.
void foo(std::ofstream out_file);
// ...
foo("some_file.txt");
如果没有explicit
,这将尝试打开some_file.txt
并覆盖其内容。 也许这就是你想要的,但这是一个相当大的副作用,在调用点并不明显。 但是,请考虑我们必须做什么,因为相关的std::ofstream
构造函数explicit
。
foo(std::ofstream("some_file.txt"));
它更加清晰,呼叫者并不感到惊讶(或者至少不应该感到惊讶)。
无论正在构造的类型是内置的、来自第三方库的还是你自己编写的:这无关紧要。explicit
只要你不希望在没有某人非常明确的情况下构造一个类型(因此是关键字),这是他们的意图,这很有用。
将自定义数据类型用作构造函数参数时explicit
带来的值与将基本类型用作参数时的值相同。
这真的应该是答案的结尾,因为试图区分这些情况是在发明一种不存在的复杂性。然而,OP不接受这一点,并拒绝澄清为什么这甚至是一个问题。因此,为了给出这个答案内容,我将说明为什么这个问题未能证明区别。然后,我将回到名义上的问题,并演示explicit
具有价值的一种方式,无论构造函数的参数类型如何。
隐式转换序列
该问题显示了从int
构造pointA
对象失败的情况,但此失败与转换是显式还是隐式无关。它不能,因为pointA
和X
都有隐式构造函数。explicit
说明符无法发挥作用。问题中的第二个代码块在问题的上下文中没有值。与其妄下结论说explicit
没有价值,OP应该质疑转换失败的原因。OP问了错误的问题,但我们被困住了。
问题中的代码演示的是,隐式转换最多可以使用一个用户定义的转换。将int
转换为X
是一种用户定义的转换。将X
转换为pointA
是第二次用户定义的转换。不能在单个隐式转换中同时使用两者。因此,问题的失败pointA pa = 10;
.
更合适的代码示例
要获得与问题中的第一个代码块相当的情况,应该使用X
类型的值而不是int
类型的值进行初始化。由于第一个代码块使用文字,让我们从这种情况开始。(这比内置情况稍微复杂一些,因为内置类型具有内置文本,而自定义类型没有。我们需要一个用户定义的文字。
// X, pointA, and pointB defined as in the question.
// User-defined literal X
X operator ""_X (unsigned long long i) { return static_cast<int>(i); }
// Now use an X literal in the sample code instead of an int literal.
int main()
{
pointA pa = 10_X; // OK
//pointB pb = 10_X; // error: conversion from 'X' to non-scalar type 'pointB' requested
}
这里我们只使用一个用户定义的转换,所以只要用户定义的转换不explicit
,隐式转换是可能的。pointB
的构造函数采用X
参数被标记为explicit
,因此该行无法编译。相反,pointA
行成功,就像文本是基本类型的情况一样。这里没有区别,尽管OP试图发明一个。
如果您不喜欢用户定义的文本(或者即使您不喜欢),也可以通过显式调用用户定义的转换来获得等效的结果X
,如
pointA pa = X{10}; // Succeeds
//pointB pb = X{10}; // Fails
同样,可以看到标记一个构造函数explicit
的效果。
利用explicit
是时候返回所问的名义问题了。explicit
说明符的值是多少?一个好处是避免昂贵的结构。如果pointA
在X
中制作数据的浅拷贝,而pointB
做深拷贝,则可能有充分的理由避免创建pointB
对象,除非明确要求。同样,参数是基本类型还是用户定义类型并不重要。
该问题使用变量初始化作为此值的一个示例。我发现一个弱示例,因为将point p2 = 10;
更改为更紧凑的point p2{10};
允许初始化。那些采用更紧凑样式的人不会注意到由于将构造函数标记为explicit
而有所不同。这里没有什么explicit
价值。OP选择了一个糟糕的例子来证明explicit
的价值。
演示explicit
值的更好方法之一是利用函数参数和函数重载。考虑一个已重载以接受pointA
或pointB
参数的函数。
void fun(const pointA &) { std::cout << "fun(pointA)n"; }
void fun(const pointB &) { std::cout << "fun(pointB)n"; }
让我们假设pointA
是X
对象的廉价"视图",而pointB
是昂贵的副本。如果我们有一个X
对象用作参数,我们希望将其转换为此函数的pointA
。事实也正是如此。
int main() {
X test{0}; // Assume we have this object from somewhere.
fun(test); // Converts `test` to `pointA` and calls that overload.
// The overload taking a `pointB` is not called.
}
这两个重载都是重载解析中的候选函数。但是,采用pointB
参数的版本是不可行的,因为没有从X
到pointB
的隐式转换。因此,该版本将从考虑范围中删除,只留下采用pointA
参数的版本。
这演示了将构造函数标记为explicit
的一些价值。可以避免昂贵的副本,而无需程序员考虑它。在这种情况下,不仅可以避免潜在的歧义,还可以通过自动选择更有效的重载来避免歧义。
此外,如果每次出现X
都替换为int
,则此示例将同样有效。">基本类型"和"自定义数据类型"之间的区别是OP想象力的虚构。不要买账。