[lv5] ft_containers(3/6) vectorの実装
[(2/6)vector_otherにもどる] https://note.com/syamashi/n/n55ee0dad207d
[(4/6)stackにすすむ] https://note.com/syamashi/n/nbc30c2c89db4
-std=c++98 をつけてコンパイルすると、c++11の記法でコケます。
どんどん置換します。
auto -> 型の明記
using A = B -> typedef B A
noexcept -> delete
return reverse_iterator{last} -> return reverse_iterator(last)
for(auto v: V) -> for( ; ; )
std::allocator_traits -> allocから直接呼び出す。 // alloc.construct()
vector : vector(alloc) -> ほかのコンストラクタに投げない。
// : first(NULL), last(NULL), reserved_last(NULL), alloc(alloc)
select_on_container_copy_construction -> delete
std::move -> delete
cbegin / cend -> delete
Q. reverse_iterator{last}
A. 一様初期化(c++11)
https://cpprefjp.github.io/lang/cpp11/uniform_initialization.html
reverse_iterator(last) で代替した。
チュートリアルを置換しただけのものが以下。
/testfiles/Makefile
CFLAGS = -Wall -Werror -Wextra -std=c++98
/containers/vector.hpp
#ifndef VECTOR_HPP
#define VECTOR_HPP
#include <iostream>
#include <limits>
#include "../utils/algorithm.hpp"
#include "../utils/iterator.hpp"
#include "../utils/random_access_iterator.hpp"
#include "../utils/util.hpp"
namespace ft {
template <typename T, typename Allocator = std::allocator<T> >
class vector {
public:
// value_typeなどネストされた型名
typedef T value_type;
typedef T* pointer;
typedef const pointer const_pointer;
typedef value_type& reference;
typedef const value_type& const_reference;
typedef Allocator allocator_type;
typedef std::size_t size_type;
typedef std::ptrdiff_t difference_type;
typedef pointer iterator;
typedef const_pointer const_iterator;
typedef ft::reverse_iterator<iterator> reverse_iterator;
typedef ft::reverse_iterator<const_iterator> const_reverse_iterator;
// コンストラクター
vector()
: first(NULL), last(NULL), reserved_last(NULL), alloc(allocator_type()) {}
vector(const allocator_type& alloc)
: first(NULL), last(NULL), reserved_last(NULL), alloc(alloc) {}
explicit vector(size_type count, const T& value = T(),
const Allocator& alloc = Allocator())
: first(NULL), last(NULL), reserved_last(NULL), alloc(alloc) {
resize(count, value);
}
template <typename InputIterator>
vector(InputIterator first, InputIterator last,
const Allocator& alloc = Allocator(),
typename ft::enable_if<!ft::is_integral<InputIterator>::value,
InputIterator>::type* = NULL)
: first(NULL), last(NULL), reserved_last(NULL), alloc(alloc) {
reserve(ft::distance(first, last));
for (pointer i = first; i != last; ++i) {
push_back(*i);
}
}
// コピー
vector(const vector& r)
: first(NULL), last(NULL), reserved_last(NULL), alloc(r.alloc) {
// コピー元の要素数を保持できるだけのストレージを確保
reserve(r.size());
// コピー元の要素をコピー構築
// destはコピー先
// [src, last)はコピー元
for (pointer dest = first, src = r.begin(), last = r.end(); src != last;
++dest, ++src) {
construct(dest, *src);
}
last = first + r.size();
}
// デストラクター
~vector() {
// 1. 要素を末尾から先頭に向かう順番で破棄
clear();
// 2. 生のメモリーを解放する
deallocate();
}
vector& operator=(const vector& r) {
// 1. 自分自身への代入なら何もしない
if (this == &r) return *this;
// 2. 要素数が同じならば
if (size() == r.size()) { // 要素ごとにコピー代入
std::copy(r.begin(), r.end(), begin());
}
// 3. それ以外の場合で
else
// 予約数が十分ならば、
if (capacity() >= r.size()) {
// 有効な要素はコピー
std::copy(r.begin(), r.begin() + r.size(), begin());
// 残りはコピー構築
for (const_iterator src_iter = r.begin() + r.size(), src_end = r.end();
src_iter != src_end; ++src_iter, ++last) {
construct(last, *src_iter);
}
}
// 4. 予約数が不十分ならば
else {
// 要素をすべて破棄
destroy_until(rbegin());
// 予約
reserve(r.size());
// コピー構築
for (const_iterator src_iter = r.begin(), src_end = r.end(),
dest_iter = begin();
src_iter != src_end; ++src_iter, ++dest_iter, ++last) {
construct(dest_iter, *src_iter);
}
}
return *this;
}
// 容量確認
size_type size() const { return end() - begin(); }
bool empty() const { return begin() == end(); }
size_type capacity() const { return reserved_last - first; }
// 要素アクセス
void push_back(const_reference value) {
// 予約メモリーが足りなければ拡張
if (size() + 1 > capacity()) {
// 現在のストレージサイズ
size_type c = size();
// 0の場合は1に
if (c == 0)
c = 1;
else
// それ以外の場合は2倍する
c *= 2;
reserve(c);
}
construct(last, value);
++last;
}
reference operator[](size_type i) { return first[i]; }
const_reference operator[](size_type i) const { return first[i]; }
reference at(size_type i) {
if (i >= size()) throw std::out_of_range("index is out of range.");
return first[i];
}
const_reference at(size_type i) const {
if (i >= size()) throw std::out_of_range("index is out of range.");
return first[i];
}
reference front() { return *first; }
const_reference front() const { return *first; }
reference back() { return *(last - 1); }
const_reference back() const { return *(last - 1); }
pointer data() { return first; }
const_pointer data() const { return first; }
// イテレーターアクセス
iterator begin() { return first; }
iterator end() { return last; }
const_iterator begin() const { return first; }
const_iterator end() const { return last; }
reverse_iterator rbegin() { return reverse_iterator(last); }
reverse_iterator rend() { return reverse_iterator(first); }
const_reverse_iterator rbegin() const { return reverse_iterator(last); }
const_reverse_iterator rend() const { return reverse_iterator(first); }
void clear() { destroy_until(rend()); }
/*
1. すでに指定された要素数以上に予約されているなら何もしない
2. まだ動的メモリー確保が行われていなければ動的メモリー確保をする
3. 有効な要素がある場合は新しいストレージにコピーする
*/
void reserve(size_type sz) {
// すでに指定された要素数以上に予約されているなら何もしない
if (sz <= capacity()) return;
// 動的メモリー確保をする
pointer ptr = allocate(sz);
// 古いストレージの情報を保存
pointer old_first = first;
pointer old_last = last;
size_type old_capacity = capacity();
// 新しいストレージに差し替え
first = ptr;
last = first;
reserved_last = first + sz;
// 例外安全のため
// 関数を抜けるときに古いストレージを破棄する
// std::scope_exit e(
// [&] { traits::deallocate(alloc, old_first, old_capacity); });
// 古いストレージから新しいストレージに要素をコピー構築
// 実際にはムーブ構築
for (pointer old_iter = old_first; old_iter != old_last; ++old_iter, ++last) {
// このコピーの理解にはムーブセマンティクスの理解が必要
construct(last, *old_iter);
}
// 新しいストレージにコピーし終えたので
// 古いストレージの値は破棄
for (reverse_iterator riter = reverse_iterator(old_last),
rend = reverse_iterator(old_first);
riter != rend; ++riter) {
destroy(&*riter);
}
// scope_exitによって自動的にストレージが破棄される
alloc.deallocate(old_first, old_capacity);
}
/*
1. 現在の要素数より少なくリサイズする場合、末尾から要素を破棄する
2. 現在の要素数より大きくリサイズする場合、末尾に要素を追加する
3. 現在の要素数と等しくリサイズする場合、何もしない
*/
void resize(size_type sz) {
// 現在の要素数より少ない
if (sz < size()) {
difference_type diff = size() - sz;
destroy_until(rbegin() + diff);
last = first + sz;
}
// 現在の要素数より大きい
else if (sz > size()) {
reserve(sz);
for (; last != reserved_last; ++last) {
construct(last);
}
}
}
void resize(size_type sz, const_reference value) {
// 現在の要素数より少ない
if (sz < size()) {
difference_type diff = size() - sz;
destroy_until(rbegin() + diff);
last = first + sz;
}
// 現在の要素数より大きい
else if (sz > size()) {
reserve(sz);
for (; last != reserved_last; ++last) {
construct(last, value);
}
}
}
private:
// 先頭の要素へのポインター
pointer first;
// 最後の要素の1つ前方のポインター
pointer last;
// 確保したストレージの終端
pointer reserved_last;
// アロケーターの値
allocator_type alloc;
// ヘルパー関数
private:
pointer allocate(size_type n) { return alloc.allocate(n); }
void deallocate() { alloc.deallocate(first, capacity()); }
void construct(pointer ptr) { alloc.construct(ptr, 0); }
void construct(pointer ptr, const_reference value) {
alloc.construct(ptr, value);
}
void destroy(pointer ptr) { alloc.destroy(ptr); }
void destroy_until(reverse_iterator rend) {
for (reverse_iterator riter = rbegin(); riter != rend; ++riter, --last) {
destroy(&*riter);
}
}
};
}; // namespace ft
#endif
/testfiles/tester.hpp
//// tester
void vector_test();
/testfiles/main.cpp
#include "tester.hpp"
int main() {
vector_test();
}
tutorial_test()は削除して、vector_test()に吸収します。
/testfiles/test_vector.cpp
#include "tester.hpp"
template <typename T>
void pout(T s) {
static int no;
cout << endl;
cout << "--- [" << ++no << "]:" << s << " ---" << endl;
}
template <class T>
void vdebug(T& V) {
cout << "size:" << V.size() << " capacity:" << V.capacity() << endl;
cout << "{ ";
for (typename T::iterator it = V.begin(); it != V.end(); ++it)
cout << *it << " ";
cout << "}" << endl;
}
void vector_test() {
}
Q. pout()
A. --- [テストNo] タイトル ---
Q. vdebug()
A. 引数にvectorを渡したら、サイズ、キャパ、配列の中身を列挙させます。
ここから、cppreferenceを参照して、不足しているvectorの実装を進めます。
・cppreference ( vector )
https://en.cppreference.com/w/cpp/container/vector
・Member types
/containers/vector.hpp
{
typedef typename ft::random_access_iterator<value_type> iterator;
typedef typename ft::random_access_iterator<const value_type> const_iterator;
typedef typename ft::reverse_iterator<iterator> reverse_iterator;
typedef typename ft::reverse_iterator<const_iterator> const_reverse_iterator;
}
Q. iterator
もともとusing iterator = pointerとしていましたが、
iterator = ft::random_access_iteratorとします。
そうはいっても、iteratorのメンバ変数は*Tだけだし、vectorのメンバ変数も*Tだけ。名称が違うだけで扱いは全く同じ。iteratorを使わずとも、pointerでvectorの関数実装はできてしまいます。
iteratorのありがたみはmapで実感できるので、とりあえず進めます。
iteratorが変わり、pointerと互換性がなくなりました。
Constructorのコンパイルエラーを解消します。
・Constructor 5つ / operator=
/containers/vector.hpp
// コンストラクター
vector()
: first(NULL), last(NULL), reserved_last(NULL), alloc(allocator_type()) {}
vector(const allocator_type& alloc)
: first(NULL), last(NULL), reserved_last(NULL), alloc(alloc) {}
explicit vector(size_type count, const T& value = T(),
const Allocator& alloc = Allocator())
: first(NULL), last(NULL), reserved_last(NULL), alloc(alloc) {
resize(count, value);
}
template <typename InputIterator>
vector(InputIterator first, InputIterator last,
const Allocator& alloc = Allocator(),
typename ft::enable_if<!ft::is_integral<InputIterator>::value,
InputIterator>::type* = NULL)
: first(NULL), last(NULL), reserved_last(NULL), alloc(alloc) {
reserve(ft::distance(first, last));
for (InputIterator i = first; i != last; ++i) {
push_back(*i);
}
}
// コピー
vector(const vector& r)
: first(NULL), last(NULL), reserved_last(NULL), alloc(r.alloc) {
// コピー元の要素数を保持できるだけのストレージを確保
reserve(r.size());
// コピー元の要素をコピー構築
// destはコピー先
// [src, last)はコピー元
pointer dest = first;
for (const_iterator src = r.begin(), last = r.end(); src != last;
++dest, ++src) {
construct(dest, *src);
}
last = first + r.size();
}
vector& operator=(const vector& r) {
// 1. 自分自身への代入なら何もしない
if (this == &r) return *this;
// 2. 要素数が同じならば
if (size() == r.size()) { // 要素ごとにコピー代入
std::copy(r.begin(), r.end(), begin());
}
// 3. それ以外の場合で
else
// 予約数が十分ならば、
if (capacity() >= r.size()) {
// 有効な要素はコピー
std::copy(r.begin(), r.begin() + r.size(), begin());
// 残りはコピー構築
last = first + r.size();
for (const_iterator src_iter = r.begin() + r.size(), src_end = r.end();
src_iter != src_end; ++src_iter, ++last) {
construct(last, *src_iter);
}
}
// 4. 予約数が不十分ならば
else {
// 要素をすべて破棄
destroy_until(rbegin());
// 予約
reserve(r.size());
// コピー構築
for (const_iterator src_iter = r.begin(), src_end = r.end();
src_iter != src_end; ++src_iter, ++last) {
construct(last, *src_iter);
}
}
return *this;
}
make test でdiffなしなので、ここまでセーブ。
https://github.com/syamashi/ft_container/tree/663c8365b98a3cb0de88c5b051c8b83078e4638e/newgame
やっとアウトラインが固まりました。もう、スムーズに実装を進められます。何か喋れそうな関数と、自作テストケースを置いておきます。
・destructor
・assign
Q. 考察
1. capacityよりも大きいcountが来た場合、capacityから作り直し。
2. そうじゃなければ、sizeを合わせる。
/containers/vector.hpp
void assign(size_type count, const T& value) {
if (count > capacity()) {
clear();
deallocate();
first = allocate(count);
last = first;
reserved_last = first + count;
for (size_type i = 0; i < count; ++i) construct(last++, value);
} else if (count > size()) {
pointer ptr = first;
for (size_type i = 0; i < count; ++i) {
if (i < size())
*(ptr++) = value;
else
construct(last++, value);
}
} else {
clear();
for (size_type i = 0; i < count; ++i) construct(last++, value);
}
}
template <class InputIt>
void assign(InputIt src_first, InputIt src_last,
typename ft::enable_if<!ft::is_integral<InputIt>::value,
InputIt>::type* = NULL) {
size_type count = src_last - src_first;
if (count > capacity()) {
clear();
deallocate();
first = allocate(count);
last = first;
reserved_last = first + count;
for (InputIt head = src_first; head != src_last; ++head)
construct(last++, *head);
} else if (count > size()) {
pointer ptr = first;
for (size_type i = 0; i < count; ++i) {
if (i < size())
*(ptr++) = *src_first++;
else
construct(last++, *src_first++);
}
} else {
clear();
for (InputIt head = src_first; head != src_last; ++head)
construct(last++, *head);
}
}
・at
Q. std::out_of_range
A. atは配列外参照エラーを吐きます。
V.at(3)とV[3]は、機能は同じ。
ですが、operator[] はエラーを吐かないので、atが推奨されています?
でも、いつもV[]を使います。見やすいから。
/containers/vector.hpp
reference at(size_type i) {
if (i >= size()) throw std::out_of_range("index is out of range.");
return first[i];
}
const_reference at(size_type i) const {
if (i >= size()) throw std::out_of_range("index is out of range.");
return first[i];
}
・operator[]/ front / back / data
・begin / end / rbegin / rend
・empty
・size
・max_size
max_sizeですが、定義通りに作っても環境差異が出ます。
定義パターン1
std::numeric_limits<size_type>::max() / sizeof(value_type);
定義パターン2
alloc.max_size() をかえす
実装パターン3(これを採用)
ワカモレにベタ合わせ。
参考値
vector<char> : std::numeric_limits<size_type>::max() / 2
vector<int> : std::numeric_limits<size_type>::max() / 4
Q. なぜcharは2byteのサイズをとるのだろうか。
/containers/vector.hpp
size_type max_size() const {
size_t div = sizeof(value_type);
if (div == 1) ++div;
return std::numeric_limits<size_type>::max() / div;
}
・reserve
・capacity
・clear
・insert(本家を見ないで挑戦)
Q. 考察
・insert(V.begin()+1, 5)
resize: {1, 2, 3, 4, 0}
| last にconstructしておく
insert: {1, 0, 2, 3, 4}
~~~~~~~ insertする場所までずらす
insert: {1, 5, 2, 3, 4}
| value入れ
1. 新しいsizeがcapacityを超えるなら、新しいsizeで作り直し。
2. insert分の新しいポインタをlast以降に作る。
3. 新しいlastに、旧lastを埋めていく。
4. valueを挿入
/containers/vector.hpp
iterator insert(iterator pos, const T& value) {
size_type count = 1;
difference_type offset = pos - begin();
size_type c = capacity();
size_type pre_c = c;
size_type new_size = size() + count;
while (new_size > c) {
if (c == 0)
c = 1;
else
c = c << 1;
if ((c >> 1) != pre_c) throw std::overflow_error("vector::insert");
pre_c = c;
}
reserve(c);
for (; last != first + new_size; ++last) construct(last);
iterator tail = last - 1;
iterator range_end = begin() + offset + count - 1;
// pos + count - 1 までmemmove
for (; tail > range_end; --tail) *tail = *(tail - count);
iterator range_begin = begin() + offset - 1;
for (; tail > range_begin; --tail) *tail = value;
return begin() + offset;
}
void insert(iterator pos, size_type count, const T& value) {
if (count < 0) throw std::length_error("negative length.");
if (count == 0) return;
difference_type offset = pos - begin();
size_type c = capacity();
size_type pre_c = c;
size_type new_size = size() + count;
while (c < new_size) {
if (c == 0)
c = 1;
else
c = c << 1;
if ((c >> 1) != pre_c) throw std::overflow_error("vector::insert");
pre_c = c;
}
reserve(c);
for (; last != first + new_size; ++last) construct(last);
iterator tail = last - 1;
iterator range_end = begin() + offset + count - 1;
// pos + count - 1 までmemmove
for (; tail > range_end; --tail) *tail = *(tail - count);
iterator range_begin = begin() + offset - 1;
for (; tail > range_begin; --tail) *tail = value;
}
template <class InputIt>
void insert(iterator pos, InputIt src_first, InputIt src_last,
typename ft::enable_if<!ft::is_integral<InputIt>::value,
InputIt>::type* = NULL) {
difference_type count = src_last - src_first;
if (count < 0) throw std::length_error("negative length.");
if (count == 0) return;
difference_type offset = pos - begin();
size_type c = capacity();
size_type pre_c = c;
size_type new_size = size() + count;
while (c < new_size) {
if (c == 0)
c = 1;
else
c = c << 1;
if ((c >> 1) != pre_c) throw std::overflow_error("vector::insert");
pre_c = c;
}
reserve(c);
for (; last != first + new_size; ++last) construct(last);
iterator tail = last - 1;
iterator range_end = begin() + offset + count - 1;
// pos + count - 1 までmemmove
for (; tail > range_end; --tail) *tail = *(tail - count);
iterator range_begin = begin() + offset - 1;
--src_last;
for (; src_last > src_first - 1; --src_last) *tail-- = *src_last;
}
・erase
Q. 要素がないときのerase
A. no-op?だそうです。
// The iterator first does not need to be dereferenceable if first==last:
// erasing an empty range is a no-op.
ちなみに実行すると、本家クラッシュします。no-op == 未定義かも。
私はクラッシュアレルギーなのでブロックしておきます。
Q. invalid read
A. tutorialそのままだと、src+1して、++srcしたあと、読んではいけない箇所を読んでいるかも。修正しておきます。
/containers/vector.hpp
iterator erase(iterator pos) {
// The iterator first does not need to be dereferenceable if first==last:
// erasing an empty range is a no-op.
if (first == last) return NULL;
difference_type offset = pos - begin();
for (iterator src = pos + 1; src < end(); ++src) {
*(src - 1) = *src;
}
destroy(--last);
return (begin() + offset);
}
・push_back
・pop_back
・resize
・swap
普通のswapは普通に書けばいいです。
see also の std::swapへの対応もしておきます。
/containers/vector.hpp
namespace ft {
class vector {
...
void swap(vector& other) {
pointer save_first = other.first;
pointer save_last = other.last;
pointer save_reserved_last = other.reserved_last;
allocator_type save_alloc = other.alloc;
other.first = this->first;
other.last = this->last;
other.reserved_last = this->reserved_last;
other.alloc = this->alloc;
this->first = save_first;
this->last = save_last;
this->reserved_last = save_reserved_last;
this->alloc = save_alloc;
}
...
};
}; // namespace ft
namespace std {
template <class T, class Alloc>
void swap(ft::vector<T, Alloc>& lhs, ft::vector<T, Alloc>& rhs) {
lhs.swap(rhs);
}
} // namespace std
Q namespace std
A. std::swap(ft::vector, ft::vector)
を実行するときに、自作のvectorがどうswapするかを、std::swapは判断できないので、定義します。
この書き方は試行錯誤した結果。なかなかここに入らなくて苦労した。
・Non-member-functions
これですべてだと思います。
・ここまでの記録https://github.com/syamashi/ft_container/tree/2fbfd4e428f28ae62371d21a421b8ff32ffb48dd/newgame
・追記があるのは
/containers/vector.hpp
/testfiles/test_vector.cpp
/testfiles/main.cpp
void bitout(size_t n){
rep(i, 64){
cout << ((n>>(63-i))&1);
if (i%8==7) cout << " ";
}
cout << endl;
}
/testfiles/tester.hpp
//// helper
void bitout(size_t n);
Q. bitout
A. size_tを、64bit表示するデバッグヘルパーです。max_size_test()で使用。
次: (stack) https://note.com/syamashi/n/nbc30c2c89db4