Matvei
Matvei

Reputation: 217

Why does this code generate different output for different compilers?

I have the following code:

#include <iostream>
#include <vector>

struct C {
  int a;

  C() : a(0) {}
  C(int a) : a(a) {}
};

std::ostream &operator<<(std::ostream &os, const C &c) {
  os << c.a;
  return os;
}

using T = std::vector<C>;

int main() {
  auto v = T({5});

  for (const auto &a : v) {
    std::cout << a << ", ";
  }
  std::cout << std::endl;

  return 0;
}

If I use g++, it prints:

5, 

If I use MS compiler, it prints:

0, 0, 0, 0, 0,

Why are the results different?

Without () around {5}, the results, of course, are the same.

Upvotes: 20

Views: 870

Answers (1)

Adrian Mole
Adrian Mole

Reputation: 51894

This is probably a bug in the g++ compiler§ (not in the MSVC compiler). The type of the v variable is (or should be, according to the Standard) a std::vector<C> containing 5 default-initialized elements.

Why? Because you use direct initialization (round brackets) rather than list initialization (curly braces, as in auto v = T{ 5 }). This direct initialization means that a constructor is called, and there is no constructor for std::vector<T> that takes a single, T argument.

This source of confusion is mentioned on cppreference:

Note that the presence of list-initializing constructor (10) means list initialization and direct initialization do different things:

Now, you may think that the compiler should be using that list-initializing constructor (as g++ does) … but it shouldn't be doing so! That constructor's argument is not of type T but of type std::initializer_list<T>; so, to call that constructor (using direct initialization), you would need auto v = T({ { 5 } }); (note the extra set of braces).

For your code, auto v = T({ 5 });, the closest-matched constructor is version #3 on the linked cppreference page (with the 2nd and 3rd arguments defaulted), so the { 5 } is treated as a size_t value for the count of elements initialized as T(). In fact, according to that same page, the defaulted 2nd argument version has been removed since C++11 – so the only match for your call is constructor version #4, which produces the same result.

In fact, clang (which gives the same result as MSVC for your code) warns about this (see it on Compiler Explorer):

warning : braces around scalar initializer [-Wbraced-scalar-init]

There are a number of ways you can force the initialization to a single-element vector with a {5} member. The simplest is to use list-initialization:

auto v = T{ { 5 } }; // Or even: auto v = T{ 5 };

Another is to add an explicit count argument:

auto v = T(1, { 5 }); // Or just: auto v = T(1, 5);

§ I do not claim to be an authority when it comes to the rules a compiler should follow in order to conform to the C++ Standard(s), so I am prepared to accept that this may be a (rare) ambiguity in the Standard, rather than a bug or defect in the g++ compiler.

Note that, although { 5 } can be used as a valid std::initializer_list<C> (as it is in the case of auto v = T{ 5 };), in terms of overload resolution for function calls (including those to constructors), it is a better match for a single size_t value; see Single-Element-Vector Initialization in a Function Call (especially the top answer).

Upvotes: 13

Related Questions