C++向量成员初始化

13

我对以下程序中 Test 类中的 vec 输出感到困惑。为什么它是一个大小为 100 的向量,而不是 1?我认为 std::vector<T> var{a}std::vector<T> var = {a} 是相同的。

#include <iostream>
#include <vector>

using namespace std;
struct Value {
   int a;
   int b;
};

class Test {
  public:
    std::vector<struct Value> vec{100};  
};

class Test2 {
  public:
    std::vector<int> vec{100};  
};

int main()
{
    Test test;
    std::cout << "test size: " << test.vec.size() << std::endl;
    Test2 test2;
    std::cout << "test2 size: " << test2.vec.size();
    
    return 0;
}

输出:

test size: 100
test2 size: 1

3
标准委员会本可以通过选择不同的初始化列表语法(比如 <a,b,c>)来避免这种混乱。 - curiousguy12
3个回答

11

std::vector有一个带有std::initializer_list<T>参数的构造函数。当使用像{100}这样的初始化列表时,如果适用,则始终会优先选择该构造函数。

对于std::vector<int>,初始值{100}std::initializer_list<int>兼容,因此将使用该构造函数。它将创建一个包含集合{100}的向量,其中只有一个值为100int

对于std::vector<Value>,初始值{100}std::initializer_list<Value>参数不兼容。由于Value没有用于将int转换为Value的转换构造函数,因此无法从{100}构造一个std::initializer_list<Value>。可以通过此示例验证不允许进行此转换。编译器将尝试选择低优先级的构造函数,并使用将100个默认构造的Value初始化的构造函数。

如果您添加一个Value::Value(int)构造函数或使用{{100}}作为Test2的初始化程序,您会发现std::vector<Value>现在只包含一个元素。在两种情况下,初始化列表都可转换为std::initializer_list<Value>,并且将使用该构造函数。

std::initializer_list<Value>{100}虽然可以编译。 - HolyBlackCat
6
啊,C++,总是准备好要咬你的脚踝。 - Paul Sanders
@HolyBlackCat 当我尝试编译时,似乎实际上并没有编译成功: https://godbolt.org/z/f5Kos3eKj - François Andrieux
Clang接受它。 :( - HolyBlackCat
1
我已经在Stack Overflow上提出了一个问题 - HolyBlackCat

4

当你发现{100}的含义时,对于T == int会发生更改。

简短回答:

vector<Value>{100}中的100不能被解释为Value类型,因此大小构造函数优先。

如果你坚持使用{100}作为Value类型,则需要添加额外的花括号:vector<Value>{{100}}

请参见这里的说明:https://godbolt.org/z/xcMT1oc5z

我的建议是避免进一步讨论法律问题,方法如下:

为了在不同类型之间保持一致初始化,使用括号进行大小初始化和方括号进行元素初始化,强制执行以下操作:

    std::vector<int> vec = std::vector<int>(100);

总的来说:

    std::vector<T> vec = std::vector<T>(100);

在这种情况下,100始终是尺寸。

2
为什么“int”和“Value”之间的含义会发生变化? - François Andrieux
2
这是很好的信息,但我认为问题是“为什么它是大小为100的向量而不是1?” - Drew Dormann
1
@FrançoisAndrieux 因为对于 T == int{100} 可以被解释为一个后验列表,该列表只有一个元素,即 100 - alfC
2
@alfC 但是为什么Value也不是这种情况呢?它是一个聚合类型,你可以像这样初始化ValueValue x{100};,它可以正常工作。编辑:如果你添加一个Value(int)构造函数,它现在会构造一个只有一个Value实例的向量。所以它并不像你所说的那么简单。 - François Andrieux
2
@AlexanderMalakhov 无法重现,对我来说打印出了 1 - HolyBlackCat
显示剩余8条评论

1

这可能不是完全有用的答案,但我决定在STD库中vector类声明中设置断点。

回答

在定义向量时,有三种方式处理赋值。结构体将提供值构建,而int将被分配为大小范围构建。

它将std::vector<struct> vect{100}; 读作构建长度为100的向量,而std::vector<int> vect{100};则类似于vect.insert(v.end(),100);

这基于传递给的对象的类型。

为了信息的完整性,最后一种选择是取一个给定的值,并将其分配给许多位置。因此,如果您有100个“x”,它会将“x”放入您的向量中100次。

旅程

我从中学到的是,在您的向量接受size_type输入和_Valty&&输入(我还不知道那是什么。稍后会查找)之间存在一个点,并在3个不同的参数之间提供构造。

我的最佳猜测是,您的结构体填充了1个args路径并充当长度声明,而原生类型的int则落入2个args路径中并充当值分配。

sizeof(Value)可能== 0,而int的大小将为1。

编辑:我猜测了1和2(或_Count == 0和Count == 1),但是我错了。它是_Count == 0和_Count == 2。这非常有趣。

    template <class... _Valty>
    _CONSTEXPR20 void _Construct_n(_CRT_GUARDOVERFLOW const size_type _Count, _Valty&&... _Val) {
        // Dispatches between the three sized constructions.
        // 1-arg -> value-construction, e.g. vector(5)
        // 2-arg -> fill, e.g. vector(5, "meow")
        // 3-arg -> sized range construction, e.g. vector{"Hello", "Fluffy", "World"}
        auto& _Al       = _Getal(); //////////////// For test1, _Count is 100, for test2, _Count is 1;
        auto&& _Alproxy = _GET_PROXY_ALLOCATOR(_Alty, _Al);
        auto& _My_data  = _Mypair._Myval2;
        _Container_proxy_ptr<_Alty> _Proxy(_Alproxy, _My_data);


        if (_Count != 0) {
            _Buy_nonzero(_Count);
            _Tidy_guard<vector> _Guard{this};

// This one happens with a struct
if constexpr (sizeof...(_Val) == 0) {
                _My_data._Mylast = _Uninitialized_value_construct_n(_My_data._Myfirst, _Count, _Al);
            } else 


if constexpr (sizeof...(_Val) == 1) {
                _STL_INTERNAL_STATIC_ASSERT(is_same_v<_Valty..., const _Ty&>);
                _My_data._Mylast = _Uninitialized_fill_n(_My_data._Myfirst, _Count, _Val..., _Al);
            } else 

// This one happens with an int
if constexpr (sizeof...(_Val) == 2) {
                _My_data._Mylast = _Uninitialized_copy(_STD forward<_Valty>(_Val)..., _My_data._Myfirst, _Al);
            } else {
                static_assert(_Always_false<_Ty>, "Should be unreachable");
            }
            _Guard._Target = nullptr;
        }

        _Proxy._Release();
    }

有趣的是,这似乎发生在分配 Allocator 引用时。虽然我并不是语言方面的专家,但我真的很想解决这个谜题!感谢这个有趣的挑战!

如果您以前从未遍历过类定义,请尝试一下。


3
请注意,标准库类的编写规则与用户类不同。它们是专门针对特定架构或编译器编写的,并且可能依赖于非可移植技巧。它们的优先级也与生产代码中通常看到的不同,并遵循非常不同的约定。总的来说,看看它们如何工作可能很有趣,但不要过度依赖这些知识作为学习材料。很难明显区分聪明的实现和非可移植的实现。 - François Andrieux
1
好观点!虽然我认为,如果你正在寻找标准库类型以意外方式反应的原因,仍然最好逐个过一遍这些过程。 - Chris Gergler

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