Ce diaporama a bien été signalé.
Le téléchargement de votre SlideShare est en cours. ×

Алексей Кутумов, Вектор с нуля

Publicité
Publicité
Publicité
Publicité
Publicité
Publicité
Publicité
Publicité
Publicité
Publicité
Publicité
Publicité

Consultez-les par la suite

1 sur 43 Publicité

Plus De Contenu Connexe

Diaporamas pour vous (20)

Les utilisateurs ont également aimé (20)

Publicité

Similaire à Алексей Кутумов, Вектор с нуля (20)

Plus par Sergey Platonov (17)

Publicité

Plus récents (20)

Алексей Кутумов, Вектор с нуля

  1. 1. STD::VECTOR FROM SCRATCH Alexey Kutumov Senior software engineer at Kaspersky Lab
  2. 2. AGENDA vector interface vector_base methods w/o size modifications reserve insert erase … Profit!
  3. 3. VECTOR INTERFACE
  4. 4. INTERFACE template <typename Type, typename Alloc = allocator<Type>> class vector;
  5. 5. MINIMAL ALLOCATOR INTERFACE template <typename Type> struct allocator { typedef Type value_type; value_type* allocate(size_t n, const void* /* hint */ = nullptr) { return (value_type*)(::operator new(n * sizeof(value_type))); } void deallocate(value_type* p, size_t n) { ::operator delete(p, n * sizeof(value_type)); } };
  6. 6. VECTOR TYPES, PART 1 typedef ??? iterator; typedef ??? const_iterator; typedef ??? difference_type; typedef ??? size_type;
  7. 7. VECTOR TYPES, PART 1 typedef Type* iterator; typedef const Type* const_iterator; typedef ptrdiff_t difference_type; typedef size_t size_type;
  8. 8. VECTOR TYPES, PART 2 typedef value_type& reference; typedef const value_type& const_reference; typedef Type value_type; typedef Alloc allocator_type; typedef typename allocator_traits<Alloc>::pointer pointer; typedef typename allocator_traits<Alloc>::const_pointer const_pointer; typedef std::reverse_iterator<iterator> reverse_iterator; typedef std::reverse_iterator<const_iterator> const_reverse_iterator;
  9. 9. ALLOCATOR_TRAITS (TYPES, PART 1) template <typename Allocator> struct allocator_traits { typedef Allocator allocator_type; typedef typename Allocator::value_type value_type; typedef ??? pointer; typedef ??? const_pointer; typedef ??? size_type; (difference_type, void_pointer, const_void_pointer)
  10. 10. ALLOCATOR_TRAITS (TYPES, PART 1) template <typename Allocator> struct allocator_traits { typedef Allocator allocator_type; typedef typename Allocator::value_type value_type; typedef typename detail::get_pointer<Allocator>::type pointer; typedef typename detail::get_const_pointer<Allocator>::type const_pointer; typedef typename detail::get_size_type<Allocator>::type size_type;
  11. 11. GET_POINTER template <typename Allocator> struct get_pointer { template <typename Alloc2> static auto select_type(int)->typename Alloc2::pointer; template <typename Alloc2> static auto select_type(wrap_int)->typename Allocator::value_type*; typedef decltype(select_type<Allocator>(0)) type; };
  12. 12. ALLOCATOR_TRAITS (TYPES, PART 2) template <typename Allocator> struct allocator_traits { // ... typedef ??? propagate_on_container_copy_assignment; typedef ??? propagate_on_container_move_assignment; typedef ??? propagate_on_container_swap;
  13. 13. ALLOCATOR_TRAITS (METHODS) template <typename Alloc> struct allocator_traits { static pointer allocate(Alloc& a, size_type n, const void* hint = 0); static void deallocate(Alloc& a, pointer p, size_type n); template <typename Type, typename... Args> static void construct(Alloc& alloc, Type* ptr, Args&&... args); template <typename Type> static void destroy(Alloc& alloc, Type* ptr); static Alloc select_on_container_copy_construction(const Alloc& alloc); };
  14. 14. STD allocator<> iterator_traits<> ptrdiff_t make_unsigned<> declval<>() allocator_traits<> pointer_traits<> false_type, true_type integral_constant<> forward<>() remove_reference<> reverse_iterator<> iterator_traits<>
  15. 15. VECTOR_BASE
  16. 16. VECTOR_BASE template <typename Type, typename Allocator> struct vector_base: private Allocator { iterator m_first; iterator m_last; iterator m_endOfCapacity; vector_base(); vector_base(const Allocator& alloc); vector_base(vector_base&& other); vector_base(vector_base&& other, const Allocator& alloc); vector_base& operator=(vector_base&& other); vector_base& operator=(const vector_base& other); ~vector_base();
  17. 17. VECTOR_BASE vector_base() : vector_base(Allocator()) {} vector_base(const Allocator& alloc) : Allocator(alloc) { tidy(*this); } vector_base(vector_base&& other) : Allocator(std::move(static_cast<Allocator&&>(other))) { swap_base(*this, static_cast<vector_base&>(other)); tidy(other); } vector_base(vector_base&& other, const Allocator& alloc) : Allocator(alloc) { swap_base(*this, static_cast<vector_base&>(other)); tidy(other); }
  18. 18. VECTOR_BASE vector_base& operator=(vector_base&& other) { destroy_and_free(m_first, m_last, m_endOfCapacity, allocator_from(*this)); propagate_on_move_assignment(static_cast<Allocator&&>(other)); swap_base(*this, static_cast<vector_base&>(other)); tidy(other); return *this; } vector_base& operator=(const vector_base& other) { propagate_on_copy_assignment(static_cast<const Allocator&>(other)); return *this; }
  19. 19. VECTOR_BASE void propagate_on_move_assignment(Allocator&& other) { typedef vector_allocator_traits::propagate_on_container_move_assignment tag; propagate_on_move_assignment(forward<Allocator>(other), tag()); } void propagate_on_move_assignment(Allocator&& other, true_type) { static_cast<Allocator&>(*this) = std::move(other); } void propagate_on_move_assignment(Allocator&& /* other */, false_type) { // does nothing }
  20. 20. UNINITIALIZED_* template<typename FwdIt, typename Alloc, typename ... Args> void uninitialized_construct_a(FwdIt f, FwdIt l, Alloc& a, Args&&... args); template<typename InputIt, typename FwdIt, typename Alloc> FwdIt uninitialized_copy_a(InputIt first, InputIt last, FwdIt dest, Alloc& a); template<typename Type, typename Size, typename FwdIt, typename Alloc> FwdIt uninitialized_fill_n_a(FwdIt dest, Size n, const Type& v, Alloc& a);
  21. 21. UNINITIALIZED_CONSTRUCT template<typename FwdIt, typename Alloc, typename ... Args> void uninitialized_construct_a(FwdIt f, FwdIt l, Alloc& a, Args&&... args) { FwdIt c = f; try { for (; c != l; ++c) { allocator_traits<Alloc>::construct(a, addressof(*c), forward<Args>(args)...); } } catch (...) { destroy_range(first, c, alloc); throw; } }
  22. 22. STD move() swap() addressof() enable_if<> is_function<> uninitialized_fill_n() uninitialized_copy()
  23. 23. METHODS WITHOUT SIZE MODIFICATIONS
  24. 24. METHODS WITHOUT SIZE MODIFICATIONS allocator_type get_allocator() const; *reference at(size_type pos) *; *reference operator[](size_type pos) *; *reference front() *; *reference back() *; *pointer data() *; *iterator *begin() *; *iterator *end() *; size_type size() const; size_type capacity() const; size_type max_size() const; bool empty() const;
  25. 25. RESERVE
  26. 26. RESERVE void reserve(size_type newCapacity) { if (newCapacity > max_size()) { throw std::length_error(""); } if (newCapacity > capacity()) { iterator newFirst = alloc(allocator_from(*this), newCapacity); try { relocate(newFirst, newCapacity); } catch (...) { dealloc(allocator_from(*this), newFirst, newCapacity); throw; } } }
  27. 27. RELOCATE void relocate(iterator newFirst, size_type newCapacity) { iterator newLast = detail::uninitialized_copy_a( make_move_iterator(m_first), make_move_iterator(m_last), newFirst, allocator_from(*this)); destroy_and_free(m_first, m_last, m_endOfCapacity, allocator_from(*this)); m_first = newFirst; m_last = newLast; m_endOfCapacity = m_first + newCapacity; }
  28. 28. MOVE_ITERATOR template <typename Iterator> class move_iterator { Iterator m_current; public: typedef value_type&& reference; reference operator*() const { return std::move(*m_current); }
  29. 29. STD length_error logic_error basic_string<> logic_error basic_string<> ... exception make_move_iterator() move_iterator<>
  30. 30. INSERT
  31. 31. INSERT iterator insert(const_iterator pos, const Type& value); iterator insert(const_iterator pos, Type&& value); iterator insert(const_iterator pos, size_type count, const Type& value); template <typename InputIterator> typename enable_if<is_iterator<InputIterator>::value, iterator>::type insert(const_iterator pos, InputIterator first, InputIterator last); iterator insert(const_iterator pos, std::initializer_list<Type> ilist);
  32. 32. INSERT iterator insert(const_iterator pos, const Type& value) { return insert_with_perfect_fwd(pos, value); } iterator insert(const_iterator pos, Type&& value) { return insert_with_perfect_fwd(pos, std::forward<Type>(value)); } iterator insert(const_iterator pos, std::initializer_list<Type> ilist) { return insert_range(pos, ilist.begin(), ilist.end()); } iterator insert(const_iterator pos, size_type count, const Type& value) { return insert_range(pos, make_iterator(count, value), make_iterator()); }
  33. 33. COUNTING_ITERATOR template <typename Type> struct counting_iterator { size_t m_count; Type* m_value; counting_iterator& operator++() { --m_count; return *this; } reference operator*() { return *m_value; } };
  34. 34. INSERT_WITH_PERFECT_FWD template <typename... Args> iterator insert_with_perfect_fwd(const_iterator pos, Args&&... a) { difference_type offset = pos - cbegin(); grow(size() + 1); uninitialized_construct_a(m_last, m_last + 1, alloc(*this), forward<Args>(a)...); m_last += 1; rotate(m_first + offset, m_last - 1, m_last); return m_first + offset; }
  35. 35. ROTATE first new_first last
  36. 36. INSERT_RANGE template <typename InputIterator> insert_range(const_iterator pos, InputIterator first, InputIterator last) { difference_type offset = pos - cbegin(); difference_type count = std::distance(first, last); grow(size() + count); detail::uninitialized_copy_a(first, last, m_last, allocator_from(*this)); m_last += count; rotate(m_first + offset, m_last - count, m_last); return m_first + offset; }
  37. 37. STD rotate() iter_swap() distance() is_base_of<>
  38. 38. ERASE
  39. 39. ERASE iterator erase(const_iterator pos) { return erase(pos, pos + 1); } iterator erase(const_iterator first, const_iterator last) { iterator newLast = rotate(cast_from_const(first), cast_from_const(last), m_last); detail::destroy_range(newLast, m_last, allocator_from(*this)); m_last = newLast; return cast_from_const(first); }
  40. 40. OTHER METHODS
  41. 41. OTHER METHODS void assign(size_type count, const Type& value) { erase(m_first, m_last); resize(count, value); } template <typename... Args> iterator emplace(const_iterator pos, Args&&... args) { return insert_with_perfect_fwd(pos, std::forward<Args>(args)...); } void push_back(const value_type& value) { insert_with_perfect_fwd(end(), value); }
  42. 42. CLOC --------------------------------------------------------------------------- Language files blank comment code --------------------------------------------------------------------------- C/C++ Header 20 428 33 1449 C++ 3 271 8 995 --------------------------------------------------------------------------- SUM: 23 699 41 2444 ---------------------------------------------------------------------------
  43. 43. LET'S TALK? alexey.kutumov@gmail.com https://github.com/prograholic/simple_vector

Notes de l'éditeur

  • Добрый день, меня зовут Алексей Кутумов, я являюсь старшим разработчиком в ЛК
  • План:

    Что я хочу показать:
    Представим, что мы разработчики стандартной библиотеки C++, что нам нужно от стандартной библиотеки, чтобы мы смогли реализовать std::vector.

    В этом докладе мы возьмем только компилятор (я делал с помощью компилятора из MSVS 2015 – cl.exe версии 19). Он довольно хорошо умеет C++11 и, возможно, что-то из C++14. И сами построим свою стандартную библиотеку.

    Вообще, изначально, я хотел сделать основной целью доклада мысль, что реализовать std::vector – это просто. Но потом я подумал, и понял, что это неинтересно. Ну, во-первых, это действительно просто сделать (как мне кажется), ну а во-вторых, это не совсем интересно. Понятно, что упрощение реализации неизменно ведет к сужению кейсов использования, или ухудшении производительности, ухудшении гарантий и т.д. В конце концов, int* x = new int[10], чем не вектор?

    Итак, какой у нас план:

    Сначала мы осмотрим интерфейс нашего вектора – только декларацию, и посмотрим, что-же из стандартной библиотеки нам нужно, чтобы задекларировать интерфейс вектора.

    Затем, мы будем реализовывать вектор. Придумаем такую штуку как vector_base, поймем, для чего она нужна.

    После этого, мы пойдем реализовывать вектор потихоньку.

    Начнем сначала с методов, которые не изменяют размер вектора.

    Потом реализуем метод reserve, поговорим подробнее о гарантиях, которые он дает, и как мы их достигаем.
    Потом реализуем метод insert, который вставляет один элемент, поговорим о его гарантиях.
    Затем реализуем insert с парой итераторов.
    Затем реализуем insert с n элементами.

    После этого, реализуем erase с парой итераторов.

    А после этого, внезапно, реализуем весь остальной функционал вектора.
  • Вот так выглядит декларация вектора, наверняка все знают, даже наверное наизусть эту декларацию.

    Итак, давайте я еще раз поясню, я буду показывать какие-то куски кода, потом мы будем разбирать все конструкции, которые в этом куске кода есть, при этом, будут появляться новые куски кода, в общем, вот такая рекурсия.

    Да, и еще, если не сказано обратное, то по умолчаию считаем, что мы находимся внутри пространства std

    Итак, тут нам все понятно – два параметра шаблона.

    Первый параметр – это тип объектов, которые будут храниться в векторе
    Второй параметр – это аллокатор, он имеет значение по умолчанию – стандартный аллокатор.

    Итак, мы плавно переходим к обсуждению аллокаторов.
  • Здесь представлен интерфейс аллокатора, как видно, здесь тоже нет никаких сложностей.

    От аллокатора требуется совсем немного, суметь выделить и удалить память, ну и кроме этого, объявить тип value_type. Кто нибудь смотрел доклад Александреску про аллокаторы? Поднимите руки.
    Отлично, тогда следующий вопрос вы пропускаете.

    Итак, вопрос, кто нибудь знает, зачем изначально были придуманы аллокаторы? Изначально аллокаторы были придуманы Александром Степановым для того чтобы контейнеры не зависели от модели памяти (если кто знает, то в 16-битном режиме есть разные типы указателей, near pointer, far pointer). Как раз аллокаторы абстрагировали это знание предоставлением нужного типа указателей и механизма аллокации и деаллокации памяти.

    В С++11 ввели понятие minimal allocator interface, теперь от аллокатора требуется совсем немного обязательных вещей:

    Объявить value_type – тип объектов, которые аллоцируются этим аллокатором.
    Объявить функции allocate и deallocate для управления памятью.


    Итак, с аллокатором тоже все понятно. В принципе, мы можем даже предоставить реализацию этого аллокатора
  • Снова возвращаемся к вектору.

    Итак, давайте поговорим про типы, которые вектор предоставляет клиентам. Я опустил заголовок класса и разбил декларации типов на два слайда.

    Итак, на этом слайде представлены типы, которые отдаются на откуп реализации.
    Давайте вспомним, какие требования стандарт предъявляет к этим типам?

    Давайте сначала поговорим про difference_type и size_type

    Какие требования стандарт предъявляет к двум следующим типам?
    difference_type – это должен быть тем же самым типом что и тип, значение которого возвращается при вычислении разницы двух итераторов.
    size_type – это тип, который может представить любое неотрицательное число типа difference_type

    Кто знает, какие требования к типу итератора предъявляет стандарт? Итератор должен удовлетворять модели (или концепту) RandomAccessIterator.

    Что самое простое подходит – обычный указатель, его и возьмем.
  • Итого, для этих типов, мы выбираем самую простую реализацию, которая удовлетворяет всем требованиям.

    В принципе, нас эта полностью устраивает.
  • Теперь, что касается остальных типов. С одной стороны все проще, в стандарте явно прописано, что эти типы должны быть такими, как указано на этом слайде.

    Я уверен, многие из вас знакомы с std::reverse_iterator, поднимите руки, кто знает что такое reverse_iterator?

    Отлично, тогда, я думаю, нет смысла подробно объяснять что это такое – это адаптер, который сам является итератором – он определяет операции инкремента, декремента, сравнения и т.д. Но при этом, операции инкремента и декремента работают противоположным образом по сравнению с базовым итератором – в обратную сторону.
  • Мы же подробно поговорим про allocator_traits.

    allocator_traits – относительно новая сущность, которая появилась в стандарте C++11. Что она из себя представляет, как и любые traits она позволяет получать и использовать различные свойства аллокаторов – определяемые типы, вызывать методы аллокатора и пр. единым способом.

    Давайте посмотрим на интерфейс, сначала типы, причем типы мы тоже разделим на несколько частей.

    Сюда же относятся и типы difference_type, void_pointer и const_void_pointer, но я не стал их указывать, так как места на слайдах не очень много, но мы про них помним.

    Итак, что нам говорит стандарт – первые два типа – тут все понятно, откуда взять остальные типы? У кого есть какие идеи?

    На самом деле стандарт говорит следующее – если тип Allocator предоставляет нужный тип, то нужно взять его, иначе нужно взять другой тип, причем для каждого типа стандарт говорит что нужно брать.

    Вот давайте разберем pointer – про него стандарт говорит следующее. Если Allocator предоставляет такой тип, то взять его. Если нет, то нужно взять указатель на Allocator::value_type.

    Ну вот, давайте разберем, как это можно реализовать на примере pointer
  • Вообще, что касается работы с типами на этапе компиляции, то 90% случаев – это SFINAE, остальные 10% это полный перебор типов, перегрузка и пр.

    Собственно, в приведенном примере я так и реализовал с помощью detail::get_pointer.

    Давайте на него посмотрим
  • Вот так реализован шаблон get_pointer, который выбирает нужный тип в зависимости от наличия типа pointer.

    Здесь приведены декларации двух шаблонных статических методов deduce_type.

    Первый метод принимает на вход int и возвращает Alloc2::pointer.
    Второй метод принимает на вход нечто по имени wrap_int и возвращает другой тип – указатель на value_type.

    Ну и в самом низу мы декларируем тип type на основе типа возвращаемого фукнцией select_type

    Итак, осталась пара вопросов, почему вторая функция select_type принимает на вход аргумент типа wrap_int и что это за аргумент?

    Ну здесь на самом деле все просто, wrap_int – это структура, у которой объявлен один единственный конструктор, принимающий на вход int. Для чего это сделано – для того чтобы управлять процессом выбора перегруженной функции, в случае если обе функции подходят.

    Смотрите, если наш Allocator предоставляет тип pointer, то первая функция будет определена, точно также будет определена и вторая функция. Более того, вторая функция всегда будет определена, потому что тип value_type* всегда определен – стандарт требует наличие value_type. Таким образом компилятор встает перед нелегким выбором, ему нужно выбрать одну из функций, вот он и выбирает ту, у которой входной аргумент получается явно, без неявных конверсий (и вызовов конструкторов).

    Кстати, может кто нибудь сказать, как еще можно управлять процессом выбора перегруженной функции?

    Ну можно вместо wrap_int воткнуть … - эллипсис. Но мы использовали wrap_int, потому что он нам пригодится в еще одном месте, там где эллипсис использовать нельзя.
  • Но это еще не все типы, которые нам интересны, стандарт C++11 привносит еще три типа. propagate_on_container_*.

    Кто нибудь знает, для чего эти типы нужны?

    Эти три типа определяют поведение контейнеров при вызове оператора копирующего присваивания, при вызове оператора перемещающего присваивания, и при вызове метода swap.

    Собственно, механизм определения этих типов точно такой же как и остальных типов allocator_traits, SFINAE магия решает. Этиы типы могут принимать значения либо true_type либо false_type

    Теперь, давайте разберем. Допустим propagate == true_type, тогда аллокатор участвует в операции. Это может быть нужно, если аллокатор имеет состояние (например, держит какую-то арену), тогда при копировании он может сделать новую арену, или же расшарить существующую и т.д.

    Если propagate == false_type, то аллокатор не участвует в операции, это может быть полезно, если аллокатор не имеет состояния, например, зовет malloc и free. Такому аллокатору нечего копировать.
  • Вот методы, которые предоставляются allocator_traits, с ними ситуация аналогичная

    Первые два метода обязаны предоставляться аллокатором, поэтом реализация просто их зовет.

    С остальные методами ситуация следующая: если аллокатор предоставляет такие методы, то позвать их, иначе позвать стандартные реализации.

    Если с первыми 4 методами все более или менее поняно, то последний метод стоит обговорить отдельно.

    Итак вопрос, кто нибудь знает для чего нужен этот метод?

    Этот метод используется в контейнерах при реализации копирующего конструктора. Он определяет, каким образом нам копировать аллокатор. В принципе, мы опять, можем не копировать аллокатор, а создать новый и его вернуть. Или же сделать что-то еще.

    Идея реализации абсолютно аналогична выбору типов. Точно также SFINAE магия и приоритет перегруженных методово позволяет нам выбрать нужный.
  • Итак, с чего мы все начали, мы определяли интерфейс вектора. И уже только для того чтобы задекларировать часть публичного интерфейса вектора нам пришлось реализовать следующее подмножество std.

    Я намеренно не обсуждал многие вещи, иначе мы бы до конца презентации обсуждали их, а нам надо еще реализацию вектора сделать.

    Если кому интересно, я могу после доклада, в кулуарах рассказать, как реализуются те или иные сущности в стандартной библиотеке.

    Мы еще не рассматривали методы вектора и пр, при рассмотрении методов еще добавится парочка сущностей.
  • Итак, давайте теперь перейдем к реализации нашего вектора.

    Что такое vector_base, зачем я выделил его?

    На самом деле все просто. Для того чтобы реализовывать вектор нужно определиться с его представлением. Как мы будем хранить начало вектора, как будем хранить размер, как будем хранить диапазон выделенной памяти?

    Кроме этого в этом классе мы соберем утилитарные функции по управлению вектором.
  • Опять, мы рассмотрим интерфейс нашего вектора по частям.

    Сначала конструкторы, операторы присваивания и деструктор.


    Здесь мы, кроме всего прочего определяем и внутреннее представление вектора, мы берем три итератора – итератор на начало, на конец данных и на конец выделенной области.


    Кстати, кто нибудь может сказать, зачем я здесь использую приватное наследование от аллокатора?

    Это для оптимизации. Есть такая оптимизация Empty base class optimization, если базовый класс пустой, то реализация вправе не выделять под него память при наследовании. Таким образом, sizeof(vector_base) будет равен 3* sizeof(pointer) в случае, если аллокатор пустой.

    Заметьте, что такого нельзя добиться если аллокатор объявить членом. Так как стандарт требует чтобы объект (даже пустой) всегда имел уникальный адрес.
  • Вот реализация конструкторов. Как видите она довольно простая.

    Первый конструктор делегирует второму.
    Второй конструктор копирует аллокатор и выставляет m_first, m_last и m_endOfCapacity в значения по умолчанию (0).
    Третий конструктор – забирает у other его указатели и аллокатор.
    Четвертый конструктор забирает у other указатели, и ставит новый аллокатор.

    Реализация статических методов swap_base и tidy тривиальна.

    В первом случае мы свопаем указатели, во втором случае мы их зануляем.

    Здесь нет ничего сложного
  • Теперь давайте рассмотрим операторы move и copy assignment.

    В принципе, они тоже довольно тривиальны.

    В первом случае мы освобождаем this если он чем то владел
    Затем обрабатываем аллокатор. Помните, мы обсуждали, что в allocator_traits задается стратегия как использовать аллокатор при move присваивании, вот здесь мы это обрабатываем.

    Ну а дальше просто – мы свопаем this и other, а потом обнуляем other.

    Оператор копирующего присваивания еще проще, в нем мы обрабатываем только аллокатор, все остальные действия будет производиться в другом месте.
  • Вот так реализована функция propagate_on_move_assignment

    Мы диспетчеризуем вызов двум другим функциям. И в зависимости от типа мы либо муваем аллокатор, либо вообще ничего не делаем!

    Точно также реализован и propagate_on_copy_assignment.
  • Теперь давайте опять ненадолго отвлечемся от вектора и поговорим про семейство unitialized_ функций.

    На самом деле, 2 из 3-х функций вам должны быть знакомы. Они практически полностью повторяют функции std::uninitialized_copy и std::unititalized_fill_n.

    Две остальные функции –unitialized_construct_a – дополнительная вспомогательная функция, котора тоже нам понадобится.
  • Эта функция тоже довольно проста. Мы получаем на вход итератор с неинициализированной памятью, и вызываем construct с переданными аргументами.

    Как мы уже видели ранее, в реализации construct, там вызывается placement new.

    Все остальные функции реализуются аналогичным образом.

    Один вопрос, для чего здесь используется функция addressof?

    Все очень просто – тип, по которым итерируется итератор FwdIt может иметь перегруженный оператор &, и поэтому записать &*c будет некорректно. Функция addressof как раз предназначена для получения адреса из ссылки.

    Важный вопрос, какую гарантию относительно возникновения исключений дает эта функция?
    Эта функция дает сильную гарантию. Если возникнет исключение, то функция оставит входной диапазон неизменным относительно начала выполнения этой функции. То есть никакого эффекта не будет.

    Кстати, это касается и других функций, это очень важно, так как эти функции являются строительными кирпичиками для нашего вектора.
  • Итак, к чему мы пришли, у нас есть реализация базовой части вектора, которая прячет особенности реализации и предоставляет удобные обертки для дальнейшей реализации.


    Что касается стандартной библиотеки, то вот несколько новых сущностей, которые мы добавили в библиотеку.

    Что касается последних двух функций, то их легко получить из _a версий, просто предоставив стандартный аллокатор в качестве аргумента. И это будет удовлетворять стандарту.
  • Вот список константных методов – их реализация довольно проста, поэтому я не буду здесь ее приводить.

    Практически все функции однострочные.

    Звездочками я обозначил либо const, либо cr префиксы для функций begin и end.
  • Итак, нам осталось реализовать три метода вектора.
    Начнем с reserve
  • Вот реализация функции reserve. В принципе, реализация ее довольно проста:

    Проверили входные аргументы, потом выделили память для нового хранилища.

    И потом позвали функцию relocate, которая переместит правильным образом наш вектор в новый регион памяти.

    Кстати, теперь нас интересуют гарантии исключений для этой функции. Кто помнит, какие требования в стандарте?

    Сильная гарантия – если возникнет исключение в конструкторе копирования объектов, или при выделении памяти то функция не имеет эффекта.

    Если же возникнет исключение в move конструкторе, то поведение не определено. Ну это и понятно, если конструктор копирования не изменяет исходный объект, то move конструктор изменяет, и нет гарантии, что мы сможем вернуть объект в изначальное состояние.

    Текущая реализация не ухудшает сильной гарантии. Остается только посмотреть на реализацию relocate.
  • Вот реализация функции relocate.

    Как мы помним, функция uninitialized_copy предъявляет сильную гарантию, но здесь мы делаем финт. Мы делаем move_iterator из обычного итератора, таким образом, вместо копирования будет производиться перемещение, если оно доступно.

    Таким образом, мы вполне легально можем переместить все объекты без копирования, что обычно дешевле.

    Ну и в конце остается рутина – очистить и удалить старый регион и переназначить указатели на новое хранилище.
  • Вот самая интересная часть move_iterator – оператор разыменования возвращает rvalue ссылку, а не lvalue ссылку.

    Таким образом, если у типа есть move конструктор или move оператор присваивания, то именно он и вызовется.
  • Итак, после реализации метода resever наша стандартная библиотека расширилась еще немного, и обрела реализацию иерархии исключений и реализацию basic_string!!! Что удивительно, реализация basic_string тоже использует length_error для реализации метода reserve, а конструктор length_error принимает на вход std::string, получается этакая рекурсия!!

    На самом деле, я не реализовывал basic_string, вместо этого я реализовал шаблонный конструктор у length_error, который принимает любой аргумент, в том числе и строку.
  • Второй метод – вставка элемента или нескольких в заданную позицию
  • Вот я перечислил методы insert, которые должны быть реализованы.

    Давайте вспомним какие требования к безопасности к ним предъявляет стандарт.
    Стандарт говорит, что если вставляем один элемент в конец и он CopyInsertable или move конструктор не кидает исключений, то в случае исключения функция не имеет эффекта - строгая гарантия. Во всех остальных случаях – базовая гарантия, инварианты вектора не нарушены, состояние не определено, но валидно.

    Кто может сказать, для чего в 4-й декларации используется enable_if? Чтобы различить 3 и 4 метод, например, позвали insert(pos, 0, 0). Какую функцию выбрать? Компилятор не сможет. И вот в этом случае нужен наш enable_if.

    Итак, давайте посмотрим на интерфейс и подумаем, может мы сможем выразить некоторые методы через другие.
  • В принципе, мы 5 методов можем свести к двум.

    Благодаря тому, что у нас есть perfect forwarding, то мы можем одинаково передавать любые типы ссылок. Чем мы и воспользовались здесь. То есть две реализации свелись к одной.


    Вторая часть insert – вставляет диапазон.

    Я здесь не стал указывать пятую реализацию, которая принимает пару итераторов. Ее реализация не менее тривиальна чем представленные здесь.

    Отдельно стоит сказать про последнюю реализацию. Изначально казалось, что она ближе к первым двум, но на самом деле нет, и я сейчас объясню. Мы можем сделать специальный итератор, который просто считает свою позицию, и при разыменовании всегда возвращает один и тот же элемент.

    Таким образом, мы можем сделать такой итератор и передать его в insert_range.
  • А вот и реализация этого counting iterator – у нас есть счетчик, текущее положение итератора, и оператор разыменования, который возвращает всегда одно и тоже значение.

    Ну и операции инкремента и декремента оператора сответственно, уменьшают или увеличивают значение счетчика.

    Таким образом мы получаем хитрый итератор, который всегда возвращает одно и тоже значение

    Кстати, какая категория итератора может быть у этого итератора – random access iterator?
  • Итак, давайте рассмотрим реализацию метода

    Итак, что мы делаем:
    Вызываем метод grow, запрашиваем новый размер.
    Потом конструируем один элемент в КОНЦЕ вектора, увеличиваем размер вектора.
    Затем вызываем функцию из стандартной библиотеки rotate, которая меняет местами элементы в последовательности так, что нужный элемент оказывается на своем месте.

    Ну вот и все!

    Давайте посмотрим более детально.

    grow – это очень простой метод, он по сути реализует стратегию роста capacity вектора. По сути он вычисляет новое значение capacity, так чтобы оно было не меньше запрашиваемого и текущего capacity, и потом зовет reserve.

    Reserve как мы помним, дает строгую гарантию.

    unitialized_construct_a – мы уже разбирали, он конструирует объект в памяти.

    После этого мы увеличиваем размер вектора на 1.

    А потом, происходит самая магия, мы вращаем элементы в векторе так, чтобы последний элемент встал на место first + offset (то есть на место нашего pos), а все последующие элементы встанут за ним в том же порядке, в котором они и были.

    Вспоминаем про наши требования – вставка в конец одного элемента должна иметь строгую гарантию.

    Вставка в конец элемента – это значит, что pos == end(), мы помним, что вставляем всегда перед позицией.

    А это значит, что элемент уже на своем месте, и ничего вращать не надо!

    Получается, что все условия соблюдены!
  • Единственный слайд с картинкой.

    Итак, реализация алгоритма rotate довольно простая:

    rotate на вход принимает 3 итератора, начало и конец, и еще один параметр – новое начало, элементы от new_firtst до end будут идти перед элементами [first, new_first).

    Вот я здесь привел картинку, которая, как мне кажется поясняет работу алгоритма.

    Я не буду приводить реализацию этого алгоритма, она довольно простая.

    Вообще говоря, функция rotate имеет базовую гарантию, но в случае, когда ничего делать не надо – эта функция как раз ничего не делает.
  • Как видите, реализация метода insert_range очень похожа на реализацию предыдущего метода.

    Гарантии здесь те же самые.

    Кстати, кто может сказать, какое здесь сделано допущение?

    Мы используем std::distance, вообще говоря, distance определена для InputIterator. Но InputIterator вообще говоря только однопроходные, то есть повторно итерироваться нельзя. Поэтому здесь надо диспетчеризировать по категории итератора.

  • Итак, для реализации insert нам понадобились следующие новые сущности из стандартной библиотеки.

    std::rotate, которая в свою очередь зовет iter_swap

    distance, для реализации метода insert_range – для подсчета расстояния между итераторами.
    is_base_of, для определения категории итератора
  • Вот и реализация всех методов erase.

    Стандарт требует только базовой гарантии для методов.

    Мы опять используем фокус с rotate, чтобы ненужные элементы переместить в конец. После того как мы переместили в конец, мы зовем у них деструкторы и обновляем m_last, заметьте, capacity мы не трогали.
  • Итого, реализация вектора с нуля заняла у меня полторы тыщи строк. При этом я реализовал подмножество
    type_traits, algorithm, utility, memory, iterator. В общем всех подсистем понемногу.

    Также почти 1000 строк кода у меня заняли тесты на вектор.
  • Дискас!

×