yohhoyの日記

技術的メモをしていきたい日記

std::vectorのサイズ指定初期化

C++標準ライブラリのシーケンスコンテナstd::vectorに関して、C++03/11でのサイズ指定コンストラクタ差異に関するメモ。

114 :デフォルトの名無しさん:2012/08/01(水) 10:00:54.97
>>111
vector(n), vector::resize(n) について。

C++03 では (n, value) と同じオーバーロードがデフォルト引数 T() で呼び出されて value の n 個のコピーからなる vector が得られた。
(ただし resize(n) は呼び出し前からあった要素はそのままね。)

C++11 での右辺値参照導入に際して、おそらくこれらの操作がそのままだと CopyConstructible を要求することになってしまって理不尽な要求や無駄なコピーを生じてしまうことから、 DefaultConstructible だけで使えるようにそれぞれ n だけを受け取るオーバーロードが追加されて n 個それぞれ個別に初期化された要素からなる vector が得られるようになった。

この変更によって、例えば >>75 のように T が内部に shared_ptr を持つようなオブジェクトの場合の shared_ptr の use_count(), unique() などについてvector(n) などの結果が変わることになってしまった。

http://toro.2ch.net/test/read.cgi/tech/1343559275/114

下記はC++03とC++11で挙動に差異が生じるコード。C++03標準ライブラリではstd::shared_ptrが提供されないため、Boost.Smart_Pointersライブラリで代用する。

#include <vector>
#include <iostream>
// C++03
#include <boost/smart_ptr.hpp>
using boost::shared_ptr;  using boost::make_shared;
// C++11
#include <memory>
using std::shared_ptr;  using std::make_shared;

class X {
  shared_ptr<int> c_;
public:
  X()
  {
    c_ = make_shared<int>(0);
  }

  int increment()
  {
    return (*c_)++;
  }
};

int main()
{
  std::vector<X> v(10);
  std::cout << v[0].increment() << std::endl;
  std::cout << v[0].increment() << std::endl;
  std::cout << v[5].increment() << std::endl;  // ★ C++03→2, C++11→0
}

実行結果(gcc 4.6.3):

$ g++ -std=c++03 input.cpp; ./a.out
0
1
2

$ g++ -std=c++0x input.cpp; ./a.out
0
1
0

C++03

下記2つの表記は同一コンストラクタを呼び出す。コンテナv0, v1共に各要素は “デフォルトコンストラクトされたT型オブジェクトのコピー” で初期化される。

std::vector<T> v0(10);  // vector<T>(10, T())を呼出
std::vector<T> v1(10, T());

C++03 23.1.1/p3(表中), 23.2.4.1より該当箇所を引用。

expression assertion/note
pre/post-condition
X(n, t)
X a(n, t);
post: size() == n.
constructs a sequence with n copies of t.

explicit vector(size_type n, const T& value = T(), const Allocator& = Allocator());

C++11

下記2つの表記は異なるコンストラクタを呼び出す。コンテナv0の各要素は “デフォルトコンストラクトされたT型オブジェクト”*1 で初期化され、コンテナv1の各要素は “デフォルトコンストラクトされたT型オブジェクトのコピー” で初期化される。

std::vector<T> v0(10);
std::vector<T> v1(10, T());

N3337 23.3.6.2/p3-4, p6-7より引用。

explicit vector(size_type n);
3 Effects: Constructs a vector with n value-initialized elements.
4 Requires: T shall be DefaultConstructible.

vector(size_type n, const T& value, const Allocator& = Allocator());
6 Effects: Constructs a vector with n copies of value, using the specified allocator.
7 Requires: T shall be CopyInsertable into *this.

2012-08-20追記:N1858にこの変更に関するRationale記載あり。

The rationale below applies to all sequences.

Each sequence currently has a constructor similar to the deque constructor:

explicit deque(size_type n, const T& value = T(), const Allocator& = Allocator());

This paper proposes that this constructor be split into two as follows:

explicit deque(size_type n);
deque(size_type n, const T& value, const Allocator& = Allocator());

The rationale for this change is to allow the size_type constructor to work with types which aren't CopyConstructible. That is, the first (proposed) constructor would default construct n value_type's into the container instead of today's behavior of default constructing one value_type and then copy constructing that value n times. This will allow (for example):

deque<unique_ptr<T>> c(100);  // ok, 100 unique_ptr's default constructed

The second (proposed) constructor still requires CopyConstructible and so can not be used with movable but non-copyable types such as unique_ptr.

The member function resize is similarly split into two for the exact same reasons. The second signature still takes its parameter by value instead of by const reference. This does not reflect the author's preference. Rather that is a separate issue and not addressed in this paper.

N1858 Rvalue Reference Recommendations for Chapter 23

関連URL

*1:厳密には "value-initialized" されたT型の一時オブジェクト(N3337 23.3.6.2/p3)。T=int 型のような組み込み型の場合、不定値ではなくゼロ初期化(zero-initialize)が行われる。