嵌套列表(字符串的向量列表)初始化失败

18

这段代码:

#include <vector>
#include <string>
#include <iostream>

class MyClass
{
public:
  MyClass(const std::vector<std::vector<std::string>> & v)
  {
    std::cout << "Vector of string vectors size: " << v.size() << "\n";

    for (size_t i = 0; i < v.size(); i++)
      std::cout << "Vector #" << i << " has size " << v[i].size() << "\n";
  }
};

int main()
{
  MyClass({ { "a" } }); // <--- ok
  MyClass({ { "a", "b" } }); // <--- PROBLEM
  MyClass({ { std::string("a"), "b" } }); // <--- ok
  MyClass({ { "a", "b", "c" } }); // <--- ok
  MyClass({ { "a" },{ "c" } }); // <--- ok
  MyClass({ { "a", "b" },{ "c", "d" } }); // <--- ok
}

输出结果如下(Visual Studio 2017):

Vector of string vectors size: 1
Vector #0 has size 1
Vector of string vectors size: 4
Vector #0 has size 97
Vector #1 has size 0
Vector #2 has size 0
Vector #3 has size 0
Vector of string vectors size: 1
Vector #0 has size 2
Vector of string vectors size: 1
Vector #0 has size 3
Vector of string vectors size: 2
Vector #0 has size 1
Vector #1 has size 1
Vector of string vectors size: 2
Vector #0 has size 2
Vector #1 has size 2

所以,除了一个包含两个字符串的向量中只有一个向量的情况外,它在所有情况下都可以正常工作。如果我们从其中一个字符串字面值显式构造std :: string,则也适用于上述情况。如果两者都只是普通的字符串字面值,则编译器似乎会“混淆”,并构造一个包含97个字符串的4项向量。请注意,97是“a”的字符代码。

我猜我的问题是,编译器应该将这种有问题的构造解释为我期望的方式,还是初始化嵌套列表的这种代码是错误的?


3
值得一提的是,g++ 6.4.0 报错:"error: call of overloaded 'MyClass(<brace-enclosed initializer list>)' is ambiguous"。 - molbdnilo
1
我尝试了几个编译器...但它们都返回一个模糊的错误。 - YesThatIsMyName
请注意,void f(MyClass); f({{"a", "b"}}); 不是二义性的,但会调用 MyClass 的复制构造函数... - Holt
3个回答

17

MyClass({{"a", "b"}}) 中的内部向量是使用范围构造函数创建的:

template <class InputIterator>
  vector (InputIterator first, InputIterator last, const allocator_type& alloc = allocator_type());

发生这种情况是因为{ "a", "b" }被解释为一对原始指针,而不是std::initializer_list<std::string>


8

进入调试器,查看有问题的构造函数,发现VC++选择了接受两个迭代器(在这种情况下是const char*)作为参数的vector<vector<int>>构造函数。
也就是说,它将构造函数看作是

std::vector<std::vector<std::string>> {"a", "b"}

当两个指针不属于同一数组时,这当然会导致未定义的行为。
另外需要注意的是,g++ 编译了以下两个:
std::vector<std::vector<std::string>> as{{"a", "b"}};
std::vector<std::vector<std::string>> bs{"a", "b"};

但是在后一种情况下会崩溃,而前一种情况表现正常。 VC++编译器按照你所期望的方式编译双括号变量构造,因此我怀疑(希望)这是VC++的一个bug。

这是VC正确的罕见情况。由于UB被调用,VC可以输出完全符合C++20标准的VC2020源代码,并且仍然是正确的。 - YSC
@YSC 行为是未定义的,但编译器不应该像clang和g++那样对构造函数发出模棱两可的调用吗? - Holt
感谢您的回复。请注意,如果我只是像这样构建一个向量,std::vector<std::vectorstd::string> v2({ { "a", "b" } });(就像您的答案一样),它可以正常工作,您将获得一个包含两个字符串的向量的向量。问题仅在使用此初始化列表构造另一个类时才会发生。 - Urmas Rahu
@Holt,那不是我的意图。我认为UB的定义本身就回答了你的问题。一旦达到UB,其他任何事情都无关紧要。显然,在UB之后的代码是未定义的,但在它之前的代码也是有毒的。标准没有强制要求,因此即使程序因其他问题而不合法,编译器也不需要发出错误或警告。 - YSC
2
@YSC 我同意,但必须涉及UB,并且在进行过载解析并选择了UB构造函数之前,不涉及UB代码(否则,大多数C++代码都将是UB)。对我来说,现在不清楚1)MSVC是否正确(即,UB构造函数是更好的匹配),在这种情况下代码是UB,2)clang和g++是否正确,调用是模棱两可的,3)没有一个是正确的,应该调用第一个构造函数。 - Holt
显示剩余3条评论

0

我已经找到了一个解决方法,可以避免使用VC++时出现的未定义行为。您可以定义第二个构造函数,如下所示:

MyClass(const std::vector<std::vector<int>> &)
{
}

接下来是可能引起问题的代码行:

MyClass({ { "a", "b" } }); // <--- PROBLEM

代码将不再编译并会给出“构造函数重载决议不明确”的错误提示,指向问题所在。您可以将字面值强制转换为std::string以解决此问题。


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