Инициализация вложенного списка (вектор векторов строк) не выполняется

Этот код:

#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 из одного из строковых литералов. Если оба являются просто строковыми литералами, компилятор, похоже, "запутался" и конструирует вектор из 4 элементов, первый из которых содержит 97 строк. Заметим, что 97 - это код символа "a".

Я думаю, мой вопрос в том, должен ли компилятор интерпретировать эту проблемную конструкцию, как я ожидал, или этот плохой код для инициализации вложенного списка?

Ответы

Ответ 1

Внутренний вектор в MyClass({ { "a", "b" } }) создает с использованием конструктора диапазонов:

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

Это происходит потому, что { "a", "b" } интерпретируется не как std::initializer_list<std::string> а как пара исходных указателей.

Ответ 2

Вхождение в конструктор-нарушитель в отладчике показывает, что VC++ выбрал vector<vector<int>> который принимает два итератора (в этом случае они являются const char*).
То есть, он рассматривает конструкцию как

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++ есть ошибка.

Ответ 3

Я нашел обходное решение, которое помогает избежать этого неопределенного поведения с помощью VC++. Вы можете определить второй конструктор следующим образом:

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

Затем строки кода, которые могли бы дать проблему,

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

больше не будет компилироваться и даст вам ошибку "ошибка перегрузки конструктора, которая была неоднозначной", указывая на проблему. Затем вы можете вывести литерал в std :: string, чтобы решить проблему.