当传递的参数是自定义类型时,显式构造函数带来的值是什么



我理解关键字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对象失败的情况,但此失败与转换是显式还是隐式无关。它不能,因为pointAX都有隐式构造函数。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说明符的值是多少?一个好处是避免昂贵的结构。如果pointAX中制作数据的浅拷贝,而pointB做深拷贝,则可能有充分的理由避免创建pointB对象,除非明确要求。同样,参数是基本类型还是用户定义类型并不重要。

该问题使用变量初始化作为此值的一个示例。我发现一个弱示例,因为将point p2 = 10;更改为更紧凑的point p2{10};允许初始化。那些采用更紧凑样式的人不会注意到由于将构造函数标记为explicit而有所不同。这里没有什么explicit价值。OP选择了一个糟糕的例子来证明explicit的价值。

演示explicit值的更好方法之一是利用函数参数和函数重载。考虑一个已重载以接受pointApointB参数的函数。

void fun(const pointA &) { std::cout << "fun(pointA)n"; }
void fun(const pointB &) { std::cout << "fun(pointB)n"; }

让我们假设pointAX对象的廉价"视图",而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参数的版本是不可行的,因为没有从XpointB的隐式转换。因此,该版本将从考虑范围中删除,只留下采用pointA参数的版本。

这演示了将构造函数标记为explicit的一些价值。可以避免昂贵的副本,而无需程序员考虑它。在这种情况下,不仅可以避免潜在的歧义,还可以通过自动选择更有效的重载来避免歧义。

此外,如果每次出现X都替换为int,则此示例将同样有效。">基本类型"和"自定义数据类型"之间的区别是OP想象力的虚构。不要买账。

最新更新