18
#include <vector>

int main()
{
    auto v = std::vector{std::vector<int>{}};
    return v.front().empty(); // error
}

See online demo

However, according to Scott Meyers' Effective Modern C++ (emphasis in original):

If, however, one or more constructors declare a parameter of type std::initializer_list, calls using the braced initialization syntax strongly prefer the overloads taking std::initializer_lists. Strongly. If there's any way for compilers to construe a call using a braced initializer to be a constructor taking a std::initializer_list, compilers will employ that interpretation.

So, I think std::vector{std::vector<int>{}}; should produce an object of std::vector<std::vector<int>> rather than std::vector<int>.

Who is wrong? and why?

xmllmx
  • 39,765
  • 26
  • 162
  • 323

2 Answers2

8

Meyers is mostly correct (the exception is that T{} is value-initialization if a default constructor exists), but his statement is about overload resolution. That takes place after CTAD, which chooses the class (and hence the set of constructors) to use.

CTAD doesn’t “prefer” initializer-list constructors in that it prefers copying to wrapping for nestable templates like std::vector or std::optional. (It’s possible to override this with deduction guides, but the standard library uses the default, as one might expect.) This makes some sense in that it prevents creating strange types like std::optional<std::optional<int>>, but it makes generic code harder to write because it gives

template<class T> void f(T x) {
  std::vector v{x};
  // …
}

a meaning that depends on the type of its argument in an irregular and non-injective fashion. In particular, v might be std::vector<int> with T=int or with T=std::vector<int>, despite being std::vector<std::deque<int>> if T=std::deque<int>. It’s unfortunate that a tool for computing one type based on some others is not usable in a generic context.

Davis Herring
  • 36,443
  • 4
  • 48
  • 76
6

auto v = std::vector{std::vector<int>{}}; actually creates a std::vector<int> because it uses std::vector copy constructor. It is interpreted by the compiler as:

auto vTemp = std::vector<int>{};
auto v = std::vector<int>( vTemp );

So v ends up being a std::vector<int>, not a std::vector<std::vector<int>>.

As reported by "P Kramer" and "Marek P" in comments, the following syntaxes will help any compiler accomplishing what you expect:

auto v = std::vector{{std::vector<int>{}}};
auto v = std::vector<std::vector<int>>{ std::vector<int>{} };
jpo38
  • 20,821
  • 10
  • 70
  • 151
  • Why does the ctor with `std::initializer_list` not take the priority as Scott said? – xmllmx Aug 30 '21 at 07:25
  • That may not be the first time a compiler does not respect what Scott says. Note that MSVC compiler even refuses to compile the first line, it reports `use of class template requires template argument list`...and with argument list there would be no more ambiguity. – jpo38 Aug 30 '21 at 07:29
  • 1
    Except for the suggestions, this merely restates the situation in the question. That “interpretation” (which gets the value category and lifetime of the argument wrong) doesn’t illustrate *why* `std::vector` is chosen. – Davis Herring Aug 30 '21 at 14:19