Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does std::initializer_list in ctor not behave as expected?

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

like image 609
xmllmx Avatar asked Aug 30 '21 07:08

xmllmx


Video Answer


2 Answers

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.

like image 174
Davis Herring Avatar answered Nov 12 '22 17:11

Davis Herring


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>{} };
like image 36
jpo38 Avatar answered Nov 12 '22 17:11

jpo38