标准库中潜藏着哪些贪心初始化列表的例子?

14
自C++11起,标准库容器和std::string具有接受初始化列表的构造函数。此构造函数优先于其他构造函数(即使在其他“最佳匹配”标准被忽略的情况下,正如@JohannesSchaub-litb在评论中指出的那样)。这导致将所有括号形式()的构造函数转换为它们的花括号版本{}时出现了一些众所周知的陷阱。
#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
#include <string>

void print(std::vector<int> const& v)
{
    std::copy(begin(v), end(v), std::ostream_iterator<int>(std::cout, ","));
    std::cout << "\n";
}

void print(std::string const& s)
{
    std::cout << s << "\n";
}

int main()
{
    // well-known 
    print(std::vector<int>{ 11, 22 });  // 11, 22, not 11 copies of 22
    print(std::vector<int>{ 11 });      // 11,     not 11 copies of 0

    // more surprising
    print(std::string{ 65, 'C' });      // AC,     not 65 copies of 'C'
}

在这个网站上我找不到第三个例子,这件事在Lounge<C++>聊天中被提出(与@rightfold、@Abyx和@JerryCoffin讨论)。有些令人惊讶的是,将取一个计数和一个字符的std::string构造函数转换为使用{}而不是(),会将其含义从字符的n个副本改变为第n个字符(通常来自ASCII表)后跟另一个字符。

这不符合通常的花括号禁止缩小转换规则,因为65是一个常量表达式,可以表示为char,并且在转换回int时将保留其原始值(§8.5.4/7,第4个项目)(感谢@JerryCoffin)。

问题: 在标准库中是否存在更多例子,其中将()样式的构造函数转换为{}样式,会被初始化列表构造函数贪心地匹配?


1
可能任何容器都会有这个问题。 - aaronman
快速搜索标准库只显示了:stringvalarray、所有容器、min/max/minmax、正则表达式、一些随机分布和seed_seq - Kerrek SB
2
抱歉有些迂腐。"其他所有条件相等,..."并不完全正确。即使初始化列表构造函数提供了一个更差的匹配(对于将两个构造函数仅作为具有initlist元素作为参数的两个函数进行比较的“更差”的定义),它也会被选择。其工作方式是其他构造函数被简单地忽略。 - Johannes Schaub - litb
@JohannesSchaub-litb 谢谢,已更新。 - TemplateRex
我甚至不知道为什么在语言中添加了initializer_list<T>;很少有情况下你会使用编译时已知数量的对象来构造容器。在我看来,它所做的一切都是引入一些愚蠢的边缘情况。 - Simple
4
有一个例外,即默认构造函数比“initializer_list”构造函数的优先级更高。因此,“{}”调用默认构造函数,而永远不会调用零长度的“initializer_list”。除非您的类缺少默认构造函数,否则将使用具有零个条目的“initializer_list”构造函数。(如果有多个“initializer_list”构造函数,则会出现歧义错误。) - CTMacUser
2个回答

5
我假设你的示例:std::vector<int>std::string也适用于其他容器,例如std::list<int>std::deque<int>等等,它们与std::vector<int>有同样的问题。同样,int不是唯一的类型,它也适用于charshortlong及其无符号版本(可能还有几种其他整数类型)。
我认为还有std::valarray<T>但我不确定T是否允许为整数类型。实际上,我认为它们有不同的语义:
std::valarray<double>(0.0, 3);
std::valarray<double>{0.0, 3};

还有一些其他标准的C++类模板需要使用std::initializer_list<T>作为参数,但我认为其中没有任何一个在使用括号而不是花括号时会使用重载的构造函数。


2
哦,太好了,std::valarray 的参数顺序是(val, count),而不像所有序列容器一样是(count, val),这使得它更容易出错。 - TemplateRex
2
据我所知,一旦你在源代码中键入std::valarray,就会出错。 - Dietmar Kühl
那个回答说“只是为了好玩”...如果他真的在严肃的代码中使用valarray,而不仅仅是在SO上的12行答案中,我会感到惊讶。不要把SO上的代码误认为是现实世界!;-) - Jonathan Wakely
不论 valarray 本身的优点如何,它也存在一个鸡生蛋的效应:valarray 很难理解,所以使用它可能会导致误解,从而使其更加晦涩。这让我想起了位域的糟糕性能,因为由于它们性能差,没有人使用它们,也就无法修复它们。 - TemplateRex
1
@TemplateRex:将std::set<int>替换为std::deque<int>:我没有验证它的构造函数。关于std::valarray<T>:它们相对较晚才成为标准,最初版本被投票通过后,推动它们的人就消失了。因此,它们的接口没有得到纠正,尽管例如David Vandevoorde试图这样做。[Blitz++](http://sourceforge.net/projects/blitz/)展示了真正需要的东西。然而,没有任何工作在标准上的人有时间和精力来修补它。`std::valarray<T>`足够有用,可以使用,但并不是真正想要的。 - Dietmar Kühl
显示剩余4条评论

4

只需要搜索initializer_list的出现。

  • 所有序列都有像vector一样的构造函数:

    • deque
    • dynarray
    • forward_list
    • list
    • vector
  • valarray

  • basic_string

  • 无序集合,有一个以整数为参数来确定初始桶数的构造函数。

    • unordered_set
    • unordered_multiset

我认为就是这些了。

#include <unordered_set>
#include <iostream>

int main() {
    std::unordered_set<int> f (3);
    std::unordered_set<int> g {3};
    std::cout << f.size() << "/" << g.size() << std::endl; // prints 0/1.
}

谢谢,这很系统化。如果您可以将其扩展到类型和值的条件(以便它们不受缩小禁令的限制),那么它将成为被接受的答案。 - TemplateRex

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