Urmas Rahu
Urmas Rahu

Reputation: 313

Nested list (vector of vectors of strings) initialization fails

This code:

#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
}

outputs this (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

So, it works OK in all cases except in the case where we have a vector of one vector, containing two strings. It also works in the above case if we explicitly construct std::string from one of the string literals. If both are just plain string literals, the compiler seems to get "confused" and constructs a vector of 4 items, the first of which contains 97 strings. Note that 97 is the character code of "a".

I guess my question is, should the compiler interpret this problematic construction as I'd expect, or is this bad code to initialize a nested list like this?

Upvotes: 18

Views: 1884

Answers (3)

Urmas Rahu
Urmas Rahu

Reputation: 313

I have found a workaround that helps to avoid this undefined behavior with VC++. You can define a second constructor like this:

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

Then, the code lines that would give the problem,

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

will not compile anymore and will give you the "constructor overload resolution was ambiguous" error, pointing to the problem. You can then typecast the literal to std::string to resolve the issue.

Upvotes: 0

Valery Kopylov
Valery Kopylov

Reputation: 383

The inner vector in MyClass({ { "a", "b" } }) is creating using range constructor:

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

This happens because { "a", "b" } is interpreted not as std::initializer_list<std::string> but as a pair of raw pointers.

Upvotes: 17

molbdnilo
molbdnilo

Reputation: 66371

Stepping into the offending constructor in the debugger reveals that VC++ has picked the vector<vector<int>> constructor that takes two iterators (they are const char*s in this case).
That is, it treats the construction like

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

This, of course, leads to undefined behaviour since the two pointers don't belong to the same array.

As a side note, g++ compiles both of

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

but crashes miserably on the latter, while the former behaves as expected.

VC++ compiles the double-braced variable construction in the way you would expect, so I suspect (hope) that there's a bug in VC++.

Upvotes: 8

Related Questions