为什么从两个字符串字面量构建一个包含自定义构造函数的结构体向量会导致崩溃?

25
你能猜出这个简单程序的输出吗?
#include <vector>
#include <string>
#include <exception>
#include <iostream>

int main()
{
    try { 
        struct X {
            explicit X(int) {}
            X(std::string) {} // Just to confuse you more...
        };
        std::vector<X>{"a", "b"};
    } catch (std::exception& x) {
        std::cerr << x.what();
    }
}

嗯,我做不到,这让我浪费了一整天的“研究”时间,最后才从一些复杂的实际代码中提取出来(到处都是类型别名,非POD成员的匿名联合体和手动编排的构造函数/析构函数等,只为了那种感觉)。
而且... 我还是看不懂发生了什么! 有人可以给个小提示吗?(希望只是一个盲点。我已经不再专业地使用C++了。)
注意:使用(最新的)MSVC /W4 和 GCC -Wall 进行干净的编译*;两者输出相同(语义上)。 * 即使没有“迷惑读者”的那一行。我觉得我会做噩梦。
(请原谅我尽量不透露太多细节,以免剧透——毕竟,这本来就是显而易见的,对吗?只是对我来说完全相反……)

4
将其更改为vector<X>{"a", "b", "c"};以理解问题。将其更改为vector<X>{X{"a"}, X{"b"}};以修复问题。 - Eljay
4
调试器真的很酷。在这里,你本可以介入并看到程序已经陷入了错误的vector构造函数中。 - user4581301
3
你会喜欢这个... - user4581301
12
"统一初始化"引入了许多潜在问题之一。我个人认为重复使用花括号是一个错误,只会增加问题的复杂性;特别是在聚合初始化方面,它在语义上与非聚合初始化有所不同,但现在无法仅通过语法来区分。 - M.M
2
请注意,stl标签指的是标准模板库(Standard Template Library),这是一个非常有影响力的库,它在1998年形成了C++标准库的思想基础(以及一些初始实现)。很可能您今天并没有使用STL,因此删除该标签是合理的。std可能更合适,但可能仍然过于宽泛,因为这涉及到std库容器。不确定是否有相应的标签。 - user4581301
显示剩余28条评论
1个回答

30
std::vector<X>{"a", "b"};

这将使用接受两个迭代器的构造函数,从类型为const char*的两个迭代器创建一个向量。
template< class InputIt >
constexpr vector( InputIt first, InputIt last,
                  const Allocator& alloc = Allocator() );

构造函数使用范围 `[first, last)` 的内容来构建容器。 只有当 `InputIt` 满足 `LegacyInputIterator`,以避免与重载(3)产生歧义时,该重载才参与重载解析。
constexpr vector( size_type count,
                  const T& value,
                  const Allocator& alloc = Allocator() );

真是倒霉,这两个const char[]的衰变成为了完美的迭代器,满足了LegacyInputIterator的要求。

这些迭代器并不指向一个数组/连续区域,因此程序的行为是未定义的。

底层发生的情况很可能是它试图从第一个const char*到达第二个,并在经过'a'之后的空字符终止符时越界。

一个实际可行的类似构造:

const char* arr = "working";

struct X {
    explicit X(int i) {
        std::cout << static_cast<char>(i); 
    }
};

const char* first = arr;     // begin iterator
const char* last = arr + 7;  // end iterator

std::vector<X>{first, last}; // prints "working"

4
@Sz.: 在std::vector::vector(begin, end)之外,编译器不知道这两个指针需要相关联。而在内部,编译器也不知道它们是否相关联。 - Ben Voigt
2
@Sz. 如果这些字符串被解释为迭代器,它们是指向char的迭代器,这可能触发了您的int构造函数。此时它们不是指向char*的指针。 - Mark Ransom
4
@Sz。不幸的是,const char[]的衰变成为一个完美的迭代器,满足了_LegacyInputIterator_的要求。 - Ted Lyngmo
2
另一个脚注:我认为原始示例中int构造函数的explicit修饰符是另一个有效地混淆我们的红色干扰物:令人惊讶的是,即使是它也无法阻止这些隐式转换的阴谋... - Sz.
2
@Sz。只需添加X(字符)=删除;以免其他人再次陷入同样的错误。 - Aykhan Hagverdili
显示剩余10条评论

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