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

43
在C++标准中,第13.3.1.7节[over.match.list]中声明了以下内容:
在复制列表初始化中,如果选择了一个显式构造函数,则初始化是非法的。
这就是为什么我们不能像下面这样做的原因:
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

由于禁止明确说明,我感觉我在这里有所遗漏。就我所看到的,使用列表初始化语法选择显式构造函数似乎并不是什么问题:通过使用列表初始化语法,程序员已经表达了进行某种“转换”的愿望。

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


2
我不确定,但我认为这很合乎逻辑。调用f({“answer”,42}),你可能永远不知道你正在传递一个foo,并且你尝试使用的构造函数是显式的,这强制执行显式转换。 - Geoffroy
1
@Geoffroy:如果从{"answer", 42}可以传递其他内容,则重载决议将是模棱两可的,因此会强制我明确指定类型。 - R. Martinho Fernandes
2
我不明白为什么你不认为这种转换是隐式的。 - Mat
1
sehe: "如果f()有另一个接受初始化列表的重载呢?" 如果有呢?{"answer", 42}不是一个初始化列表,因为元素类型不相同。因此它不能选择接受初始化列表的函数。 - Nicol Bolas
2
好的,但无论涉及哪些确切的步骤,在 f({"a",1}); 中仍然会隐含创建一个 foo。您是否已经明确要求使用 explicit 来防止这种情况发生? - Mat
显示剩余2条评论
4个回答

29

概念上,复制列表初始化是将一个复合值转换为目标类型。提出措辞并解释原理的论文已经认为“拷贝”在“复制列表初始化”中不太恰当,因为它实际上并没有传达背后的真正原理。但是由于兼容性考虑,这个术语保留了下来。一个{10,20}的二元组/元组值不应该能够拷贝初始化一个String(int size, int reserve),因为字符串不是一对。

显式构造函数被认为是被禁止使用的。以下情况下这种做法很有道理

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

String s = { 0 };

0并不表示字符串的值。因此,这会导致错误,因为两个构造函数都被考虑了,但是选择了一个显式构造函数,而不是将0视为null指针常量。

不幸的是,在函数重载决议中,这种情况也会发生。

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

int main() { print({10}); }

由于存在二义性,这个也是不正确的写法。在C++11发布之前,一些人(包括我)认为这很不幸,但是并没有提出文件建议更改(据我所知)。


1
太棒了!终于有一个实际的例子说明了某些事情出了问题。不幸的是,这意味着这个问题不能轻易地被修复 :( (我猜你最后一个例子中的意思是 void f(int i),对吗?) - R. Martinho Fernandes
@RMartin 我的意思是 "Int"。如果是 "int",那么它将是一个精确匹配,并且会优先选择其他用户定义的转换。 "Int" 的含义是一个简单的包装器,就像著名的 SafeInt 类一样。 - Johannes Schaub - litb
啊,我现在明白了。只有一个构造函数是真正适用的:Int 的构造函数。但由于 String 中的 explicit 构造函数也被考虑在内,所以调用是有歧义的。谢谢。 - R. Martinho Fernandes
也许可以添加一个非常简短的一行定义来使最后一个例子更清晰。就像Martinho一样,我只有在阅读评论后才明白它(它不是模棱两可的,但如果您不期望它,它并不完全清晰)。 - Konrad Rudolph
@konrad 谢谢,也许最好还是用 std::vector<int> 替换它。 - Johannes Schaub - litb

2

你的问题是不是因为“explicit”在阻止隐式转换,而你正在要求它进行隐式转换?

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


你的意思是像这样 f({42}) 吗?这个会有什么问题吗?另外我的代码中没有强制类型转换 - R. Martinho Fernandes

2
这个语句的意思很多。其中之一是,它必须“查看”显式构造函数。毕竟,如果不能查看显式构造函数,就无法选择它。当它寻找将花括号列表转换为的候选项时,它必须从所有候选项中进行选择。即使是后来被发现是非法的那些候选项也要选择。
如果重载决议导致多个函数同样可行,则会导致一个模棱两可的调用,需要手动用户干预。

1

据我理解,关键字explicit的目的是使用该构造函数拒绝隐式转换。

所以你想知道为什么显式构造函数不能用于隐式转换?显然,因为该构造函数的作者明确地使用了关键字explicit来拒绝它。你发布的标准引用只是说明explicit关键字也适用于初始化列表(不仅适用于某些类型的简单值)。

补充:

更正确地说,使用某些构造函数时,关键字explicit的目的是使其绝对清晰,即强制所有代码显式调用此构造函数。

在我看来,像f({a,b})这样的语句,当f是函数名称时,与显式构造函数调用无关。这是绝对不清楚的(并且依赖于上下文),即取决于存在的函数重载使用哪个构造函数(以及什么类型)。

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

因此,有些构造函数可以隐式使用,例如 f({a,b}),而其他构造函数则要求读者对其使用的事实非常清楚,这就是为什么我们将它们声明为显式的原因。

补充2:

我的观点是:有时候即使没有任何问题,声明构造函数为显式也是有意义的。在我看来,构造函数是否显式更多地取决于其逻辑而不是任何类型的警告。

例如:

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. Martinho Fernandes
1
@R. Martinho Fernandes:好的,我会用“转换(cast)”这个词,如果有更好的请指教。在我看来,这跟将一个类型的变量使用另一个类型的值进行初始化非常相似,而这正是隐式转换所做的事情(同时直接使用了构造函数)。 - Serge Dundich
这也不是一个转换。与隐式转换的区别在于,隐式转换发生在您没有编写任何代码的情况下。对于此操作,您需要编写代码来初始化一个对象({ })。当你看这段代码时,很明显有些事情正在发生。 - R. Martinho Fernandes
2
@R. Martinho Fernandes:“当你看代码时,很明显有些事情正在发生。”是的。有一些_隐含的_事情正在发生。explicit关键字意味着你必须只_显式地_使用构造函数。在这种情况下,你不仅会看到_某些_事情正在发生——你还会看到确切地发生了什么(即使用了这个特定的构造函数)。 - Serge Dundich

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