有效地初始化具有初始值的向量

时间:2015-10-25 22:08:10

标签: c++

鉴于此类型的测试:

struct Counter
{
    static int count;

    Counter(int)
    {
        count++;
    }

    Counter(const Counter&)
    {
        count++;
    }

    Counter(Counter&&) noexcept
    {

    }
};

int Counter::count = 0;

假设我们有以下内容:

std::vector<Counter> vec(5, 0);

根据VS2015,创建了6个Counter个对象。我知道只有5个永久对象。为什么编译器没有从构造函数参数中放置对象,或者将临时对象移动到第一个位置然后从中复制其余的对象?

即使矢量的初始大小设置为0,仍然会创建1个对象。

std::vector<Counter> vec(0, 0);

如果在运行时之前不知道大小并且通常为0(无操作)并且容器中的类型复制或构建成本昂贵,这可能很重要。

在一个语句中初始化向量通常很方便,特别是如果它们是初始化列表中的类成员或常量。我怎样才能像下面的代码一样有效地做到这一点:

std::vector<Counter> vec;
vec.reserve(size);
for (size_t i = 0; i < size; i++)
{
    vec.emplace_back(0);
}

它构造的对象只包含存储在向量中的对象。

2 个答案:

答案 0 :(得分:1)

我认为这是有效的,基于您的定义

测试不同的版本;

Copies: 100000005 , Construct: 1, Equal copies 0
real    0m0.075s
user    0m0.073s
sys 0m0.002s

并使用emplace_back:

Copies: 0 , Construct: 100000005, Equal copies 0
real    0m0.195s
user    0m0.191s
sys 0m0.004s

你可能意味着它节省空间。但是,这是基于用例的选择,似乎编译器设计者更喜欢速度。

这是代码(我跟踪也相等)

struct Counter
{
static int count_ctor;
static int count_copy;
static int count_equal;

Counter(int){count_ctor++;}    
Counter(const Counter&){count_copy++;}    
Counter(Counter&&) noexcept{}
Counter & operator=(Counter const &){ count_equal++ ;}
};

int Counter::count_copy = 0;
int Counter::count_ctor = 0;
int Counter::count_equal = 0;

int main(void)
{
  int size(100000005);

#ifdef EMPLACE
  std::vector<Counter> v;
  v.reserve(size);
  for(int i = size; i>0 ; --i){ v.emplace_back(0);}
#else
  std::vector<Counter> v(size,0);
#endif    
  std::printf("Copies: %d , Construct: %d, Equal copies %d",Counter::count_copy, Counter::count_ctor, Counter::count_equal);
  return 0;
}

使用g ++ -DEMPLACE --std = c ++ 11 -O3 或不使用EMPLACE进行编译,以获得所需的二进制文件。

第二次测试

为了驳斥OP所做的假设,我们进行了以下测试:

  1. 创建在多个较大类中创建的许多小向量
  2. 使用默认构造复制策略或通过调用emplace包装函数创建的所有对象。
  3. 我们用

    制作了两个二进制文件
    g++ -DEMPLACE --std=c++11 -O3 copyc.cpp -o copyc && g++ --std=c++11 -O3 copyc.cpp -o copyc_copy
    

    并且为了避免两者中的任何一个从操作系统中获得优先处理,我们在它们之间设置了10秒的标准暂停,并且我们在系统处于空闲状态时启动。

    示例性运行如下。

    export K=10192 ; time ./copyc_copy $K ; sleep 10; time ./copyc $K
    Copies: 10192 , Construct: 1, Equal copies 0
    real    0m2.888s
    user    0m0.666s
    sys 0m2.219s
    Copies: 0 , Construct: 10192, Equal copies 0
    real    0m3.376s
    user    0m1.105s
    sys 0m2.270s
    

    我在多种情况下运行它,也在反向运行

    Copies: 0 , Construct: 10192, Equal copies 0
    real    0m3.154s
    user    0m0.886s
    sys 0m2.267s
    Copies: 10192 , Construct: 1, Equal copies 0
    real    0m2.573s
    user    0m0.531s
    sys 0m2.025s
    

    据说这是一个不切实际的测试,但是花了这个匹配时间,我打赌编译器设计者做了更多,所有从gnu到clang和VS决定实现构造复制策略。我确信他们也有其他原因。

    第二次测试的代码如下:

    #include <vector>
    #include <iostream>
    #include <cstdlib>
    
    template<typename T> static std::vector<T> get5()
    {
    std::vector<T> s;
    s.reserve(5);
    for(int i=5; i!=0 ;--i)
    {
    s.emplace_back(T());
    }
    return s;
    }
    
    struct test_struct
    {
        volatile int internals[255];
    };
    
    struct test_create
    {
    std::vector<test_struct> s;
    test_create() : s(5){}
    };
    
    struct test_emplace
    {
    std::vector<test_struct> s;
    test_emplace() : s(get5<test_struct>()){}
    };
    
    
    struct Counter
    {
    static int count_ctor;
    static int count_copy;
    static int count_equal;
    
    #ifdef EMPLACE
    test_emplace t[100];
    #else
    test_create t[100];
    #endif
    
    Counter(int)
    {
    count_ctor++;
    }
    
    Counter(const Counter&)
    {
    count_copy++;
    }
    
    Counter(Counter&&) noexcept
    {
    
    }
    Counter & operator=(Counter const &){ count_equal++ ;}
    };
    
    int Counter::count_copy = 0;
    int Counter::count_ctor = 0;
    int Counter::count_equal = 0;
    
    int main(int arg, char const * argv[])
    {
    int size(std::atoi(argv[1]));
    
    #ifdef EMPLACE
    std::vector<Counter> v;
    v.reserve(size);
    for(int i = size; i>0 ; --i)
    {
        v.emplace_back(0);
    }
    #else
    std::vector<Counter> v(size,0);
    #endif
    
    std::printf("Copies: %d , Construct: %d, Equal copies %d",Counter::count_copy, Counter::count_ctor, Counter::count_equal);
    
    return 0;
    
    }
    

答案 1 :(得分:1)

您可以简单地定义一个以您想要的方式创建矢量的函数。

作为一个函数,初始化代码是异常安全的。

#include <iostream>
#include <vector>
#include <stddef.h>     // ptrdiff_t
#include <utility>      // std::forward
using namespace std;

struct Counter
{
    static int n_constructor_calls;

    Counter( int )
    {
        ++n_constructor_calls;
    }

    Counter( Counter const& )
    {
        ++n_constructor_calls;
    }

    Counter( Counter&& ) noexcept
    {
        ++n_constructor_calls;
    }
};

int Counter::n_constructor_calls = 0;

//--------------------------------------

using Size = ptrdiff_t;
using Index = Size;

template< class Item, class... Args >
auto make_vector( Size const n, Args&&... args )
    -> vector<Item>
{
    vector<Item>    result;
    result.reserve( n );
    for( Index i = 0; i < n; ++i )
    {
        result.emplace_back( forward<Args>( args )... );
    }
    return result;
}

auto main() -> int
{
    auto vec = make_vector<Counter>( 5, 42 );
    cout << Counter::n_constructor_calls << " constructor calls.\n";
}

(输出“5个构造函数调用。”)

你基本上问,为什么不定义vector构造函数来执行此操作,

  

为什么编译器不会从构造函数参数中置位对象,或者将临时对象移动到第一个位置然后从中复制其余对象?

一个原因是这个构造函数是在C ++ 11中引入移动语义之前定义的。

相对于现有的C ++代码库来说,引入其他构造函数(更改过载行为)或更改现有构造函数的行为是非常昂贵的。