std::strings的std::initializer_list的奇怪行为

8

这个问题可能已经被问过了,但我没有找到答案。

下面的代码可以通过gcc编译,但在运行时会崩溃,出现std::length_error异常 (实例)。

void test(const std::string &value) { std::cout << "string overload: " << value << std::endl; }

//void test(const std::vector<std::string> &) { std::cout << "vector overload" << std::endl; }

int main()
{
    test({"one", "two"});
}

从字符串初始化器列表创建字符串的能力似乎是有争议的,例如,无法创建上面代码中被注释掉的重载。即使允许这样的构造方式,为什么它会导致失败呢?


initializer_list 版本的 std::string 仅适用于字符列表,而不是字符串列表。对于字符串列表,您将获得对象的标准列表初始化。当不含糊时,注释重载是可以的。例如,如果列表有两个以上的元素。 - sklott
注意(因为这不是主要问题):问题在于“one”和“two”不是std :: string。您可以执行test({{“one”},{“two”}});或使用C ++ 17字符串字面量test({“one”s,“two”s});(使用using namespace std :: literals;)。任何一个都可以(https://godbolt.org/z/F91SWI)。 - Max Langhof
@Max Langhof,谢谢! - Yuriy
2个回答

11

它被称为

string(const char* b, const char* e) 

string 构造函数重载。

仅在 be 指向相同的字符串字面值时有效。否则是未定义行为。


1
我在 cppreference 找不到这个重载。它是做什么的? - Yksisarvinen
3
这是一段C++代码,用于定义basic_string类的构造函数。它接受两个迭代器参数,表示一个范围内的元素将被用来初始化字符串对象。可选的allocator参数可以指定用于分配内存的分配器对象。 - rafix07
我最初在创建字符串和向量的两个重载时遇到了问题(在问题代码中有注释)。这导致了“模棱两可的调用”错误。因此,“实现使用语法test({“one”,“two”})进行向量重载的选择”是一个关于如何使用语法test({“one”,“two”})进行向量重载的问题。 - Yuriy
那个重载应该是显式的。 - Yakk - Adam Nevraumont
@Scheff 看这个字符串池利用漏洞:http://coliru.stacked-crooked.com/a/8d20450bba0425e0 - Yakk - Adam Nevraumont
显示剩余4条评论

6

首先,没有使用接受初始化列表的构造函数,因为这样的构造函数看起来像:

basic_string(initializer_list<charT>, const Allocator& = Allocator());
                              ^^^^^

因此,编译器会搜索另一个适当的构造函数,并找到了这样一个构造函数。它就是构造函数。

template<class InputIterator>
basic_string(InputIterator begin, InputIterator end, const Allocator& a = Allocator());

表达式"one""two"被视为类型为const char *的迭代器。

因此,函数test具有未定义的行为。

例如,您可以编写以下代码(前提是相同内容的字符串文字在内存中作为一个字符串文字存储,但这不能保证,并取决于所选的编译器选项)。

#include <iostream>
#include <string>

void test(const std::string &value) { std::cout << "string overload: " << value << std::endl; }

//void test(const std::vector<std::string> &) { std::cout << "vector overload" << std::endl; }

int main()
{
    test({ "one", "one" + 3 });
}

而且您将获得一个有效的结果。

string overload: one

请注意,这个结构是关于IT技术的。
{ "one", "two" }

不是std :: initializer_list <T>类型的对象。这个结构没有类型。它是一个braced-init-list,用作初始化器。简单来说,编译器会首先尝试使用第一个参数为std :: initializer_list类型的构造函数与此初始化程序一起使用。

例如,如果您使用std :: vector<const char *>类,那么编译器确实将使用其带有std :: initializer_list的构造函数,并相应地使用这个大括号初始化程序来初始化其参数。例如:

#include <iostream>
#include <vector>

int main()
{
    std::vector<const char *> v( { "one", "two" } );

    for ( const auto &s : v ) std::cout << s << ' ';
    std::cout << '\n';
}

4
这是否依赖于编译器将两个 "one" 编译为引用相同地址的事实?这在标准中得到了保证吗?答:是的,这确实依赖于编译器将两个 "one" 编译为引用相同地址的事实。同时,C++ 标准也保证了字符串字面值在编译时会被分配到唯一的地址上。 - Scheff's Cat
抱歉,在我还没来得及读完之前,这个引起了我的注意。;-) 我自己也太过于多疑,不能依赖那个... - Scheff's Cat
@LightnessRacesinOrbit 这取决于编译器选项。通常,您可以使用编译器选项选择编译器相对于字符串字面值的行为。 - Vlad from Moscow
2
就像GCC中的-fno-merge-constants一样。 - Lightness Races in Orbit

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