为什么我不能使用左值初始化这个std::vector?

26

我遇到了一个有趣的问题,但我无法理解发生了什么:

/* 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 */

据我所见,我提供的单个整数参数可以被解释为使用参数 size_type count 调用构造函数,或者使用初始化列表调用构造函数。当我提供一个左值时,它似乎只调用 initialiser_list 构造函数,但是当我给出一个右值 int 时(至少是字面量),它会调用 size_t count 构造函数。这是为什么呢?

此外,这意味着:

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

结果只会生成一个长度为 1 的向量;

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

这将会生成大小为num_elements的向量,但是我认为应该避免这种初始化方式,因为有时会遇到最令人烦恼的解析问题。


@AnoopRana 我刚在我的编译器 Visual Studio 2022 上进行了检查,结果是一个大小为 1 的向量。 - Zebrafish
@AnoopRana 您的演示与提问者的代码不符,它添加了一个额外的大括号。有必要解释一下语法对重载决策的影响。 - Nathan Pierson
1
啊,看起来我们也在结合几件不同的事情。声明是 std::vector<int> myvector{num_elements}; 创建一个只有一个元素的向量。这是正确的,因为 std::vector<int> 不同于 std::vector<int*>。(它有一个接受整数初始化列表的构造函数,而 std::vector<int*> 没有。) - Nathan Pierson
1
构造函数10解释了std::vector::vector的区别,请参阅上面的“注释”特别解决此问题。 - David C. Rankin
这是关于“新”的括号初始化的常见抱怨(特别是由于必须向初学者教授此内容的教师和培训师)。它们甚至在语言版本之间更改了行为(一种破坏性的更改),因为它太令人困惑了。委员会内部对此进行了很多辩论。但仍然很令人困惑。这是C++默认设置出现错误的事情之一。 - JHBonarius
1个回答

24

简述

这个问题不仅限于std::vector,而是源于以下规则(来自标准引用)。


让我们逐个案例看看发生了什么,以及为什么在使用lvalue时会出现所述的缩小转换错误/警告。

案例1

这里我们考虑:

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 

请注意,std::vector<int*>没有接受int初始化列表的初始化器列表构造函数。

因此,在这种情况下,将使用size_t count构造函数。现在让我们看看出现缩小转换错误/警告的原因。

当使用名为lvalue的变量时会产生错误/警告,但是使用prvalue int时不会产生错误/警告的原因是,在前一种情况下,lvalue不是常量表达式,因此我们有一个缩小转换。这可以从dcl.init.list#7中得到证明:

缩小转换是隐式转换

  • 从整数类型或未作用域枚举类型到不能表示原始类型所有值的整数类型的转换,除非源是一个整数提升后可以适应目标类型的常量表达式

(强调我的)

这意味着从类型为int的表达式lvalue(它是一个左值表达式)到向量的std::vector::vector(size_t, /*other parameters*/)构造函数的size_t参数的转换是一个缩小转换。但是,从prvalue int 6到向量的std::vector::vector(size_t, /*other parameters*/)size_t参数的转换不是缩小转换

为了证明这确实是这种情况,请看一些例子:

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

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

演示


案例2

在这里我们考虑:

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

在这种情况下,对于std::vector<int>,有一个初始化列表构造函数可用,由于我们在这里使用了大括号{}而不是小括号(),因此它将优先于size_t count构造函数。因此,将创建一个大小为1的向量。更多详细信息请参见使用大括号初始化列表时为什么首选std::initializer_list构造函数?
另一方面,当我们使用:
std::vector<int> myvector(num_elements); //this uses size_t ctor

在这种情况下,std::vectorsize_t构造函数将被用作初始化列表构造函数甚至不可行的情况,因为我们使用了括号()。因此,将创建一个大小为6的向量。您可以使用下面给出的示例进行确认:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接