Why can't I initialize this std::vector with an l-value?
Asked Answered
S

1

26

I've come across an interesting issue, and I can't understand what's happening:

/* I WANT 6 ELEMENTS */
int lvalue = 6;

std::vector<int*> myvector { 6 }; /* WORKS FINE */
std::vector<int*> myvector{ lvalue }; /* DOESN'T WORK */
/* Element '1': conversion from 'int' to 'const unsigned __int64 requires a narrowing conversion */

From what I can see a single integer argument that I've provided can either be interpreted as calling the constructor with argument size_type count, or the one that takes an initializer list. It seems to call the initialiser_list constructor only when I provide an l-value but the size_t count constructor when I give an r-value int (well, a literal at least). Why is this?

Also this means that:

int num_elements = 6;
std::vector<int> myvector{num_elements};

Results in a vector of only size 1;

std::vector<int> myvector(num_elements);

Results in a vector of size num_elements, but I thought this initialization should be avoided because of occasionally running into most vexing parse issues.

Studious answered 5/6, 2022 at 5:30 Comment(5)
@AnoopRana I just checked on my compiler, Visual Studio 2022, it results in a vector of size 1.Studious
@AnoopRana Your demo doesn't match asker's code, it adds an additional set of braces. Some explanation of exactly how the syntax affects the overload resolution seems warranted.Coupe
Ah. It looks like we're also combining a couple different things. The claim was that std::vector<int> myvector{num_elements}; creates a vector with one element. Which it does, because std::vector<int> is not the same as std::vector<int*>. (It has a constructor that takes initializer lists of ints, and the std::vector<int*> does not.)Coupe
Constructor 10 explains the difference std::vector::vector and see Notes above the example specifically addressing this issue.Attempt
This is a common complaint regarding the "new" bracket initialization (especially by teachers and trainers that have to teach this to beginners). They even changed behavior between language versions (a breaking change) because it was so confusing. There has been much debate within the committee about this. But it's still confusing. This is one of the things where C++ got the defaults wrong.Fuze
D
24

TL;DR

The problem is not specific/limited to std::vector but instead is a consequence of the rule quoted below from the standard.


Let's see on case by case basis what is happening and why do we get the mentioned narrowing conversion error/warning when using lvalue.

Case 1

Here we consider:

int lvalue = 6; // lvalue is not a constant expression 

//---------------------------v------------------->constant expression so works fine
std::vector<int*> myvector { 6 };
std::vector<int*> myvector{ lvalue };
//--------------------------^^^^^^--------------->not a constant expression so doesn't work 

First note that std::vector<int*> does not have an initializer list constructor that takes an initializer list of int.

So in this case the size_t count ctor will be used. Now let's see the reason for getting narrowing conversion error/warning.

The reason we get an error/warning when using the variable named lvalue while not when using a prvalue int is because in the former case lvalue is not a constant expression and so we have a narrowing conversion. This can be seen from dcl.init.list#7 which states:

A narrowing conversion is an implicit conversion

  • from an integer type or unscoped enumeration type to an integer type that cannot represent all the values of the original type, except where the source is a constant expression whose value after integral promotions will fit into the target type.

(emphasis mine)

This means that the conversion from lvalue(which is an lvalue expression) which is of type int to size_t parameter of the vector's std::vector::vector(size_t, /*other parameters*/) ctor, is a narrowing conversion. But the conversion from prvalue int 6 to the size_t parameter of the vector's std::vector::vector(size_t, /*other parameters*/) is not a narrowing conversion.

To prove that this is indeed the case, lets look at some examples:

Example 1

int main()
{
//----------------v---->no warning as constant expression
    std::size_t a{1};
    
    int i = 1;
//----------------v---->warning here i is not a constant expression
    std::size_t b{i};  

    constexpr int j = 1;
//----------------v---->no warning here as j is a constexpr expression
    std::size_t c{j};
    return 0;
}

Example 2

struct Custom 
{
  Custom(std::size_t)
  {
      
  }
};
int main()
{
//-----------v---->constant expression
    Custom c{3}; //no warning/error here as there is no narrowing conversion
    
    int i = 3;  //not a constant expressoion

//-----------v---->not a constant expression and so we get warning/error
    Custom d{i}; //warning here of narrowing conversion here
    

    constexpr int j = 3; //constant expression 

//-----------v------>no warning here as j is a constant expression and so there is no narrowing conversion
    Custom e{j};  
    return 0;
}

Demo


Case 2

Here we consider:

//------------v-------------------------->note the int here instead of int* unlike case 1 
std::vector<int> myvector{num_elements};//this uses constructor initializer list ctor 

In this case there is a initializer list ctor available for std::vector<int> and it will be preferred over the size_t count constructor as we've used braces {} here instead of parenthesis (). And so a vector of size 1 will be created. More details at Why is the std::initializer_list constructor preferred when using a braced initializer list?.


On the other hand, when we use:

std::vector<int> myvector(num_elements); //this uses size_t ctor

Here the size_t ctor of std::vector will be used as the initializer list ctor is not even viable in this case as we've used parenthesis (). And so a vector of size 6 will be created. You can confirm this using the example given below:

struct Custom 
{
   
  Custom(std::size_t)
  {
      std::cout<<"size t"<<std::endl;
  }
  Custom(std::initializer_list<int>)
  {
      std::cout<<"initializer_list ctor"<<std::endl;
  }
};
int main()
{
    Custom c(3); //uses size_t ctor, as the initializer_list ctor is not viable 
    return 0; 
}
Dopester answered 5/6, 2022 at 6:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.