Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 687
Zebrafish Avatar asked Sep 09 '25 20:09

Zebrafish


1 Answers

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

Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!