如果复制列表初始化允许显式构造函数会出现啥问题?

Posted

技术标签:

【中文标题】如果复制列表初始化允许显式构造函数会出现啥问题?【英文标题】:What could go wrong if copy-list-initialization allowed explicit constructors?如果复制列表初始化允许显式构造函数会出现什么问题? 【发布时间】:2012-02-27 17:55:17 【问题描述】:

在 C++ 标准 §13.3.1.7 [over.match.list] 中,声明如下:

在复制列表初始化中,如果选择了explicit 构造函数,则初始化格式错误。

这就是为什么我们不能这样做的原因,例如:

struct foo 
    // explicit because it can be called with one argument
    explicit foo(std::string s, int x = 0);
private:
    // ...
;

void f(foo x);

f( "answer", 42 );

(请注意,这里发生的情况不是转换,即使构造函数是“隐式”的,它也不会是一个转换。这是使用它的 foo 对象的初始化直接构造函数。除了std::string,这里没有转换。)

这对我来说似乎很好。隐式转换绝不会咬我。

如果 "answer", 42 可以初始化别的东西,编译器就不会背叛我做错事:

struct bar 
    // explicit because it can be called with one argument
    explicit bar(std::string s, int x = 0);
private:
    // ...
;

void f(foo x);
void f(bar x);

f( "answer", 42 ); // error: ambiguous call

没问题:调用不明确,代码无法编译,我必须明确选择重载。

f(bar  "answer", 42 ); // ok

由于明确规定了禁令,我觉得我在这里遗漏了一些东西。据我所知,列表初始化选择显式构造函数对我来说似乎不是问题:通过使用列表初始化语法,程序员已经表达了进行某种“转换”的愿望。

会出什么问题?我错过了什么?

【问题讨论】:

我不确定,但我认为这很合乎逻辑。调用 f( "answer", 42 ),您可能永远不会知道您传递的是 foo,并且您尝试使用的构造函数是显式的,它强制执行显式转换。 @Geoffroy:如果可以从 "answer", 42 传递其他内容,重载决议将是模棱两可的,因此迫使我明确类型。 我不明白你为什么不认为这种转换是隐式的。 sehe:“如果 f() 有另一个接受初始化列表的重载怎么办?”如果真的发生了怎么办? "answer", 42 不是初始化列表,因为元素的类型不同。因此它不能选择一个带有初始化列表的函数。 好的,但是无论涉及的确切步骤顺序如何,您仍然会在f("a",1); 中隐式创建foo 。你不是明确要求explicit 发生吗? 【参考方案1】:

从概念上讲,复制列表初始化是将复合值转换为目标类型。提出措辞并解释基本原理的论文已经认为“复制列表初始化”中的“复制”一词是不幸的,因为它并没有真正传达其背后的实际基本原理。但保留它是为了与现有措辞兼容。 10, 20 对/元组值不应复制初始化 String(int size, int reserve),因为字符串不是对。

考虑使用显式构造函数,但禁止使用。这在以下情况下是有意义的

struct String 
  explicit String(int size);
  String(char const *value);
;

String s =  0 ;

0 不传达字符串的值。所以这会导致错误,因为考虑了 both 构造函数,但选择了 explicit 构造函数,而不是将 0 视为空指针常量。

不幸的是,这也发生在跨函数的重载决议中

void print(String s);
void print(std::vector<int> numbers);

int main()  print(10); 

由于模棱两可,这也是格式错误的。在 C++11 发布之前,有些人(包括我)认为这很不幸,但没有提出对此进行更改的论文(据我所知)。

【讨论】:

太棒了!最后一个实际的例子出了问题。可悲的是,这意味着这不容易解决:((我想你的意思是最后一个例子中的void f(int i),对吧?) @RMartin 不,我的意思是“Int”。如果它是“int”,那么它将是完全匹配的,并且将被选择而不是其他用户定义的转换。 “Int”是一个简单的包装器,就像著名的 SafeInt 类一样。 啊,我明白了。只有真正真正适用的构造函数:Int 的构造函数。但由于无论如何都考虑了来自Stringexplicit,所以这个调用是模棱两可的。谢谢。 也许添加一个非常简短的Int 的单行定义以使最后一个示例更清晰。像 Martinho 一样,我是在阅读 cmets 后才知道的(这不是模棱两可,但如果你不期待它,它并不完全清楚)。 @konrad 谢谢,也许最好用 std::vector 替换它【参考方案2】:

难道不是因为“显式”是为了停止隐式转换,而您是在要求它进行隐式转换吗?

如果您使用单参数构造函数指定了结构,您会问这个问题吗?

【讨论】:

你的意思是像f( 42 )这样的东西?它有什么问题?另外我的代码中没有演员表【参考方案3】:

此声明:

在复制列表初始化中,如果选择了explicit 构造函数,则初始化格式错误。

意味着很多东西。其中,它意味着它必须查看显式构造函数。毕竟,如果它看不到它,它就无法选择显式构造函数。当它寻找将大括号列表转换成的候选时,它必须从所有候选中进行选择。甚至那些以后会被发现是非法的。

如果重载解决方案导致多个函数同样可行,那么它会导致需要手动用户干预的模棱两可的调用。

【讨论】:

【参考方案4】:

据我了解,关键字 explicit 的真正目的是拒绝使用此构造函数进行隐式转换。

所以你问为什么显式构造函数不能用于隐式转换?显然是因为该构造函数的作者通过使用关键字 explicit 明确拒绝了它。您发布的标准中的引用只是指出 explicit 关键字也适用于初始化列表(不仅仅是某种类型的简单值)。

添加:

更准确地说:关键字 explicit 与一些构造函数一起使用的目的是绝对清楚地表明该构造函数在某个地方使用(即强制所有代码显式调用该构造函数) .

f是函数名时,像f(a,b)这样的IMO语句与显式构造函数调用无关。绝对不清楚(和上下文相关)这里使用了哪个构造函数(以及什么类型),例如这取决于存在的函数重载。

另一方面,像f(SomeType(a,b)) 这样的东西完全不同——很明显,我们使用SomeType 类型的构造函数,它接受两个参数a,b,并且我们使用函数f 重载,它将最好接受SomeType 类型的单个参数。

因此,有些构造函数可以像 f(a,b) 这样隐式使用,而其他构造函数则要求读者绝对清楚它们的使用事实,这就是我们将它们声明为 explicit 的原因。

添加2:

我的观点是:有时即使没有任何问题,显式声明构造函数也是绝对有意义的。 IMO 构造函数是否显式更多的是其逻辑问题,而不是任何类型的警告。

例如

double x = 2; // looks absolutely natural
std::complex<double> x1 = 3;  // also looks absolutely natural
std::complex<double> x2 =  5, 1 ;  // also looks absolutely natural

但是

std::vector< std::set<std::string> >  seq1 = 7; // looks like nonsense
std::string str = some_allocator; // also looks stupid

【讨论】:

代码不涉及转换(std::string除外)。 @R.马蒂尼奥费尔南德斯:嗯。好的。如果更好的话,我会使用 cast 这个词。 IMO 它非常类似于用另一种类型的值初始化一种类型的变量,这正是隐式转换的含义(并且它同时直接使用构造函数)。 这也不是演员表。这与隐式转换之间的区别在于,隐式转换无需您为其编写 any 代码即可发生。为此,您编写代码来初始化一个对象 ( )。当您查看代码时,很明显正在发生一些事情。 @R. Martinho Fernandes:“当您查看代码时,很明显正在发生一些事情。”是的。 隐含的事情正在发生。 explicit 关键字意味着你必须explicitly 使用构造函数。在这种情况下,您不会只看到 某事 正在发生 - 您会看到到底发生了什么(即,正是使用了这个特定的收缩器)。

以上是关于如果复制列表初始化允许显式构造函数会出现啥问题?的主要内容,如果未能解决你的问题,请参考以下文章

9——对象的创建和撤销,构造函数和析构函数

C++中派生类的构造函数怎么显式调用基类构造函数?

构造函数拷贝构造函数析构函数

python 调用父类方法, 重写父类构造方法, 不显式调用,会报错

构造函数和析构函数

显式默认构造函数