Repository: WG21-SG14/SG14 Branch: master Commit: c92614381100 Files: 30 Total size: 794.4 KB Directory structure: gitextract_rzo9ruc3/ ├── .gitignore ├── .travis.yml ├── CMakeLists.txt ├── Docs/ │ ├── Proposals/ │ │ ├── D0447R16 - Introduction of hive to the Standard Library.html │ │ ├── Fixed_Point_Library_Proposal.md │ │ ├── p0037.html │ │ ├── rawstorage.html │ │ ├── ring_proposal_r5.tex │ │ ├── uninitialized.html │ │ └── unstable_remove.html │ ├── fixed_point.md │ └── plf_licensing.txt ├── README.md ├── SG14/ │ ├── algorithm_ext.h │ ├── flat_map.h │ ├── flat_set.h │ ├── inplace_function.h │ ├── plf_colony.h │ ├── ring.h │ └── slot_map.h └── SG14_test/ ├── SG14_test.h ├── flat_map_test.cpp ├── flat_set_test.cpp ├── inplace_function_test.cpp ├── main.cpp ├── plf_colony_test.cpp ├── ring_test.cpp ├── slot_map_test.cpp ├── uninitialized_test.cpp └── unstable_remove_test.cpp ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Windows image file caches Thumbs.db ehthumbs.db # Folder config file Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msm *.msp # Windows shortcuts *.lnk # ========================= # Operating System Files # ========================= # OSX # ========================= .DS_Store .AppleDouble .LSOverride # Thumbnails ._* # Files that might appear on external disk .Spotlight-V100 .Trashes # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk *.tlog VS2015/SG14/SG14_test/Release/vc140.pdb *.obj VS2015/SG14/SG14_test/Release/SG14_test.log *.pdb VS2015/SG14/SG14_test/Debug/vc120.idb *.idb *.ipdb VS2015/SG14/Release/SG14_test.iobj *.ilk VS2015/SG14/Release/SG14_test.exe *.exe VS2015/SG14/.vs/SG14/v14/.suo *.opensdf VS2015/SG14/SG14.sdf *.suo VS2015/SG14/SG14.sdf VS2015/SG14/SG14_test/Debug/SG14_test.log # Xcode xcuserdata # CLion cmake/.idea /.vs /.vs/SG14/v15 /.vs/slnx.sqlite /.vs/SG14/v15/Browse.VC.opendb /.vs/SG14/v15/Browse.VC.db /.vs/ProjectSettings.json /build/ ================================================ FILE: .travis.yml ================================================ language: cpp before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; brew upgrade cmake; brew install cmake; fi - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then eval "${MATRIX_EVAL}"; DEPS_DIR="${TRAVIS_BUILD_DIR}/deps"; mkdir ${DEPS_DIR} && cd ${DEPS_DIR}; CMAKE_URL="https://cmake.org/files/v3.10/cmake-3.10.0-Linux-x86_64.tar.gz"; mkdir cmake && travis_retry wget --no-check-certificate --quiet -O - ${CMAKE_URL} | tar --strip-components=1 -xz -C cmake; export PATH=${DEPS_DIR}/cmake/bin:${PATH}; cd ..; fi matrix: include: - os: windows env: - CONFIG=Release - os: windows env: - CONFIG=Debug - os: osx osx_image: xcode9.2 env: - CONFIG=Release - os: osx osx_image: xcode9.2 env: - CONFIG=Debug - os: linux dist: trusty addons: apt: sources: - ubuntu-toolchain-r-test packages: - gcc-5 - g++-5 env: - MATRIX_EVAL="CC=gcc-5 && CXX=g++-5 && CONFIG=Debug" - os: linux dist: trusty addons: apt: sources: - ubuntu-toolchain-r-test packages: - gcc-5 - g++-5 env: - MATRIX_EVAL="CC=gcc-5 && CXX=g++-5 && CONFIG=Release" - os: linux dist: trusty addons: apt: sources: - ubuntu-toolchain-r-test packages: - gcc-6 - g++-6 env: - MATRIX_EVAL="CC=gcc-6 && CXX=g++-6 && CONFIG=Debug" - os: linux dist: trusty addons: apt: sources: - ubuntu-toolchain-r-test packages: - gcc-6 - g++-6 env: - MATRIX_EVAL="CC=gcc-6 && CXX=g++-6 && CONFIG=Release" - os: linux dist: trusty addons: apt: sources: - ubuntu-toolchain-r-test packages: - gcc-7 - g++-7 env: - MATRIX_EVAL="CC=gcc-7 && CXX=g++-7 && CONFIG=Debug" - os: linux dist: trusty addons: apt: sources: - ubuntu-toolchain-r-test packages: - gcc-7 - g++-7 env: - MATRIX_EVAL="CC=gcc-7 && CXX=g++-7 && CONFIG=Release" - os: linux dist: trusty addons: apt: sources: - ubuntu-toolchain-r-test packages: - gcc-8 - g++-8 env: - MATRIX_EVAL="CC=gcc-8 && CXX=g++-8 && CONFIG=Debug" - os: linux dist: trusty addons: apt: sources: - ubuntu-toolchain-r-test packages: - gcc-8 - g++-8 env: - MATRIX_EVAL="CC=gcc-8 && CXX=g++-8 && CONFIG=Release" script: - mkdir build && cd build && cmake .. && cmake --build . && ./bin/sg14_tests notifications: email: on_success: never on_failure: always ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.10) project(sg14 CXX) find_package(Threads REQUIRED) # Prefer C++17, downgrade if it isn't available. set(CMAKE_CXX_STANDARD_REQUIRED OFF) set(CMAKE_CXX_STANDARD 17) set(SG14_INCLUDE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/SG14") set(SG14_TEST_SOURCE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/SG14_test") # Output binary to predictable location. set(BINARY_OUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/bin) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${BINARY_OUT_DIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${BINARY_OUT_DIR}) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${BINARY_OUT_DIR}) foreach(OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES}) string(TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${BINARY_OUT_DIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${BINARY_OUT_DIR}) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${BINARY_OUT_DIR}) endforeach(OUTPUTCONFIG CMAKE_CONFIGURATION_TYPES) link_directories(${CMAKE_CURRENT_BINARY_DIR}/lib) # For future use with testing library. ## # Project ## add_library(${PROJECT_NAME} INTERFACE) include(GNUInstallDirs) target_include_directories(${PROJECT_NAME} INTERFACE $ $ ) ## # Unit Tests ## set(TEST_SOURCE_FILES ${SG14_TEST_SOURCE_DIRECTORY}/main.cpp ${SG14_TEST_SOURCE_DIRECTORY}/flat_map_test.cpp ${SG14_TEST_SOURCE_DIRECTORY}/flat_set_test.cpp ${SG14_TEST_SOURCE_DIRECTORY}/inplace_function_test.cpp ${SG14_TEST_SOURCE_DIRECTORY}/plf_colony_test.cpp ${SG14_TEST_SOURCE_DIRECTORY}/ring_test.cpp ${SG14_TEST_SOURCE_DIRECTORY}/slot_map_test.cpp ${SG14_TEST_SOURCE_DIRECTORY}/uninitialized_test.cpp ${SG14_TEST_SOURCE_DIRECTORY}/unstable_remove_test.cpp ) set(TEST_NAME ${PROJECT_NAME}_tests) add_executable(${TEST_NAME} ${TEST_SOURCE_FILES}) target_link_libraries(${TEST_NAME} ${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT}) target_include_directories(${TEST_NAME} PRIVATE "${SG14_TEST_SOURCE_DIRECTORY}") # Compile options if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") target_compile_options(${TEST_NAME} PRIVATE -Wall -Wextra -Werror) elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") target_compile_options(${TEST_NAME} PRIVATE -Wall -Wextra -Werror) set_source_files_properties(${SG14_TEST_SOURCE_DIRECTORY}/plf_colony_test.cpp PROPERTIES COMPILE_FLAGS "-Wno-unused-parameter" ) if (CMAKE_CXX_COMPILER_VERSION MATCHES "^7.*") set_source_files_properties(${SG14_TEST_SOURCE_DIRECTORY}/slot_map_test.cpp PROPERTIES COMPILE_FLAGS "-Wno-error=unused-variable -Wno-error=unused-but-set-variable") # Fix gcc7 issues with structured bindings message("Disabled -Wunused-variable and -Wunused-but-set-variable for gcc ${CMAKE_CXX_COMPILER_VERSION}.") endif() elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT ${TEST_NAME}) target_compile_options(${TEST_NAME} PRIVATE /Zc:__cplusplus /permissive- /W4 /WX) add_definitions(-DNOMINMAX -D_SCL_SECURE_NO_WARNINGS) set_source_files_properties(${SG14_TEST_SOURCE_DIRECTORY}/plf_colony_test.cpp PROPERTIES COMPILE_FLAGS "/wd4127") # Disable conditional expression is constant, use if constexpr endif() install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME}_targets) install(EXPORT ${PROJECT_NAME}_targets NAMESPACE ${PROJECT_NAME}:: FILE ${PROJECT_NAME}-config.cmake DESTINATION "${CMAKE_INSTALL_DATADIR}/cmake/${PROJECT_NAME}" ) install(DIRECTORY "${SG14_INCLUDE_DIRECTORY}" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") ================================================ FILE: Docs/Proposals/D0447R16 - Introduction of hive to the Standard Library.html ================================================ Introduction of std::hive to the standard library Audience: LEWG, SG14, WG21
Document number: D0447R16
Date: 2021-06-21
Project: Introduction of std::hive to the standard library
Reply-to: Matthew Bentley <mattreecebentley@gmail.com>

Introduction of std::hive to the standard library

Table of Contents

  1. Introduction
  2. Questions for the committee
  3. Motivation and Scope
  4. Impact On the Standard
  5. Design Decisions
  6. Technical Specification
  7. Acknowledgements
  8. Appendixes:
    1. Basic usage examples
    2. Reference implementation benchmarks
    3. Frequently Asked Questions
    4. Specific responses to previous committee feedback
    5. Typical game engine requirements
    6. Time complexity requirement explanations
    7. Why not constexpr?
    8. Reference implementation differences and link

Revision history

I. Introduction

The purpose of a container in the standard library cannot be to provide the optimal solution for all scenarios. Inevitably in fields such as high-performance trading or gaming, the optimal solution within critical loops will be a custom-made one that fits that scenario perfectly. However, outside of the most critical of hot paths, there is a wide range of application for more generalized solutions.

hive is a formalisation, extension and optimization of what is typically known as a 'bucket array' container in game programming circles; similar structures exist in various incarnations across the high-performance computing, high performance trading, 3D simulation, physics simulation, robotics, server/client application and particle simulation fields (see: https://groups.google.com/a/isocpp.org/forum/#!topic/sg14/1iWHyVnsLBQ).

The concept of a bucket array is: you have multiple memory blocks of elements, and a boolean token for each element which denotes whether or not that element is 'active' or 'erased', commonly known as a skipfield. If it is 'erased', it is skipped over during iteration. When all elements in a block are erased, the block is removed, so that iteration does not lose performance by having to skip empty blocks. If an insertion occurs when all the blocks are full, a new memory block is allocated.

The advantages of this structure are as follows: because a skipfield is used, no reallocation of elements is necessary upon erasure. Because the structure uses multiple memory blocks, insertions to a full container also do not trigger reallocations. This means that element memory locations stay stable and iterators stay valid regardless of erasure/insertion. This is highly desirable, for example, in game programming because there are usually multiple elements in different containers which need to reference each other during gameplay and elements are being inserted or erased in real time.

Problematic aspects of a typical bucket array are that they tend to have a fixed memory block size, do not re-use memory locations from erased elements, and utilize a boolean skipfield. The fixed block size (as opposed to block sizes with a growth factor) and lack of erased-element re-use leads to far more allocations/deallocations than is necessary. Given that allocation is a costly operation in most operating systems, this becomes important in performance-critical environments. The boolean skipfield makes iteration time complexity undefined, as there is no way of knowing ahead of time how many erased elements occur between any two non-erased elements. This can create variable latency during iteration. It also requires branching code, which may cause issues on processors with deep pipelines and poor branch-prediction failure performance.

A hive uses a non-boolean method for skipping erased elements, which allows for O(1) amortized iteration time complexity and more-predictable iteration performance than a bucket array. It also utilizes a growth factor for memory blocks and reuses erased element locations upon insertion, which leads to fewer allocations/reallocations. Because it reuses erased element memory space, the exact location of insertion is undefined, unless no erasures have occurred or an equal number of erasures and insertions have occurred (in which case the insertion location is the back of the container). The container is therefore considered unordered but sortable. Lastly, because there is no way of predicting in advance where erasures ('skips') may occur during iteration, an O(1) time complexity [ ] operator is not necessarily possible (depending on implementation) and therefore, the container is bidirectional but not random-access.

There are two patterns for accessing stored elements in a hive: the first is to iterate over the container and process each element (or skip some elements using the advance/prev/next/iterator ++/-- functions). The second is to store the iterator returned by the insert() function (or a pointer derived from the iterator) in some other structure and access the inserted element in that way. To better understand how insertion and erasure work in a hive, see the following images.

Insertion to back

The following images demonstrate how insertion works in a hive compared to a vector when size == capacity.

Visual demonstration of inserting to a full vector Visual demonstration of inserting to a full hive

Non-back erasure

The following images demonstrate how non-back erasure works in a hive compared to a vector.

Visual demonstration of randomly erasing from a vector Visual demonstration of randomly erasing from a hive

II. Questions for the Committee

  1. It is possible to make the memory() function constant time at a cost (see details in it's entry in the design decisions section of the paper) but since this is expected to be a seldom-used function I've decided not to do so and leave the time complexity as implementation-defined. If there are any objections, please state them. Also, memory_usage() has been suggested as a better name?
  2. The conditions under which memory blocks are retained by the erase() functions and added to the "reserved" pile instead of deallocated, is presently implementation-defined. Are there any objections to this? Should we define this? See the notes on erase() in Design Decisions, and the item in the FAQ. One option is to specify that only the current back block may be retained, however I feel like this should be implementation-defined.
  3. Given that this is a largely unordered container, should resize() be included? Currently it is not and I see no particular reason to do so, but if there are valid reasons let me know.
  4. Bikeshedding is welcome for reshape(). Was previously set_block_capacity_limits(), which is more clear, but does not describe the action when container already contains blocks which are outside of the newly-specified capacity limits. change_block_capacity_limits()?

III. Motivation and Scope

Note: Throughout this document I will use the term 'link' to denote any form of referencing between elements whether it be via ids/iterators/pointers/indexes/references/etc.

There are situations where data is heavily interlinked, iterated over frequently, and changing often. An example is the typical video game engine. Most games will have a central generic 'entity' or 'actor' class, regardless of their overall schema (an entity class does not imply an ECS). Entity/actor objects tend to be 'has a'-style objects rather than 'is a'-style objects, which link to, rather than contain, shared resources like sprites, sounds and so on. Those shared resources are usually located in separate containers/arrays so that they can re-used by multiple entities. Entities are in turn referenced by other structures within a game engine, such as quadtrees/octrees, level structures, and so on.

Entities may be erased at any time (for example, a wall gets destroyed and no longer is required to be processed by the game's engine, so is erased) and new entities inserted (for example, a new enemy is spawned). While this is all happening the links between entities, resources and superstructures such as levels and quadtrees, must stay valid in order for the game to run. The order of the entities and resources themselves within the containers is, in the context of a game, typically unimportant, so an unordered container is okay.

Unfortunately the container with the best iteration performance in the standard library, vector[1], loses pointer validity to elements within it upon insertion, and pointer/index validity upon erasure. This tends to lead to sophisticated and often restrictive workarounds when developers attempt to utilize vector or similar containers under the above circumstances.

std::list and the like are not suitable due to their poor locality, which leads to poor cache performance during iteration. This is however an ideal situation for a container such as hive, which has a high degree of locality. Even though that locality can be punctuated by gaps from erased elements, it still works out better in terms of iteration performance[1] than every existing standard library container other than deque/vector, regardless of the ratio of erased to non-erased elements.

Some more specific requirements for containers in the context of game development are listed in the appendix.

As another example, particle simulation (weather, physics etcetera) often involves large clusters of particles which interact with external objects and each other. The particles each have individual properties (spin, momentum, direction etc) and are being created and destroyed continuously. Therefore the order of the particles is unimportant, what is important is the speed of erasure and insertion. No current standard library container has both strong insertion and non-back erasure speed, so again this is a good match for hive.

Reports from other fields suggest that, because most developers aren't aware of containers such as this, they often end up using solutions which are sub-par for iterative performance such as std::map and std::list in order to preserve pointer validity, when most of their processing work is actually iteration-based. So, introducing this container would both create a convenient solution to these situations, as well as increasing awareness of better-performing approaches in general. It will also ease communication across fields, as opposed to the current scenario where each field uses a similar container but each has a different name for it.

IV. Impact On the Standard

This is purely a library addition, requiring no changes to the language.

V. Design Decisions

The three core aspects of a hive from an abstract perspective are:

  1. A collection of element memory blocks + metadata, to prevent reallocation during insertion (as opposed to a single memory block)
  2. A method of skipping erased elements in O(1) time during iteration (as opposed to reallocating subsequent elements during erasure)
  3. An erased-element location recording mechanism, to enable the re-use of memory from erased elements in subsequent insertions, which in turn increases cache locality and reduces the number of block allocations/deallocations

Each memory block houses multiple elements. The metadata about each block may or may not be allocated with the blocks themselves (could be contained in a separate structure). This metadata should include at a minimum, the number of non-erased elements within each block and the block's capacity - which allows the container to know when the block is empty and needs to be removed from the iterative chain, and also allows iterators to judge when the end of one block has been reached. A non-boolean method of skipping over erased elements during iteration while maintaining O(1) amortized iteration time complexity is required (amortized due to block traversal, which would typically require a few more operations). Finally, a mechanism for keeping track of elements which have been erased must be present, so that those memory locations can be reused upon subsequent element insertions.

The following aspects of a hive must be implementation-defined in order to allow for variance and possible performance improvement, and to conform with possible changes to C++ in the future:

However the implementation of these is significantly constrained by the requirements of the container (lack of reallocation, stable pointers to non-erased elements regardless of erasures/insertions).

In terms of the reference implementation the specific structure and mechanisms have changed many times over the course of development, however the interface to the container and its time complexity guarantees have remained largely unchanged (with the exception of the time complexity for updating skipfield nodes - which has not impacted significantly on performance). So it is reasonably likely that regardless of specific implementation, it will be possible to maintain this general specification without obviating future improvements in implementation, so long as time complexity guarantees for the above list are implementation-defined.

Below I explain the reference implementation's approach in terms of the three core aspects described above, along with descriptions of some alternatives implementation approaches.

1. Collection of element memory blocks + metadata

In the reference implementation this is essentially a doubly-linked list of 'group' structs containing (a) a dynamically-allocated element memory block, (b) memory block metadata and (c) a dynamically-allocated skipfield. The memory blocks and skipfields have a growth factor of 2 from one group to the next. The metadata includes information necessary for an iterator to iterate over hive elements, such as the last insertion point within the memory block, and other information useful to specific functions, such as the total number of non-erased elements in the node. This approach keeps the operation of freeing empty memory blocks from the hive container at O(1) time complexity. Further information is available here.

Using a vector of group structs with dynamically-allocated element memory blocks, using the swap-and-pop idiom where groups need to be erased from the iterative sequence, would not work. To explain, when a group becomes empty of elements, it must be removed from the sequence of groups, because otherwise you end up with highly-variable latency during iteration due to the need to skip over an unknown number of empty groups when traversing from one non-empty group to the next. Simply erasing the group will not suffice, as this would create a variable amount of latency during erasure when the group becomes empty, based on the number of groups after that group which would need to be reallocated backward in the vector. But even if one swapped the to-be-erased group with the back group, and then pop'd the to-be-erased group off the back, this would not solve the problem, as iterators require a stable pointer to the group they are traversing in order to traverse to the next group in the sequence. If an iterator pointed to an element in the back group, and the back group was swapped with the to-be-erased group, this would invalidate the iterator.

A vector of pointers to group structs is more-possible. Erasing groups would still have highly-variable latency due to reallocation, however the cost of reallocating pointers may be negligible depending on architecture. While the number of pointers can be expected to be low in most cases due to the growth factor in memory blocks, if the user has defined their own memory block capacity limits the number of pointers could be large, and this has to be taken into consideration. In this case using a pop-and-swap idiom is still not possible, because while it would not necessarily invalidate the internal references of an iterator pointing to an element within the back group, the sequence of blocks would be changed and therefore the iterator would be moved backwards in the iterative sequence.

A vector of memory blocks, as opposed to a vector of pointers to memory blocks or a vector of group structs with dynamically-allocated memory blocks, would also not work, both due to the above points and because as it would (a) disallow a growth factor in the memory blocks and (b) invalidate pointers to elements in subsequent blocks when a memory block became empty of elements and was therefore removed from the vector. In short, negating hive's beneficial aspects.

2. A non-boolean method of skipping erased elements in O(1) time during iteration

The reference implementation currently uses a skipfield pattern called the Low complexity jump-counting pattern. This effectively encodes the length of runs of consecutive erased elements, into a skipfield, which allows for O(1) time complexity during iteration. Since there is no branching involved in iterating over the skipfield aside from end-of-block checks, it can be less problematic computationally than a boolean skipfield (which has to branch for every skipfield read) in terms of CPUs which don't handle branching or branch-prediction failure efficiently (eg. Core2). It also does not have the variable latency associated with a boolean skipfield.

The pattern stores and modifies the run-lengths during insertion and erasure with O(1) time complexity. It has a lot of similarities to the High complexity jump-counting pattern, which was a pattern previously used by the reference implementation. Using the High complexity jump-counting pattern is an alternative, though the skipfield update time complexity guarantees for that pattern are effectively undefined, or between O(1) and O(skipfield length) for each insertion/erasure. In practice those updates result in one memcpy operation which resolves to a single block-copy operation, but it is still a little slower than the Low complexity jump-counting pattern. The method you use to skip erased elements will typically also have an effect on the type of memory-reuse mechanism you can utilize.

A pure boolean skipfield is not usable because it makes iteration time complexity undefined - it could for example result in thousands of branching statements + skipfield reads for a single ++ operation in the case of many consecutive erased elements. In the high-performance fields for which this container was initially designed, this brings with it unacceptable latency. However another strategy using a combination of a jump-counting and boolean skipfield, which saves memory at the expense of computational efficiency, is possible as follows:

  1. Instead of storing the data for the low complexity jump-counting pattern in it's own skipfield, have a boolean bitfield indicating which elements are erased. Store the jump-counting data in the erased element's memory space instead (possibly alongside free list data).
  2. When iterating, check whether the element is erased or not using the boolean bitfield; if it is not erased, do nothing. If it is erased, read the jump value from the erased element's memory space and skip forward the appropriate number of nodes both in the element memory block and the boolean bitfield.

This approach has the advantage of still performing O(1) iterations from one non-erased element to the next, unlike a pure boolean skipfield approach, but compared to a pure jump-counting approach introduces 3 additional costs per iteration via (1) a branch operation when checking the bitfield, (2) an additional read (of the erased element's memory space) and (3) a bitmasking operation + bitshift to read the bit. But it does reduce the memory overhead of the skipfield to 1 bit per-element, which reduces the cache load. An implementation and benchmarking would be required in order to establish how this approach compares to the current implementation's performance.

Another method worth mentioning is the use of a referencing array - for example, having a vector of elements, together with a vector of either indexes or pointers to those elements. When an element is erased, the vector of elements itself is not updated - no elements are reallocated. Meanwhile the referencing vector is updated and the index or pointer to the erased element is erased. When iteration occurs it iterates over the referencing vector, accessing each element in the element vector via the indexes/pointers. The disadvantages of this technique are (a) much higher memory usage, particularly for small elements and (b) highly-variable latency during erasure due to reallocation in the referencing array. Since once of the goals of hive is predictable latency, this is likely not suitable.

Packed arrays are not worth mentioning as the iteration method is considered separate from the referencing mechanism, making them unsuitable for a std:: container.

3. Erased-element location recording mechanism

There are two valid approaches here; both involve per-memory-block free lists, utilizing the memory space of erased elements. The first approach forms a free list of all erased elements. The second forms a free list of the first element in each run of consecutive erased elements ("skipblocks", in terms of the terminology used in the jump-counting pattern papers). The second can be more efficient, but requires a doubly-linked free list rather than a singly-linked free list, at least with a jump-counting skipfield - otherwise it would become an O(N) operation to update links in the skipfield, when a skipblock expands or contracts during erasure or insertion.

The reference implementation currently uses the second approach, using three things to keep track of erased element locations:

  1. Metadata for each memory block includes a 'next block with erasures' pointer. The container itself contains a 'blocks with erasures' list-head pointer. These are used by the container to create an intrusive singly-linked list of memory blocks with erased elements which can be re-used for future insertions.
  2. Metadata for each memory block also includes a 'free list head' index number, which records the index (within the memory block) of the first element of the last-created skipblock - the 'head' skipblock.
  3. The memory space of the first erased element in each skipblock is reinterpret_cast'd via pointers as two index numbers, the first giving the index of the previous skipblock in that memory block, the second giving the index of the next skipblock in the sequence. In the case of the 'head' skipblock in the sequence, a unique number is used for the 'next' index. This forms a free list of runs of erased element memory locations which may be re-used.

Using indexes for next and previous links, instead of pointers, reduces the necessary bit-depth of the next and previous links, thereby reducing the necessary over-alignment of the container's element type. If a global (ie. all memory blocks) free list were used, pointers would be necessary, as hive is bidirectional and does not support the [ ] operator. This would potentially increase the necessary over-alignment of the element type to 128 bits for a doubly-linked free list. A global free list would also decrease cache locality when traversing the free list by jumping between memory blocks.

Previous versions of the reference implementation used a singly-linked free list of erased elements instead of a doubly-linked free list of skipblocks. This was possible with the High complexity jump-counting pattern, but not possible using the Low complexity jump-counting pattern as it cannot calculate a skipblock's start node location from a middle node's value like the High complexity pattern can. But using free-lists of skipblocks is a more efficient approach as it requires fewer free list nodes. In addition, re-using only the start or end nodes of a skipblock is faster because it never splits a single skipblock in two (which would require adding a new skipblock to the free list).

One cannot use a stack of pointers (or similar) to erased elements for this mechanism, as early versions of the reference implementation did, because this can create allocations during erasure, which changes the exception guarantees of erase(). One could instead scan all skipfields until an erased location was found, or simply have the first item in the list above and then scan the first available block, though both of these approaches would be slow.

In terms of the alternative boolean + jump-counting skipfield approach described in the erased-element-skip-method section above, one could store both the jump-counting data and free list data in any given erased element's memory space, provided of course that elements are aligned to be wide enough to fit both.

Implementation of iterator class

Any iterator implementation is going to be dependent on the erased-element-skipping mechanism used. The reference implementation's iterator stores a pointer to the current 'group' struct mentioned above, plus a pointer to the current element and a pointer to its corresponding skipfield node. An alternative approach is to store the group pointer + an index, since the index can indicate both the offset from the memory block for the element, as well as the offset from the start of the skipfield for the skipfield node. However multiple implementations and benchmarks across many processors have shown this to be worse-performing than the separate pointer-based approach, despite the increased memory cost for the iterator class itself.

++ operation is as follows, utilising the reference implementation's Low-complexity jump-counting pattern:

  1. Add 1 to the existing element and skipfield pointers.
  2. Dereference skipfield pointer to get value of skipfield node, add value of skipfield node to both the skipfield pointer and the element pointer. If the node is erased, its value will be a positive integer indicating the number of nodes until the next non-erased node, if not erased it will be zero.
  3. If element pointer is now beyond end of element memory block, change group pointer to next group, element pointer to the start of the next group's element memory block, skipfield pointer to the start of the next group's skipfield. In case there is a skipblock at the beginning of this memory block, dereference skipfield pointer to get value of skipfield node and add value of skipfield node to both the skipfield pointer and the element pointer. There is no need to repeat the check for end of block, as the block would have been removed from the iteration sequence if it were empty of elements.

-- operation is the same except both step 1 and 2 involve subtraction rather than adding, and step 3 checks to see if the element pointer is now before the beginning of the memory block. If so it traverses to the back of the previous group, and subtracts the value of the back skipfield node from the element pointer and skipfield pointer.

Iterators are bidirectional but also provide constant time complexity >, <, >=, <= and <=> operators for convenience (eg. in for loops when skipping over multiple elements per loop and there is a possibility of going past a pre-determined end element). This is achieved by keeping a record of the order of memory blocks. In the reference implementation this is done by assigning a number to each memory block in its metadata. In an implementation using a vector of pointers to memory blocks instead of a linked list, one could use the position of the pointers within the vector to determine this. Comparing relative order of the two iterators' memory blocks via this number, then comparing the memory locations of the elements themselves, if they happen to be in the same memory block, is enough to implement all greater/lesser comparisons.

Additional notes on specific functions

Results of implementation

In practical application the reference implementation is generally faster for insertion and (non-back) erasure than current standard library containers, and generally faster for iteration than any container except vector and deque. For full details, see benchmarks.

VI. Technical Specification

Suggested location of hive in the standard is 22.3, Sequence Containers.

22.3.7 Header <hive> synopsis [hive.syn]

#include <initializer_list> // see 17.10.2
#include <compare> // see 17.11.1
#include <concepts> // see 18.3
#include <stdexcept> // see 19.2
#include <utility> // see 20.2.1
#include <memory> // see 20.10

namespace std {
   // 22.3.14, class template hive

	struct hive_limits;
	enum class hive_priority;

   template <class T, class Allocator = allocator<T>, hive_priority priority = hive_priority::performance> class hive;

   namespace pmr {
      template <class T>
      using hive = std::hive<T, polymorphic_allocator<T>>;
   }
}

Iterator Invalidation

All read-only operations, swap, std::swap, splice, operator= && (source), reserve, trim Never.
clear, operator= & (destination), operator= && (destination) Always.
reshape Only if memory blocks exist whose capacities do not fit within the supplied limits.
shrink_to_fit Only if capacity() != size().
erase Only for the erased element. If an iterator is == end() it may be invalidated if the back element of the hive is erased (similar to deque (22.3.9)). Likewise if a reverse_iterator is == rend() it may be invalidated if the front element of the hive is erased. The same applies with cend() and crend() for const_iterator and const_reverse_iterator respectively.
insert, emplace If an iterator is == end() or == begin() it may be invalidated by a subsequent insert/emplace. Likewise if a reverse_iterator is == rend() or == rbegin() it may be invalidated by a subsequent insert/emplace. The same rules apply with cend(), cbegin() and crend(), crbegin() for const_iterator and const_reverse_iterator respectively.

22.3.14 Class template hive [hive]

22.3.14.1 Class template hive overview [hive.overview]

  1. A hive is a sequence container that allows constant-time insert and erase operations. Insertion location is the back of the container when no erasures have occured. When erasures have occured it will re-use existing erased element memory spaces where possible and insert to those locations. Storage management is handled automatically and is specifically organized in multiple blocks of sequential elements. Unlike vectors (22.3.12) and deques (22.3.9), fast random access to hive elements is not supported, but specializations of advance/next/prev give access which can be better than linear time in the number of elements traversed.
  2. Erasures are processed using implementation-defined strategies for skipping erased elements during iteration, rather than reallocating subsequent elements as is expected in a vector or deque.
  3. Memory block element capacities have an implementation-defined growth factor, for example a new block's capacity could be equal to the summed capacities of the existing blocks.
  4. Limits can be placed on the minimum and maximum element capacities of memory blocks, both by a user and by an implementation. Minimum capacity shall be no more than maximum capacity. When limits are not specified by a user, the implementation's default limits are used. Where user-specified limits do not fit within the implementation's limits (ie. user minimum is less than implementation minimum or user maximum is more than implementation maximum) an exception is thrown. User-specified limits can be supplied to a constructor or to the reshape() function, using the std::hive_limits struct with its min and max members set to the minimum and maximum element capacity limits respectively. The current limits in a hive instance can be obtained from block_capacity_limits().
  5. A hive satisfies all of the requirements of a container, of a reversible container (given in two tables in 22.2), of a sequence container, including most of the optional sequence container requirements (22.2.3), and of an allocator-aware container (Table 78). The exceptions are the operator[] and at member functions, which are not provided.
  6. hive iterators satisfy bidirectional requirements but also provide relational operators <, <=, >, >= and <=> which compare the relative ordering of two iterators in the sequence of a hive instance.
  7. Iterator operations ++ and -- take constant amortized time, other iterator operations take constant time.
template <class T, class Allocator = std::allocator<T>, priority Priority = priority::performance> class hive

T - the element type. In general T shall meet the requirements of Erasable, CopyAssignable and CopyConstructible.
However, if emplace is utilized to insert elements into the hive, and no functions which involve copying or moving are utilized, T is only required to meet the requirements of Erasable.
If move-insert is utilized instead of emplace, T shall also meet the requirements of MoveConstructible.

Allocator - an allocator that is used to acquire memory to store the elements. The type shall meet the requirements of Allocator. The behavior is undefined if Allocator::value_type is not the same as T.

Priority - if set to priority::memory_use this is a non-binding request to prioritize lowered memory usage over container performance. [ Note: The request is non-binding to allow latitude for implementation-specific optimizations. If this feature is implemented, it is not specified that the container shall have better performance when using priority::performance instead of priority::memory_usage in all scenarios, but that it shall have better performance in most scenarios. - end note ]

namespace std {

struct hive_limits
{
	size_t min, max;
	hive_limits(size_t minimum, size_t maximum) noexcept : min(minimum), max(maximum) {}
};


enum struct hive_priority { performance, memory_use };


template <class T, class Allocator = allocator<T>, hive_priority Priority = hive_priority::performance>
class hive {
public:

  // types
  using value_type = T;
  using allocator_type = Allocator;
  using pointer = typename allocator_traits<Allocator>::pointer;
  using const_pointer = typename allocator_traits<Allocator>::const_pointer;
  using reference = value_type&;
  using const_reference = const value_type&;
  using size_type = implementation-defined; // see 22.2
  using difference_type = implementation-defined; // see 22.2
  using iterator = implementation-defined; // see 22.2
  using const_iterator = implementation-defined; // see 22.2
  using reverse_iterator = implementation-defined; // see 22.2
  using const_reverse_iterator = implementation-defined; // see 22.2



  hive() noexcept(noexcept(Allocator())) : hive(Allocator()) { }
  explicit hive(std::hive_limits block_capacity_limits) noexcept(noexcept(Allocator())) : hive(Allocator()) { }
  explicit hive(const Allocator&) noexcept;
  explicit hive(std::hive_limits block_capacity_limits, const Allocator&) noexcept;
  explicit hive(size_type n, std::hive_limits block_capacity_limits = implementation-defined, const Allocator& = Allocator());
  hive(size_type n, const T& value, std::hive_limits block_capacity_limits = implementation-defined, const Allocator& = Allocator());
  template<class InputIterator1, class InputIterator2>
    hive(InputIterator1 first, InputIterator2 last, std::hive_limits block_capacity_limits = implementation-defined, const Allocator& = Allocator());
  hive(const hive& x);
  hive(hive&&) noexcept;
  hive(const hive&, const Allocator&);
  hive(hive&&, const Allocator&);
  hive(initializer_list<T>, std::hive_limits block_capacity_limits = implementation-defined, const Allocator& = Allocator());
  ~hive() noexcept;
  hive& operator= (const hive& x);
  hive& operator= (hive&& x) noexcept(allocator_traits<Allocator>::propagate_on_container_move_assignment::value || allocator_traits<Allocator>::is_always_equal::value);
  hive& operator= (initializer_list<T>);
  template<class InputIterator1, class InputIterator2> void assign(InputIterator1 first, InputIterator2 last);
  void assign(size_type n, const T& t);
  void assign(initializer_list<T>);
  allocator_type get_allocator() const noexcept;



  // iterators
  iterator               begin() noexcept;
  const_iterator         begin() const noexcept;
  iterator               end() noexcept;
  const_iterator         end() const noexcept;
  reverse_iterator       rbegin() noexcept;
  const_reverse_iterator rbegin() const noexcept;
  reverse_iterator       rend() noexcept;
  const_reverse_iterator rend() const noexcept;

  const_iterator         cbegin() const noexcept;
  const_iterator         cend() const noexcept;
  const_reverse_iterator crbegin() const noexcept;
  const_reverse_iterator crend() const noexcept;


  // capacity
  [[nodiscard]] bool empty() const noexcept;
  size_type size() const noexcept;
  size_type max_size() const noexcept;
  size_type capacity() const noexcept;
  size_type memory() const noexcept;
  void reserve(size_type n);
  void shrink_to_fit();
  void trim() noexcept;


  // modifiers
  template <class... Args> iterator emplace(Args&&... args);
  iterator insert(const T& x);
  iterator insert(T&& x);
  void insert(size_type n, const T& x);
  template <class InputIterator1, class InputIterator2> void insert(InputIterator1 first, InputIterator2 last);
  void insert(initializer_list<T> il);
  iterator erase(const_iterator position);
  iterator erase(const_iterator first, const_iterator last);
  void swap(hive&) noexcept(allocator_traits<Allocator>::propagate_on_container_swap::value || allocator_traits<Allocator>::is_always_equal::value);
  void clear() noexcept;


  // hive operations
  void splice(hive &x);

  std::hive_limits block_capacity_limits() const noexcept;
  void reshape(std::hive_limits block_capacity_limits);

  iterator get_iterator(pointer p) noexcept;
  const_iterator get_iterator(const_pointer p) const noexcept;

  void sort();
  template <class Compare> void sort(Compare comp);

  friend bool operator== (const hive &x, const hive &y);
  friend bool operator!= (const hive &x, const hive &y);


  class iterator
  {
    friend void advance(iterator &it, Distance n);
    friend iterator next(iterator it, difference_type distance = 1);
    friend iterator prev(iterator it, difference_type distance = 1);
    friend difference_type distance(iterator first, iterator last);
  }


  class const_iterator
  {
    friend void advance(const_iterator &it, Distance n);
    friend const_iterator next(const_iterator it, difference_type distance = 1);
    friend const_iterator prev(const_iterator it, difference_type distance = 1);
    friend difference_type distance(const_iterator first, const_iterator last);
  }


  class reverse_iterator
  {
    friend void advance(reverse_iterator &it, Distance n);
    friend reverse_iterator next(reverse_iterator it, difference_type distance = 1);
    friend reverse_iterator prev(reverse_iterator it, difference_type distance = 1);
    friend difference_type distance(reverse_iterator first, reverse_iterator last);
  }


  class const_reverse_iterator
  {
    friend void advance(const_reverse_iterator &it, Distance n);
    friend const_reverse_iterator next(const_reverse_iterator it, difference_type distance = 1);
    friend const_reverse_iterator prev(const_reverse_iterator it, difference_type distance = 1);
    friend difference_type distance(const_reverse_iterator first, const_reverse_iterator last);
  }


  // swap
  friend void swap(hive& x, hive& y)
    noexcept(noexcept(x.swap(y)));


  // erase
  template <class Predicate>
    friend size_type erase_if(hive& c, Predicate pred);
  template <class U>
    friend size_type erase(hive& c, const U& value);
}


template<class InputIterator, class Allocator = allocator<iter-value-type <InputIterator>>>
  hive(InputIterator, InputIterator, Allocator = Allocator())
    -> hive<iter-value-type <InputIterator>, Allocator>;

22.3.14.2 hive constructors, copy, and assignment [hive.cons]

explicit hive(const Allocator&);
  1. Effects: Constructs an empty hive, using the specified allocator.
  2. Complexity: Constant.

explicit hive(size_type n, const T& value, std::hive_limits block_capacities = implementation-defined, const Allocator& =Allocator());
  1. Preconditions: T shall be Cpp17MoveInsertable into *this.
  2. Effects: Constructs a hive with n copies of value, using the specified allocator.
  3. Complexity: Linear in n.
  4. Throws: length_error if block_capacities.min or block_capacities.max are outside the implementation's minimum and maximum element memory block capacity limits, or if block_capacities.min > block_capacities.max.
  5. Remarks: If n is larger than block_capacities.min, the capacity of the first block created will be the smaller of n or block_capacities.max.

template <class InputIterator1, class InputIterator2>
  hive(InputIterator1 first, InputIterator2 last, std::hive_limits block_capacities = implementation-defined, const Allocator& = Allocator());
  1. Preconditions: InputIterator1 shall be std::equality_comparable_with InputIterator2.
  2. Effects: Constructs a hive equal to the range [first, last), using the specified allocator.
  3. Complexity: Linear in distance(first, last).
  4. Throws: length_error if block_capacities.min or block_capacities.max are outside the implementation's minimum and maximum element memory block capacity limits, or if block_capacities.min > block_capacities.max. Or
  5. Remarks: If iterators are random-access, let n be last - first; if n is larger than block_capacities.min, the capacity of the first block created will be the smaller of n or block_capacities.max.

22.3.14.3 hive capacity [hive.capacity]

size_type capacity() const noexcept;
  1. Returns: The total number of elements that the hive can currently contain without needing to allocate more memory blocks.

size_type memory() const noexcept;
  1. Returns: The memory use, in bytes, of the container as a whole, including elements but not including any dynamic allocation incurred by those elements.

void reserve(size_type n);
  1. Effects: A directive that informs a hive of a planned change in size, so that it can manage the storage allocation accordingly. Since minimum and maximum memory block sizes can be specified by users, after reserve(), capacity() is not guaranteed to be equal to the argument of reserve(), may be greater. Does not cause reallocation of elements.
  2. Complexity: It does not change the size of the sequence and creates at most (n / block_capacity_limits().max) + 1 allocations.
  3. Throws: length_error if n > max_size()223.

223) reserve() uses Allocator::allocate() which may throw an appropriate exception.


void shrink_to_fit();
  1. Preconditions: T is Cpp17MoveInsertable into *this.
  2. Effects: shrink_to_fit is a non-binding request to reduce capacity() to be closer to size(). [ Note: The request is non-binding to allow latitude for implementation-specific optimizations. - end note ] It does not increase capacity(), but may reduce capacity() by causing reallocation. It may move elements from multiple memory blocks and consolidate them into a smaller number of memory blocks.
    If an exception is thrown other than by the move constructor of a non-Cpp17CopyInsertable T, there are no effects.
  3. Complexity: If reallocation happens, linear to the number of elements reallocated.
  4. Remarks: Reallocation invalidates all the references, pointers, and iterators referring to the elements reallocated as well as the past-the-end iterator. [Note: If no reallocation happens, they remain valid. —end note] The order of elements post-operation is not guaranteed to be stable.

void trim();
  1. Effects: Removes and deallocates empty memory blocks created by prior calls to reserve() or erase(). If such memory blocks are present, capacity() will be reduced.
  2. Complexity: Linear in the number of reserved blocks to deallocate.
  3. Remarks: Does not reallocate elements and no references, pointers or iterators referring to elements in the sequence will be invalidated.

22.3.14.4 hive modifiers [hive.modifiers]

iterator insert(const T& x);
iterator insert(T&& x);
void insert(size_type n, const T& x);
template <class InputIterator1, class InputIterator2>
  void insert(InputIterator1 first, InputIterator2 last);
void insert(initializer_list<T>);
template <class... Args>
  iterator emplace(Args&&... args);
  1. Preconditions: For template <class InputIterator1, class InputIterator2> void insert(InputIterator1 first, InputIterator2 last), InputIterator1 shall be std::equality_comparable_with InputIterator2.
  2. Complexity: Insertion of a single element into a hive takes constant time and exactly one call to a constructor of T. Insertion of multiple elements into a hive is linear in the number of elements inserted, and the number of calls to the copy constructor or move constructor of T is exactly equal to the number of elements inserted.
  3. Remarks: Does not affect the validity of iterators and references, unless an iterator points to end(), in which case it may be invalidated. Likewise if a reverse_iterator points to rend() it may be invalidated. If an exception is thrown there are no effects.

iterator erase(const_iterator position);
  1. Effects: Invalidates only the iterators and references to the erased element.
  2. Complexity: Constant. [Note: operations pertaining to the updating of any data associated with the erased-elemment skipping mechanism is not factored into this; it is implementation-defined and may be constant, linear or otherwise defined. —end note]

iterator erase(const_iterator first, const_iterator last);
  1. Effects: Invalidates only the iterators and references to the erased elements. In some cases if an iterator is equal to end() and the back element of the hive is erased, that iterator may be invalidated. Likewise if a reverse_iterator is equal to rend() and the front element of the hive is erased, that reverse_iterator may be invalidated.
  2. Complexity: Linear in the number of elements erased for non-trivially-destructible types, for trivially-destructible types constant in best case and linear in worst case, approximating logarithmic in the number of elements erased on average.

void swap(hive& x) noexcept(allocator_traits<Allocator>::propagate_on_container_swap::value || allocator_traits<Allocator>::is_always_equal::value);
  1. Effects: Exchanges the contents and capacity() of *this with that of x.
  2. Complexity: Constant time.

22.3.14.5 Operations [hive.operations]

void splice(hive &x);
  1. Preconditions: &x != this.
  2. Effects: Inserts the contents of x into *this and x becomes empty. Pointers and references to the moved elements of x now refer to those same elements but as members of *this. Iterators referring to the moved elements will continue to refer to their elements, but they now behave as iterators into *this, not into x.
  3. Complexity: Constant time.
  4. Throws: length_error if any of x's element memory block capacities are outside the current minimum and maximum element memory block capacity limits of *this.223

std::hive_limits block_capacity_limits() const noexcept;
  1. Effects: Returns a std::hive_limits struct with the min and max members set to the current minimum and maximum element memory block capacity limits of *this.
  2. Complexity: Constant time.

void reshape(std::hive_limits block_capacity_limits);
  1. Preconditions: T shall be Cpp17MoveInsertable into *this.
  2. Effects: Sets minimum and maximum element memory block capacities to the min and max members of the supplied std::hive_limits struct. If the hive is not empty, adjusts existing memory block capacities to conform to the new minimum and maximum block capacities, where necessary. If existing memory block capacities are within the supplied minimum/maximum range, no reallocation of elements takes place. If they are not within the supplied range, elements are reallocated to new memory blocks which fit within the supplied range and the old memory blocks are deallocated. Order of elements is not guaranteed to be stable.
  3. Complexity: If no reallocation occurs, constant time. If reallocation occurs, complexity is linear in the number of elements reallocated.
  4. Throws: length_error if block_capacities.min or block_capacities.max are outside the implementation's minimum and maximum element memory block capacity limits, or if block_capacities.min > block_capacities.max.223
  5. Remarks: The order of elements post-operation is not guaranteed to be stable (16.5.5.8).

iterator get_iterator(pointer p) noexcept;
const_iterator get_iterator(const_pointer p) const noexcept;
  1. Effects: Returns an iterator or const_iterator pointing to the same element as the pointer or const_pointer. If p does not point to an element in *this, end() is returned.

void sort();
template <class Compare>
  void sort(Compare comp);
  1. Preconditions: T is Cpp17MoveInsertable into *this.
  2. Effects: Sorts the hive according to the operator < or a Compare function object. If an exception is thrown, the order of the elements in *this is unspecified. Iterators and references may be invalidated.
  3. Complexity: Approximately N log N comparisons, where N == size().
  4. Throws: bad_alloc if it fails to allocate any memory necessary for the sort process.
  5. Remarks: Not required to be stable (16.5.5.8). May allocate memory.

22.3.14.6 Specialized algorithms [hive.special]

friend void swap(hive &x, hive &y) noexcept(noexcept(x.swap(y)));
  1. Effects: As if by x.swap(y).
  2. Remarks: This function is to be found via argument-dependent lookup only.

friend bool operator== (const hive &x, const hive &y);
friend bool operator!= (const hive &x, const hive &y);
  1. Returns: For ==, returns True if both containers have the same elements in the same iterative sequence, otherwise False. For !=, returns True if both containers do not have the same elements in the same iterative sequence, otherwise False.
  2. Remarks: These functions are to be found via argument-dependent lookup only.

class iterator
{
  friend void advance(iterator &it, Distance n);
  friend iterator next(iterator it, difference_type distance = 1);
  friend iterator prev(iterator it, difference_type distance = 1);
  friend difference_type distance(iterator first, iterator last);
}


class const_iterator
{
  friend void advance(const_iterator &it, Distance n);
  friend const_iterator next(const_iterator it, difference_type distance = 1);
  friend const_iterator prev(const_iterator it, difference_type distance = 1);
  friend difference_type distance(const_iterator first, const_iterator last);
}


class reverse_iterator
{
  friend void advance(reverse_iterator &it, Distance n);
  friend reverse_iterator next(reverse_iterator it, difference_type distance = 1);
  friend reverse_iterator prev(reverse_iterator it, difference_type distance = 1);
  friend difference_type distance(reverse_iterator first, reverse_iterator last);
}


class const_reverse_iterator
{
  friend void advance(const_reverse_iterator &it, Distance n);
  friend const_reverse_iterator next(const_reverse_iterator it, difference_type distance = 1);
  friend const_reverse_iterator prev(const_reverse_iterator it, difference_type distance = 1);
  friend difference_type distance(const_reverse_iterator first, const_reverse_iterator last);
}
  1. Complexity: Constant in best case and linear in the number of elements traversed in worst case, approximating logarithmic in the number of elements traversed on average.
  2. Remarks: These functions are to be found via argument-dependent lookup only.

22.3.14.7 Erasure [hive.erasure]

template <class U>
    friend size_type erase(hive& c, const U& value);
  1. Effects: All elements in the container which are equal to value are erased. Invalidates all references and iterators to the erased elements.
  2. Remarks: This function is to be found via argument-dependent lookup only.

template <class Predicate>
    friend size_type erase_if(hive& c, Predicate pred);
  1. Effects: All elements in the container which match predicate pred are erased. Invalidates all references and iterators to the erased elements.
  2. Remarks: This function is to be found via argument-dependent lookup only.

VII. Acknowledgements

Matt would like to thank: Glen Fernandes and Ion Gaztanaga for restructuring advice, Robert Ramey for documentation advice, various Boost and SG14 members for support, critiques and corrections, Baptiste Wicht for teaching me how to construct decent benchmarks, Jonathan Wakely, Sean Middleditch, Jens Maurer (very nearly a co-author at this point really), Patrice Roy and Guy Davidson for standards-compliance advice and critiques, support, representation at meetings and bug reports, Henry Miller for getting me to clarify why the instrusive list/free list approach to memory location reuse is the most appropriate, Ville Voutilainen and Gasper Azman for help with the colony/hive rename paper, that ex-Lionhead guy for annoying me enough to force me to implement the original skipfield pattern, Jon Blow for some initial advice and Mike Acton for some influence, the community at large for giving me feedback and bug reports on the reference implementation.
Also Nico Josuttis for doing such a great job in terms of explaining the general format of the structure to the committee.

VIII. Appendices

Appendix A - Basic usage examples

Using reference implementation.

#include <iostream>
#include <numeric>
#include "plf_hive.h"

int main(int argc, char **argv)
{
  plf::hive<int> i_hive;

  // Insert 100 ints:
  for (int i = 0; i != 100; ++i)
  {
    i_hive.insert(i);
  }

  // Erase half of them:
  for (plf::hive<int>::iterator it = i_hive.begin(); it != i_hive.end(); ++it)
  {
    it = i_hive.erase(it);
  }

  std::cout << "Total: " << std::accumulate(i_hive.begin(), i_hive.end(), 0) << std::endl;
  std::cin.get();
  return 0;
} 

Example demonstrating pointer stability

#include <iostream>
#include "plf_hive.h"

int main(int argc, char **argv)
{
  plf::hive<int> i_hive;
  plf::hive<int>::iterator it;
  plf::hive<int *> p_hive;
  plf::hive<int *>::iterator p_it;

  // Insert 100 ints to i_hive and pointers to those ints to p_hive:
  for (int i = 0; i != 100; ++i)
  {
    it = i_hive.insert(i);
    p_hive.insert(&(*it));
  }

  // Erase half of the ints:
  for (it = i_hive.begin(); it != i_hive.end(); ++it)
  {
    it = i_hive.erase(it);
  }

  // Erase half of the int pointers:
  for (p_it = p_hive.begin(); p_it != p_hive.end(); ++p_it)
  {
    p_it = p_hive.erase(p_it);
  }

  // Total the remaining ints via the pointer hive (pointers will still be valid even after insertions and erasures):
  int total = 0;

  for (p_it = p_hive.begin(); p_it != p_hive.end(); ++p_it)
  {
    total += *(*p_it);
  }

  std::cout << "Total: " << total << std::endl;

  if (total == 2500)
  {
    std::cout << "Pointers still valid!" << std::endl;
  }

  std::cin.get();
  return 0;
} 

Appendix B - Reference implementation benchmarks

Benchmark results for the hive reference implementation under GCC on an Intel Xeon E3-1241 (Haswell) are here.

Old benchmark results for an earlier version of hive under MSVC 2015 update 3, on an Intel Xeon E3-1241 (Haswell) are here. There is no commentary for the MSVC results.

Appendix C - Frequently Asked Questions

  1. Where is it worth using a hive in place of other std:: containers?

    As mentioned, it is worthwhile for performance reasons in situations where the order of container elements is not important and:

    1. Insertion order is unimportant
    2. Insertions and erasures to the container occur frequently in performance-critical code, and
    3. Links to non-erased container elements may not be invalidated by insertion or erasure.

    Under these circumstances a hive will generally out-perform other std:: containers. In addition, because it never invalidates pointer references to container elements (except when the element being pointed to has been previously erased) it may make many programming tasks involving inter-relating structures in an object-oriented or modular environment much faster, and could be considered in those circumstances.

  2. What are some examples of situations where a hive might improve performance?

    Some ideal situations to use a hive: cellular/atomic simulation, persistent octtrees/quadtrees, game entities or destructible-objects in a video game, particle physics, anywhere where objects are being created and destroyed continuously. Also, anywhere where a vector of pointers to dynamically-allocated objects or a std::list would typically end up being used in order to preserve pointer stability but where order is unimportant.

  3. Is it similar to a deque?

    A deque is reasonably dissimilar to a hive - being a double-ended queue, it requires a different internal framework. In addition, being a random-access container, having a growth factor for memory blocks in a deque is problematic (though not impossible). A deque and hive have no comparable performance characteristics except for insertion (assuming a good deque implementation). Deque erasure performance varies wildly depending on the implementation, but is generally similar to vector erasure performance. A deque invalidates pointers to subsequent container elements when erasing elements, which a hive does not, and guarantees ordered insertion.

  4. What are the thread-safe guarantees?

    Unlike a std::vector, a hive can be read from and inserted into at the same time (assuming different locations for read and write), however it cannot be iterated over and written to at the same time. If we look at a (non-concurrent implementation of) std::vector's thread-safe matrix to see which basic operations can occur at the same time, it reads as follows (please note push_back() is the same as insertion in this regard):

    std::vector Insertion Erasure Iteration Read
    Insertion No No No No
    Erasure No No No No
    Iteration No No Yes Yes
    Read No No Yes Yes

    In other words, multiple reads and iterations over iterators can happen simultaneously, but the potential reallocation and pointer/iterator invalidation caused by insertion/push_back and erasure means those operations cannot occur at the same time as anything else.

    hive on the other hand does not invalidate pointers/iterators to non-erased elements during insertion and erasure, resulting in the following matrix:

    hive Insertion Erasure Iteration Read
    Insertion No No No Yes
    Erasure No No No Mostly*
    Iteration No No Yes Yes
    Read Yes Mostly* Yes Yes

    * Erasures will not invalidate iterators unless the iterator points to the erased element.

    In other words, reads may occur at the same time as insertions and erasures (provided that the element being erased is not the element being read), multiple reads and iterations may occur at the same time, but iterations may not occur at the same time as an erasure or insertion, as either of these may change the state of the skipfield which is being iterated over, if a skipfield is used in the implementation. Note that iterators pointing to end() may be invalidated by insertion.

    So, hive could be considered more inherently thread-safe than a (non-concurrent implementation of) std::vector, but still has some areas which would require mutexes or atomics to navigate in a multithreaded environment.

  5. Any pitfalls to watch out for?

    Because erased-element memory locations may be reused by insert() and emplace(), insertion position is essentially random unless no erasures have been made, or an equal number of erasures and insertions have been made.

  6. What is the purpose of limiting memory block minimum and maximum sizes?

    One reason might be to ensure that memory blocks match a certain processor's cache or memory pathway sizes. Another reason to do this is that it is slightly slower to obtain an erased-element location from the list of groups-with-erasures (subsequently utilising that group's free list of erased locations) and to reuse that space than to insert a new element to the back of the hive (the default behavior when there are no previously-erased elements). If there are any erased elements in active memory blocks at the moment of insertion, hive will recycle those memory locations.

    So if a block size is large, and many erasures occur but the block is not completely emptied, iterative performance might suffer due to large memory gaps between any two non-erased elements and subsequent drop in data locality and cache performance. In that scenario you may want to experiment with benchmarking and limiting the minimum/maximum sizes of the blocks, such that memory blocks are freed earlier and find the optimal size for the given use case.

  7. What is hive's Abstract Data Type (ADT)?

    Though I am happy to be proven wrong I suspect hives/colonies/bucket arrays are their own abstract data type. Some have suggested it's ADT is of type bag, I would somewhat dispute this as it does not have typical bag functionality such as searching based on value (you can use std::find but it's o(n)) and adding this functionality would slow down other performance characteristics. Multisets/bags are also not sortable (by means other than automatically by key value). hive does not utilize key values, is sortable, and does not provide the sort of functionality frequently associated with a bag (e.g. counting the number of times a specific value occurs).

  8. Why must blocks be removed from the iterative sequence when empty?

    Two reasons:

    1. Standards compliance: if blocks aren't removed then ++ and -- iterator operations become undefined in terms of time complexity, making them non-compliant with the C++ standard. At the moment they are O(1) amortized, in the reference implementation this constitutes typically one update for both skipfield and element pointers, but two if a skipfield jump takes the iterator beyond the bounds of the current block and into the next block. But if empty blocks are allowed, there could be anywhere between 1 and std::numeric_limits<size_type>::max empty blocks between the current element and the next. Essentially you get the same scenario as you do when iterating over a boolean skipfield. It would be possible to move these to the back of the hive as trailing blocks, or house them in a separate list or vector for future usage, but this may create performance issues if any of the blocks are not at their maximum size (see below).
    2. Performance: iterating over empty blocks is slower than them not being present, of course - but also if you have to allow for empty blocks while iterating, then you have to include a while loop in every iteration operation, which increases cache misses and code size. The strategy of removing blocks when they become empty also statistically removes (assuming randomized erasure patterns) smaller blocks from the hive before larger blocks, which has a net result of improving iteration, because with a larger block, more iterations within the block can occur before the end-of-block condition is reached and a jump to the next block (and subsequent cache miss) occurs. Lastly, pushing to the back of a hive, provided there is still space and no new block needs to be allocated, will be faster than recycling memory locations as each subsequent insertion occurs in a subsequent memory location (which is cache-friendlier) and also less computational work is necessary. If a block is removed from the iterative sequence its recyclable memory locations are also not usable, hence subsequent insertions are more likely to be pushed to the back of the hive.
  9. Why not reserve all empty memory blocks for future use during erasure, or None, rather than leaving this decision undefined by the specification?

    The default scenario, for reasons of predictability, should be to free the memory block in most cases. However for the reasons described in the design decisions section on erase(), retaining the back block at least has performance and latency benefits. Therefore retaining no memory blocks is non-optimal in cases where the user is not using a custom allocator. Meanwhile, retaining All memory blocks is bad for performance as many small memory blocks will be retained, which decreases iterative performance due to lower cache locality. However, one perspective is that if a scenario calls for retaining memory blocks instead of deallocating them, this should be left to an allocator to manage. Otherwise you get unpredictable memory behavior across implementations, and this is one of the things that SG14 members have complained about consistently with STL implementations. This is currently an open topic for discussion.

  10. Memory block sizes - what are they based on, how do they expand, etc

    While implementations are free to chose their own limits and strategies here, in the reference implementation memory block sizes start from either the dynamically-defined default minimum size (8 elements, larger if the type stored is small) or an amount defined by the end user (with a minimum of 3 elements, as there is enough metadata per-block that less than 3 elements is generally a waste of memory unless the value_type is extremely large). Subsequent block sizes then increase the total capacity of the hive by a factor of 2 (so, 1st block 8 elements, 2nd 8 elements, 3rd 16 elements, 4th 32 elements etcetera) until the maximum block size is reached. The default maximum block size in the reference implementation is the maximum possible number that the skipfield bitdepth is capable of representing (std::numeric_limits<skipfield_type>::max()). By default the skipfield bitdepth is 16 so the maximum size of a block would be 65535 elements in that context.

    The skipfield bitdepth was initially a template parameter which could be set to any unsigned integer - unsigned char, unsigned int, Uint_64, etc. Unsigned short (guaranteed to be at least 16 bit, equivalent to C++11's uint_least16_t type) was found to have the best performance in real-world testing on x86 and x86_64 platforms due to the balance between memory contiguousness, memory waste and the number of allocations. unsigned char was found to have better performance below 1000 elements and of course lower memory use. Other platforms have not been tested. Since only two values were considered useful, they've been replaced in newer versions by a priority parameter, which specifies whether the priority of the instantiation is memory use or performance. While this is not strictly true in the sense that unsigned char will also have better performance for under 1000 elements, it is a compromise in order to have the implementation reflect a standard which may enable other implementations which do not share the same performance characteristics.

  11. Can a hive be used with SIMD instructions?

    No and yes. Yes if you're careful, no if you're not.
    On platforms which support scatter and gather operations via hardware (e.g. AVX512) you can use hive with SIMD as much as you want, using gather to load elements from disparate or sequential locations, directly into a SIMD register, in parallel. Then use scatter to push the post-SIMD-process values elsewhere after. On platforms which do not support this in hardware, you would need to manually implement a scalar gather-and-scatter operation which may be significantly slower.

    In situations where gather and scatter operations are too expensive, which require elements to be contiguous in memory for SIMD processing, this is more complicated. When you have a bunch of erasures in a hive, there's no guarantee that your objects will be contiguous in memory, even though they are sequential during iteration. Some of them may also be in different memory blocks to each other. In these situations if you want to use SIMD with hive, you must do the following:

    • Set your minimum and maximum group sizes to multiples of the width of your target processor's SIMD instruction size. If it supports 8 elements at once, set the group sizes to multiples of 8.
    • Either never erase from the hive, or:
      1. Shrink-to-fit after you erase (will invalidate all pointers to elements within the hive).
      2. Only erase from the back or front of the hive, and only erase elements in multiples of the width of your SIMD instruction e.g. 8 consecutive elements at once. This will ensure that the end-of-memory-block boundaries line up with the width of the SIMD instruction, provided you've set your min/max block sizes as above.

    Generally if you want to use SIMD without gather/scatter, it's probably preferable to use a vector or an array.

Appendix D - Specific responses to previous committee feedback

  1. Naming

    See D2332R0.

  2. "Unordered and no associative lookup, so this only supports use cases where you're going to do something to every element."

    As noted the container was originally designed for highly object-oriented situations where you have many elements in different containers linking to many other elements in other containers. This linking can be done with pointers or iterators in hive (insert returns an iterator which can be dereferenced to get a pointer, pointers can be converted into iterators with the supplied functions (for erase etc)) and because pointers/iterators stay stable regardless of insertion/erasure, this usage is unproblematic. You could say the pointer is equivalent to a key in this case (but without the overhead). That is the first access pattern, the second is straight iteration over the container, as you say. Secondly, the container does have (typically better than O(n)) advance/next/prev implementations, so multiple elements can be skipped.

  3. "Do we really need the Priority template parameter?"

    While technically a non-binding request, this parameter promotes the use of the container in heavily memory-constrained environments like embedded programming. In the context of the reference implementation this means switching the skipfield type from unsigned short to unsigned char, in other implementations it could mean something else. See more explanation in V. Technical Specifications. Unfortunately this parameter also means operator=, swap and some other functions won't work between hives of the same type but with differing priorities.

  4. "Prove this is not an allocator"

    I'm not really sure how to answer this, as I don't see the resemblance, unless you count maps, vectors etc as being allocators also. The only aspect of it which resembles what an allocator might do, is the memory re-use mechanism. It would be impossible for an allocator to perform a similar function while still allowing the container to iterate over the data linearly in memory, preserving locality, in the manner described in this document.

  5. "If this is for games, won't game devs just write their own versions for specific types in order to get a 1% speed increase anyway?"

    This is true for many/most AAA game companies who are on the bleeding edge, but they also do this for vector etc, so they aren't the target audience of std:: for the most part; sub-AAA game companies are more likely to use third party/pre-existing tools. As mentioned earlier, this structure (bucket-array-like) crops up in many, many fields, not just game dev. So the target audience is probably everyone other than AAA gaming, but even then, it facilitates communication across fields and companies as to this type of container, giving it a standardized name and understanding.

  6. "Is there active research in this problem space? Is it likely to change in future?"

    The only current analysis has been around the question of whether it's possible for this specification to fail to allow for a better implementation in future. This is unlikely given the container's requirements and how this impacts on implementation. Bucket arrays have been around since the 1990s, there's been no significant innovation in them until now. I've been researching/working on hive since early 2015, and while I can't say for sure that a better implementation might not be possible, I am confident that no change should be necessary to the specification to allow for future implementations, if it is done correctly.

    The requirement of allowing no reallocations upon insertion or erasure, truncates possible implementation strategies significantly. Memory blocks have to be independently allocated so that they can be removed (when empty) without triggering reallocation of subsequent elements. There's limited numbers of ways to do that and keep track of the memory blocks at the same time. Erased element locations must be recorded (for future re-use by insertion) in a way that doesn't create allocations upon erasure, and there's limited numbers of ways to do this also. Multiple consecutive erased elements have to be skipped in O(1) time, and again there's limits to how many ways you can do that. That covers the three core aspects upon which this specification is based. See IV. Design Decisions for the various ways these aspects can be designed.

    The time complexity of updates to whatever erased-element skipping mechanism is used should, I think, be left implementation-defined, as defining time complexity may obviate better solutions which are faster but are not necessarily O(1). These updates would likely occur during erasure, insertion, splicing and container copying.

  7. Why not iterate across the memory blocks backwards to find the first block with erasures to reuse, during insert?

    While this would statistically ensure that smaller blocks get deallocated first due to becoming empty faster than later blocks, it introduces uncertain latency issues during insert, particularly when custom memory block sizes are used and the number of elements is large. With the current implementation there is an intrusive list of blocks with erasures, and within each block's metadata there's a free list of skipblocks. When reusing the current head of the intrusive list determines the block, and the current head of that block's free list determines the skipblock to be reused. This means that the most recently erased element will be the first to reused. This works out well for two reasons: currently-contiguous sequences of elements will tend to stay that way, helping cache coherence, and when elements are erased and inserted in sequence those erased memory locations will tend to be already in the cache when inserting. Lastly, this structure involves a minimum of branching and checks, resulting in minimal latency during insertion and erasure.

Appendix E - Typical game engine requirements

Here are some more specific requirements with regards to game engines, verified by game developers within SG14:

  1. Elements within data collections refer to elements within other data collections (through a variety of methods - indices, pointers, etc). These references must stay valid throughout the course of the game/level. Any container which causes pointer or index invalidation creates difficulties or necessitates workarounds.
  2. Order is unimportant for the most part. The majority of data is simply iterated over, transformed, referred to and utilized with no regard to order.
  3. Erasing or otherwise "deactivating" objects occurs frequently in performance-critical code. For this reason methods of erasure which create strong performance penalties are avoided.
  4. Inserting new objects in performance-critical code (during gameplay) is also common - for example, a tree drops leaves, or a player spawns in an online multiplayer game.
  5. It is not always clear in advance how many elements there will be in a container at the beginning of development, or at the beginning of a level during play. Genericized game engines in particular have to adapt to considerably different user requirements and scopes. For this reason extensible containers which can expand and contract in realtime are necessary.
  6. Due to the effects of cache on performance, memory storage which is more-or-less contiguous is preferred.
  7. Memory waste is avoided.

std::vector in its default state does not meet these requirements due to:

  1. Poor (non-fill) single insertion performance (regardless of insertion position) due to the need for reallocation upon reaching capacity
  2. Insert invalidates pointers/iterators to all elements
  3. Erase invalidates pointers/iterators/indexes to all elements after the erased element

Game developers therefore either develop custom solutions for each scenario or implement workarounds for vector. The most common workarounds are most likely the following or derivatives:

  1. Using a boolean flag or similar to indicate the inactivity of an object (as opposed to actually erasing from the vector). Elements flagged as inactive are skipped during iteration.

    Advantages: Fast "deactivation". Easy to manage in multi-access environments.
    Disadvantages: Can be slower to iterate due to branching.
  2. Using a vector of data and a secondary vector of indexes. When erasing, the erasure occurs only in the vector of indexes, not the vector of data. When iterating it iterates over the vector of indexes and accesses the data from the vector of data via the remaining indexes.

    Advantages: Fast iteration.
    Disadvantages: Erasure still incurs some reallocation cost which can increase jitter.
  3. Combining a swap-with-back-element-and-pop approach to erasure with some form of dereferenced lookup system to enable contiguous element iteration (sometimes called a 'packed array': http://bitsquid.blogspot.ca/2011/09/managing-decoupling-part-4-id-lookup.html).
    Advantages: Iteration is at standard vector speed.
    Disadvantages: Erasure will be slow if objects are large and/or non-trivially copyable, thereby making swap costs large. All link-based access to elements incur additional costs due to the dereferencing system.

hive brings a more generic solution to these contexts. While some developers, particularly AAA developers, will almost always develop a custom solution for specific use-cases within their engine, I believe most sub-AAA and indie developers are more likely to rely on third party solutions. Regardless, standardising the container will allow for greater cross-discipline communication.

Appendix F - Time complexity requirement explanations

Insert (single): O(1)

One of the requirements of hive is that pointers to non-erased elements stay valid regardless of insertion/erasure within the container. For this reason the container must use multiple memory blocks. If a single memory block were used, like in a std::vector, reallocation of elements would occur when the container expanded (and the elements were copied to a larger memory block). Instead, hive will insert into existing memory blocks when able, and create a new memory block when all existing memory blocks are full. This keeps insertion at O(1).

Insert (multiple): O(N)

Multiple insertions may allow an implementation to reserve suitably-sized memory blocks in advance, reducing the number of allocations necessary (whereas singular insertion would generally follow the implementation's block growth pattern, possibly allocating more than necessary). However when it comes to time complexity it has no advantages over singular insertion, is linear to the number elements inserted.

Erase (single): O(1)

Erasure is a simple matter of destructing the element in question and updating whatever data is associated with the erased-element skipping mechanism eg. the skipfield. Since we use a skipping mechanism to avoid erasures during iterator, no reallocation of subsequent elements is necessary and the process is O(1). Additionally, when using a Low-complexity jump-counting pattern the skipfield update is also always O(1).

Note: When a memory block becomes empty of non-erased elements it must be freed to the OS (or reserved for future insertions, depending on implementation) and removed from the hive's sequence of memory blocks. It it was not, we would end up with non-O(1) iteration, since there would be no way to predict how many empty memory blocks there would be between the current memory block being iterated over, and the next memory block with non-erased (active) elements in it.

Erase (multiple): O(N) for non-trivially-destructible types, for trivially-destructible types between O(1) and O(N) depending on range start/end, approximating O(log n) average

In this case, where the element is non-trivially destructible, the time complexity is O(N), with infrequent deallocation necessary from the removal of an empty memory block as noted above. However where the elements are trivially-destructible, if the range spans an entire memory block at any point, that block and it's metadata can simply be removed without doing any individual writes to it's metadata or individual destruction of elements, potentially making this a O(1) operation.

In addition (when dealing with trivially-destructible types) for those memory blocks where only a portion of elements are erased by the range, if no prior erasures have occurred in that memory block you may be able to erase that range in O(1) time, as, for example, if you are using a skipfield there will be no need to check the skipfield within the range for previously erased elements. The reason you would need to check for previously erased elements within that portion's range is so you can update the metadata for that memory block to accurately reflect how many non-erased elements remain within the block. The non-erased element-count metadata is necessary because there is no other way to ascertain when a memory block is empty of non-erased elements, and hence needs to be removed from the hive's iteration sequence. The reasoning for why empty memory blocks must be removed is included in the Erase(single) section, above.

However in most cases the erase range will not perfectly match the size of all memory blocks, and with typical usage of a hive there is usually some prior erasures in most memory blocks. So, for example, when dealing with a hive of a trivially-destructible type, you might end up with a tail portion of the first memory block in the erasure range being erased in O(N) time, the second and intermediary memory block being completely erased and freed in O(1) time, and only a small front portion of the third and final memory block in the range being erased in O(N) time. Hence the time complexity for trivially-destructible elements approximates O(log n) on average, being between O(1) and O(N) depending on the start and end of the erasure range.

std::find: O(N)

This relies on basic iteration so is O(N).

splice: O(1)

hive only does full-container splicing, not partial-container splicing (use range-insert with std::make_move_iterator to achieve the latter, albiet with the loss of pointer validity to the moved range). When splicing, the memory blocks from the source hive are transferred to the destination hive without processing the individual elements. These blocks may either be placed at the front of the hive or the end, depending on how full the source back block is compared to the destination back block. If the destination back block is more full ie. there is less unused space in it, it is better to put it at the beginning of the source block - as otherwise this creates a larger gap to skip during iteration which in turn affects cache locality. If there are unused element memory spaces at the back of the destination container (ie. the final memory block is not full) and a skipfield is used, the skipfield nodes corresponding to those empty spaces must be altered to indicate that these are skipped elements.

Iterator operators ++ and --: O(1) amortized

Generally the time complexity is O(1), and if a skipfield pattern is used it must allow for O(1) skipping of multiple erased elements. However every so often iteration will involve a transistion to the next/previous memory block in the hive's sequence of blocks, depending on whether we are doing ++ or --. At this point a read of the next/previous memory block's corresponding skipfield would be necessary, in case the front/back element(s) in that memory block are erased and hence skipped. So for every block transition, 2 reads of the skipfield are necessary instead of 1. Hence the time complexity is O(1) amortized.

If skipfields are used they must be per-element-memory-block and independent of subsequent/previous memory blocks, as otherwise you end up with a vector for a skipfield, which would need a range erased every time a memory block was removed from the hive (see notes under Erase, above), and reallocation to a larger skipfield memory block when a hive expanded. Both of these procedures carry reallocation costs, meaning you could have thousands of skipfield nodes needing to be reallocated based on a single erasure (from within a memory block which only had one non-erased element left and hence would need to be removed from the hive). This is unacceptable latency for any field involving high timing sensitivity (all of SG14).

begin()/end(): O(1)

For any implementation these should generally be stored as member variables and so returning them is O(1).

advance/next/prev: between O(1) and O(n), depending on current iterator location, distance and implementation. Average for reference implementation approximates O(log N).

The reasoning for this is similar to that of Erase(multiple), above. Complexity is dependent on state of hive, position of iterator and length of distance, but in many cases will be less than linear. It is necessary in a hive to store metadata both about the capacity of each block (for the purpose of iteration) and how many non-erased elements are present within the block (for the purpose of removing blocks from the iterative chain once they become empty). For this reason, intermediary blocks between the iterator's initial block and its final destination block (if these are not the same block, and if the initial block and final block are not immediately adjacent) can be skipped rather than iterated linearly across, by subtracting the "number of non-erased elements" metadata from distance for those blocks.

This means that the only linear time operations are any iterations within the initial block and the final block. However if either the initial or final block have no erased elements (as determined by comparing whether the block's capacity metadata and the block's "number of non-erased elements" metadata are equal), linear iteration can be skipped for that block and pointer/index math used instead to determine distances, reducing complexity to constant time. Hence the best case for this operation is constant time, the worst is linear to the distance.

distance: between O(1) and O(n), depending on current iterator location, distance and implementation. Average for reference implementation approximates O(log N).

The same considerations which apply to advance, prev and next also apply to distance - intermediary blocks between iterator1 and iterator2's blocks can be skipped in constant time, if they exist. iterator1's block and iterator2's block (if these are not the same block) must be linearly iterated across using ++ unless either block has no erased elements, in which case the operation becomes pointer/index math and is reduced to constant time for that block. In addition, if iterator1's block is not the same as iterator2's block, and iterator2 is equal to end() or (end() - 1), or is the last element in that block, iterator2's block's elements can also counted from the metadata rather than iteration.

Appendix G - Why not constexpr?

I am somewhat awkwardly forced into a position where I have to question and push back against the currently-unsubstantiated enthusiasm around constexpr containers and functions. At the time of writing there are no compilers which both support constexpr non-trivial destructors and also have a working implementation of a constexpr container. And until that is remedied, we won't really know what we're dealing with. My own testing in terms of making hive constexpr has not been encouraging. 2% performance decrease in un-altered benchmark code is common, and I suspect the common cause of this is caching values from compile-time when it is cheaper to calculate them on-the-fly than to return them from main memory. This suspicion is based on the substantial increases in executable size in the constexpr versions.

For an example of the latter, think about size() in std::vector. This can be calculated in most implementations by (vector.end_iterator.pointer - vector.memory_block), both of which will most likely be in cache at the time of calling size(). That's if size isn't a member variable or something. Calculating a minus operation on stuff that's already in cache is about 100x faster than making a call out to main memory for a compile-time-stored value of this function, if that is necessary. Hence calculating size() will typically be faster than storing it, but a constexpr implementation and compiler currently won't make that distinction.

None of which is an issue if a container is being entirely used within a constexpr function which has been determined to be evaluated at compile time. The problems occur when constexpr containers are used in runtime code, but certain functions such as size() are determined to be able to be evaluated at compile time, and therefore have their results cached. This is not an okay situation. If there were a mechanism which specified that for a given class instance, it's constexpr functions may not be evaluated at compile time, then I would give the go-ahead. Similarly if there were a rule which stated that a class instance's member functions may only be evaluated at compile time if the class instance is instantiated and destructed at compile time, I would give the go-ahead. This is not the situation we have, and I can't support it.

Constexpr function calls:

  1. shift the responsibility of storage of pre-calculated results from the programmer to the compiler, and remove the ability for the programmer to think about the cache locality/optimal storage of precalculated results
  2. shift the decision of whether or not to evaluate at runtime/compile-time to the compiler, where in some cases doing it at compile-time and storing the result may decrease performance (see the Doom 3 BFG edition technical notes for their experience with pre-calc'ing meshes vs calculating them on the fly)
  3. may dramatically increase code size/file size in some cases if the return results of constexpr functions are large
  4. may dramatically increase compile times
  5. create the potential for cache pollution when constexpr functions return large amounts of data, or when functions returning small amounts of data are called many times

Given this, and the performance issues mentioned above, I am reluctant to make hive constexpr-by-default. Time may sort these issues out, but I am personally happier for std::array and std::vector to be the "canaries in the coalmine" here. Certainly I won't be giving the go-ahead on any change that produces, or can produce, on current compilers, a 2% performance decrease in runtime code. Though I acknowledge the functionality of constexpr code may be useful to many.

Appendix H - Reference implementation differences and link

The reference implementation has a couple of key differences from the proposal, one is that it is named 'colony' by default, for historical and userbase reasons, and typedef'd to hive for optional usage under that name. This is only possible with C++11 and above due to the limits on template typedefs under C++98/03. Likewise the template parameter 'hive_priority' is a regular enum in the reference implementation, instead of a scoped enum, in order to be usable with C++98/03, and is 'colony_priority' by default with a typedef to hive_priority. Lastly the struct 'colony_limits' is also typedef'd to 'hive_limits'. Otherwise the reference implementation is or should be identical with the std::hive proposal.

================================================ FILE: Docs/Proposals/Fixed_Point_Library_Proposal.md ================================================ **Document number**: LEWG, EWG, SG14, SG6: P0037R0 **Date**: 2015-09-28 **Project**: Programming Language C++, Library Evolution WG, SG14 **Reply-to**: John McFarlane, [fixed-point@john.mcfarlane.name](mailto:fixed-point@john.mcfarlane.name) # Fixed-Point Real Numbers ## I. Introduction This proposal introduces a system for performing binary fixed-point arithmetic using built-in integral types. ## II. Motivation Floating-point types are an exceedingly versatile and widely supported method of expressing real numbers on modern architectures. However, there are certain situations where fixed-point arithmetic is preferable. Some systems lack native floating-point registers and must emulate them in software. Many others are capable of performing some or all operations more efficiently using integer arithmetic. Certain applications can suffer from the variability in precision which comes from a dynamic radix point [\[1\]](http://www.pathengine.com/Contents/Overview/FundamentalConcepts/WhyIntegerCoordinates/page.php). In situations where a variable exponent is not desired, it takes valuable space away from the significand and reduces precision. Built-in integer types provide the basis for an efficient representation of binary fixed-point real numbers. However, laborious, error-prone steps are required to normalize the results of certain operations and to convert to and from fixed-point types. A set of tools for defining and manipulating fixed-point types is proposed. These tools are designed to make work easier for those who traditionally use integers to perform low-level, high-performance fixed-point computation. ## III. Impact On the Standard This proposal is a pure library extension. It does not require changes to any standard classes, functions or headers. ## IV. Design Decisions The design is driven by the following aims in roughly descending order: 1. to automate the task of using integer types to perform low-level binary fixed-point arithmetic; 2. to facilitate a style of code that is intuitive to anyone who is comfortable with integer and floating-point arithmetic; 3. to avoid type promotion, implicit conversion or other behavior that might lead to surprising results and 4. to preserve significant digits at the expense of insignificant digits, i.e. to prefer underflow to overflow. ### Class Template Fixed-point numbers are specializations of template class fixed_point; where the template parameters are described as follows. #### `ReprType` Type Template Parameter This parameter identifies the capacity and signedness of the underlying type used to represent the value. In other words, the size of the resulting type will be `sizeof(ReprType)` and it will be signed iff `is_signed::value` is true. The default is `int`. `ReprType` must be a fundamental integral type and should not be the largest size. Suitable types include: `std::int8_t`, `std::uint8_t`, `std::int16_t`, `std::uint16_t`, `std::int32_t` and `std::uint32_t`. In limited situations, `std::int64_t` and `std::uint64_t` can be used. The reasons for these limitations relate to the difficulty in finding a type that is suitable for performing lossless integer multiplication. #### `Exponent` Non-Type Template Parameter The exponent of a fixed-point type is the equivalent of the exponent field in a floating-point type and shifts the stored value by the requisite number of bits necessary to produce the desired range. The default value of `Exponent` is zero, giving `fixed_point` the same range as `T`. The resolution of a specialization of `fixed_point` is pow(2, Exponent) and the minimum and maximum values are std::numeric_limits::min() * pow(2, Exponent) and std::numeric_limits::max() * pow(2, Exponent) respectively. Any usage that results in values of `Exponent` which lie outside the range, (`INT_MIN / 2`, `INT_MAX / 2`), may result in undefined behavior and/or overflow or underflow. This range of exponent values is far in excess of the largest built-in floting-point type and should be adequate for all intents and purposes. ### `make_fixed` and `make_ufixed` Helper Type The `Exponent` template parameter is versatile and concise. It is an intuitive scale to use when considering the full range of positive and negative exponents a fixed-point type might possess. It also corresponds to the exponent field of built-in floating-point types. However, most fixed-point formats can be described more intuitively by the cardinal number of integer and/or fractional digits they contain. Most users will prefer to distinguish fixed-point types using these parameters. For this reason, two aliases are defined in the style of `make_signed`. These aliases are declared as: template using make_fixed; and template using make_ufixed; They resolve to a `fixed_point` specialization with the given signedness and number of integer and fractional digits. They may contain additional integer and fractional digits. For example, one could define and initialize an 8-bit, unsigned, fixed-point variable with four integer digits and four fractional digits: make_ufixed<4, 4> value { 15.9375 }; or a 32-bit, signed, fixed-point number with two integer digits and 29 fractional digits: make_fixed<2, 29> value { 3.141592653 }; ### Conversion Fixed-point numbers can be explicitly converted to and from built-in arithmetic types. While effort is made to ensure that significant digits are not lost during conversion, no effort is made to avoid rounding errors. Whatever would happen when converting to and from an integer type largely applies to `fixed_point` objects also. For example: make_ufixed<4, 4>(.006) == make_ufixed<4, 4>(0) ...equates to `true` and is considered a acceptable rounding error. ### Operator Overloads Any operators that might be applied to integer types can also be applied to fixed-point types. A guiding principle of operator overloads is that they perform as little run-time computation as is practically possible. With the exception of shift and comparison operators, binary operators can take any combination of: * one or two fixed-point arguments and * zero or one arguments of any arithmetic type, i.e. a type for which `is_arithmetic` is true. Where the inputs are not identical fixed-point types, a simple set of promotion-like rules are applied to determine the return type: 1. If both arguments are fixed-point, a type is chosen which is the size of the larger type, is signed if either input is signed and has the maximum integer bits of the two inputs, i.e. cannot lose high-significance bits through conversion alone. 2. If one of the arguments is a floating-point type, then the type of the result is the smallest floating-point type of equal or greater size than the inputs. 3. If one of the arguments is an integral type, then the result is the other, fixed-point type. Some examples: make_ufixed<5, 3>{8} + make_ufixed<4, 4>{3} == make_ufixed<5, 3>{11}; make_ufixed<5, 3>{8} + 3 == make_ufixed<5, 3>{11}; make_ufixed<5, 3>{8} + float{3} == float{11}; The reasoning behind this choice is a combination of predictability and performance. It is explained for each rule as follows: 1. ensures that the least computation is performed where fixed-point types are used exclusively. Aside from multiplication and division requiring shift operations, should require similar computational costs to equivalent integer operations; 2. loosely follows the promotion rules for mixed-mode arithmetic, ensures values with exponents far beyond the range of the fixed-point type are catered for and avoids costly conversion from floating-point to integer and 3. preserves the input fixed-point type whose range is far more likely to be of deliberate importance to the operation. Shift operator overloads require an integer type as the right-hand parameter and return a type which is adjusted to accommodate the new value without risk of overflow or underflow. Comparison operators convert the inputs to a common result type following the rules above before performing a comparison and returning `true` or `false`. #### Overflow Because arithmetic operators return a result of equal capacity to their inputs, they carry a risk of overflow. For instance, make_fixed<4, 3>(15) + make_fixed<4, 3>(1) causes overflow because because a type with 4 integer bits cannot store a value of 16. Overflow of any bits in a signed or unsigned fixed-point type is classed as undefined behavior. This is a minor deviation from built-in integer arithmetic where only signed overflow results in undefined behavior. #### Underflow The other typical cause of lost bits is underflow where, for example, make_fixed<7, 0>(15) / make_fixed<7, 0>(2) results in a value of 7. This results in loss of precision but is generally considered acceptable. However, when all bits are lost due to underflow, the value is said to be flushed and this is classed as undefined behavior. ### Dealing With Overflow and Flushes Errors resulting from overflow and flushes are two of the biggest headaches related to fixed-point arithmetic. Integers suffer the same kinds of errors but are somewhat easier to reason about as they lack fractional digits. Floating-point numbers are largely shielded from these errors by their variable exponent and implicit bit. Three strategies for avoiding overflow in fixed-point types are presented: 1. simply leave it to the user to avoid overflow; 2. promote the result to a larger type to ensure sufficient capacity or 3. adjust the exponent of the result upward to ensure that the top limit of the type is sufficient to preserve the most significant digits at the expense of the less significant digits. For arithmetic operators, choice 1) is taken because it most closely follows the behavior of integer types. Thus it should cause the least surprise to the fewest users. This makes it far easier to reason about in code where functions are written with a particular type in mind. It also requires the least computation in most cases. Choices 2) and 3) are more robust to overflow events. However, they represent different trade-offs and neither one is the best fit in all situations. For these reasons, they are presented as named functions. #### Type Promotion Function template, `promote`, borrows a term from the language feature which avoids integer overflow prior to certain operations. It takes a `fixed_point` object and returns the same value represented by a larger `fixed_point` specialization. For example, promote(make_fixed<5, 2>(15.5)) is equivalent to make_fixed<11, 4>(15.5) Complimentary function template, `demote`, reverses the process, returning a value of a smaller type. #### Named Arithmetic Functions The following named function templates can be used as a hassle-free alternative to arithmetic operators in situations where the aim is to avoid overflow. Unary functions: trunc_reciprocal, trunc_square, trunc_sqrt, promote_reciprocal, promote_square Binary functions: trunc_add, trunc_subtract, trunc_multiply, trunc_divide trunc_shift_left, trunc_shift_right, promote_add, promote_sub, promote_multiply, promote_divide Some notes: 1. The `trunc_` functions return the result as a type no larger than the inputs and with an exponent adjusted to avoid overflow; 2. the `promote_` functions return the result as a type large enough to avoid overflow and underflow; 3. the `_multiply` and `_square` functions are not guaranteed to be available for 64-bit types; 4. the `_multiply` and `_square` functions produce undefined behavior when all input parameters are the *most negative number*; 5. the `_square` functions return an unsigned type; 6. the `_add`, `_subtract`, `_multiply` and `_divide` functions take heterogeneous `fixed_point` specializations; 7. the `_divide` and `_reciprocal` functions in no way guard against divide-by-zero errors; 8. the `trunc_shift_` functions return results of the same type as their first input parameter and 9. the list is by no means complete. ### Example The following example calculates the magnitude of a 3-dimensional vector. template constexpr auto magnitude(const Fp & x, const Fp & y, const Fp & z) -> decltype(trunc_sqrt(trunc_add(trunc_square(x), trunc_square(y), trunc_square(z)))) { return trunc_sqrt(trunc_add(trunc_square(x), trunc_square(y), trunc_square(z))); } Calling the above function as follows static_cast(magnitude( make_ufixed<4, 12>(1), make_ufixed<4, 12>(4), make_ufixed<4, 12>(9))); returns the value, 9.890625. ## V. Technical Specification ### Header \ Synopsis namespace std { template class fixed_point; template using make_fixed; template using make_ufixed; template using make_fixed_from_repr; template using promote_result; template promote_result constexpr promote(const FixedPoint & from) noexcept; template using demote_result; template demote_result constexpr demote(const FixedPoint & from) noexcept; template constexpr bool operator==( const fixed_point & lhs, const fixed_point & rhs) noexcept; template constexpr bool operator!=( const fixed_point & lhs, const fixed_point & rhs) noexcept; template constexpr bool operator<( const fixed_point & lhs, const fixed_point & rhs) noexcept; template constexpr bool operator>( const fixed_point & lhs, const fixed_point & rhs) noexcept; template constexpr bool operator>=( const fixed_point & lhs, const fixed_point & rhs) noexcept; template constexpr bool operator<=( const fixed_point & lhs, const fixed_point & rhs) noexcept; template constexpr fixed_point operator-( const fixed_point & rhs) noexcept; template constexpr fixed_point operator+( const fixed_point & lhs, const fixed_point & rhs) noexcept; template constexpr fixed_point operator-( const fixed_point & lhs, const fixed_point & rhs) noexcept; template fixed_point & operator+=( fixed_point & lhs, const fixed_point & rhs) noexcept; template fixed_point & operator-=( fixed_point & lhs, const fixed_point & rhs) noexcept; template fixed_point & operator*=( fixed_point & lhs, const fixed_point & rhs) noexcept; template fixed_point & operator/=( fixed_point & lhs, const fixed_point & rhs) noexcept; template constexpr auto operator==(const Lhs & lhs, const Rhs & rhs) noexcept; template constexpr auto operator!=(const Lhs & lhs, const Rhs & rhs) noexcept; template constexpr auto operator<(const Lhs & lhs, const Rhs & rhs) noexcept; template constexpr auto operator>(const Lhs & lhs, const Rhs & rhs) noexcept; template constexpr auto operator>=(const Lhs & lhs, const Rhs & rhs) noexcept; template constexpr auto operator<=(const Lhs & lhs, const Rhs & rhs) noexcept; template constexpr auto operator+( const Lhs & lhs, const Rhs & rhs) noexcept; template constexpr auto operator-( const Lhs & lhs, const Rhs & rhs) noexcept; template constexpr auto operator*( const fixed_point & lhs, const fixed_point & rhs) noexcept; template constexpr auto operator/( const fixed_point & lhs, const fixed_point & rhs) noexcept; template constexpr auto operator*( const fixed_point & lhs, const Integer & rhs) noexcept; template constexpr auto operator/( const fixed_point & lhs, const Integer & rhs) noexcept; template constexpr auto operator*( const Integer & lhs, const fixed_point & rhs) noexcept; template constexpr auto operator/( const Integer & lhs, const fixed_point & rhs) noexcept; template constexpr auto operator*( const fixed_point & lhs, const Float & rhs) noexcept; template constexpr auto operator/( const fixed_point & lhs, const Float & rhs) noexcept; template constexpr auto operator*( const Float & lhs, const fixed_point & rhs) noexcept; template constexpr auto operator/( const Float & lhs, const fixed_point & rhs) noexcept; template fixed_point & operator+=(fixed_point & lhs, const Rhs & rhs) noexcept; template fixed_point & operator-=(fixed_point & lhs, const Rhs & rhs) noexcept; template template ::value, int>::type Dummy> fixed_point & fixed_point::operator*=(const Rhs & rhs) noexcept; template template ::value, int>::type Dummy> fixed_point & fixed_point::operator/=(const Rhs & rhs) noexcept; template constexpr fixed_point sqrt(const fixed_point & x) noexcept; template using trunc_add_result; template trunc_add_result constexpr trunc_add(const FixedPoint & addend1, const Tail & ... addend_tail); template using trunc_subtract_result; template trunc_subtract_result constexpr trunc_subtract(const Lhs & minuend, const Rhs & subtrahend); template using trunc_multiply_result; template trunc_multiply_result constexpr trunc_multiply(const Lhs & lhs, const Rhs & rhs) noexcept; template using trunc_divide_result; template trunc_divide_result constexpr trunc_divide(const FixedPointDividend & lhs, const FixedPointDivisor & rhs) noexcept; template using trunc_reciprocal_result; template trunc_reciprocal_result constexpr trunc_reciprocal(const FixedPoint & fixed_point) noexcept; template using trunc_square_result; template trunc_square_result constexpr trunc_square(const FixedPoint & root) noexcept; template using trunc_sqrt_result; template trunc_sqrt_result constexpr trunc_sqrt(const FixedPoint & square) noexcept; template constexpr fixed_point trunc_shift_left(const fixed_point & fp) noexcept; template constexpr fixed_point trunc_shift_right(const fixed_point & fp) noexcept; template using promote_add_result; template promote_add_result constexpr promote_add(const FixedPoint & addend1, const Tail & ... addend_tail); template using promote_subtract_result template promote_subtract_result constexpr promote_subtract(const Lhs & lhs, const Rhs & rhs) noexcept; template using promote_multiply_result; template promote_multiply_result constexpr promote_multiply(const Lhs & lhs, const Rhs & rhs) noexcept; template using promote_divide_result; template promote_divide_result constexpr promote_divide(const Lhs & lhs, const Rhs & rhs) noexcept; template using promote_square_result; template promote_square_result constexpr promote_square(const FixedPoint & root) noexcept; } #### `fixed_point<>` Class Template template class fixed_point { public: using repr_type = ReprType; constexpr static int exponent; constexpr static int digits; constexpr static int integer_digits; constexpr static int fractional_digits; fixed_point() noexcept; template ::value, int>::type Dummy = 0> explicit constexpr fixed_point(S s) noexcept; template ::value, int>::type Dummy = 0> explicit constexpr fixed_point(S s) noexcept; template explicit constexpr fixed_point(const fixed_point & rhs) noexcept; template ::value, int>::type Dummy = 0> fixed_point & operator=(S s) noexcept; template ::value, int>::type Dummy = 0> fixed_point & operator=(S s) noexcept; template fixed_point & operator=(const fixed_point & rhs) noexcept; template ::value, int>::type Dummy = 0> explicit constexpr operator S() const noexcept; template ::value, int>::type Dummy = 0> explicit constexpr operator S() const noexcept; explicit constexpr operator bool() const noexcept; template ::value, int>::type Dummy = 0> fixed_point &operator*=(const Rhs & rhs) noexcept; template ::value, int>::type Dummy = 0> fixed_point & operator/=(const Rhs & rhs) noexcept; constexpr repr_type data() const noexcept; static constexpr fixed_point from_data(repr_type repr) noexcept; }; ## VI. Future Issues ### Library Support Because the aim is to provide an alternative to existing arithmetic types which are supported by the standard library, it is conceivable that a future proposal might specialize existing class templates and overload existing functions. Possible candidates for overloading include the functions defined in \ and a templated specialization of `numeric_limits`. A new type trait, `is_fixed_point`, would also be useful. While `fixed_point` is intended to provide drop-in replacements to existing built-ins, it may be preferable to deviate slightly from the behavior of certain standard functions. For example, overloads of functions from \ will be considerably less concise, efficient and versatile if they obey rules surrounding error cases. In particular, the guarantee of setting `errno` in the case of an error prevents a function from being defined as pure. This highlights a wider issue surrounding the adoption of the functional approach and compile-time computation that is beyond the scope of this document. ### Alternatives to Built-in Integer Types The reason that `ReprType` is restricted to built-in integer types is that a number of features require the use of a higher - or lower-capacity type. Supporting alias templates are defined to provide `fixed_point` with the means to invoke integer types of specific capacity and signedness at compile time. There is no general purpose way of deducing a higher or lower-capacity type given a source type in the same manner as `make_signed` and `make_unsigned`. If there were, this might be adequate to allow alternative choices for `ReprType`. ### Bounded Integers The bounded::integer library [\[2\]](http://doublewise.net/c++/bounded/) exemplifies the benefits of keeping track of ranges of values in arithmetic types at compile time. To a limited extent, the `trunc_` functions defined here also keep track of - and modify - the limits of values. However, a combination of techniques is capable of producing superior results. For instance, consider the following expression: make_ufixed<2, 6> three(3); auto n = trunc_square(trunc_square(three)); The type of `n` is `make_ufixed<8, 0>` but its value does not exceed 81. Hence, an integer bit has been wasted. It may be possible to track more accurate limits in the same manner as the bounded::integer library in order to improve the precision of types returned by `trunc_` functions. For this reason, the exact value of the exponents of these return types is not given. Notes: * Bounded::integer is already supported by fixed-point library, fp [\[3\]](https://github.com/mizvekov/fp). * A similar library is the boost constrained_value library [\[4\]](http://rk.hekko.pl/constrained_value/). ### Alternative Policies The behavior of the types specialized from `fixed_point` represent one sub-set of all potentially desirable behaviors. Alternative characteristics include: * different rounding strategies - other than truncation; * overflow and underflow checks - possibly throwing exceptions; * operator return type - adopting `trunc_` or `promote_` behavior; * default-initialize to zero - currently uninitialized and * saturation arithmetic - as opposed to modular arithmetic. One way to extend `fixed_point` to cover these alternatives would be to add non-type template parameters containing bit flags or enumerated types. The default set of values would reflect `fixed_point` as it stands currently. ## VII. Prior Art Many examples of fixed-point support in C and C++ exist. While almost all of them aim for low run-time cost and expressive alternatives to raw integer manipulation, they vary greatly in detail and in terms of their interface. One especially interesting dichotomy is between solutions which offer a discrete selection of fixed-point types and libraries which contain a continuous range of exponents through type parameterization. ### N1169 One example of the former is found in proposal N1169 [\[5\]](http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1169.pdf), the intent of which is to expose features found in certain embedded hardware. It introduces a succinct set of language-level fixed-point types and impose constraints on the number of integer or fractional digits each can possess. As with all examples of discrete-type fixed-point support, the limited choice of exponents is a considerable restriction on the versatility and expressiveness of the API. Nevertheless, it may be possible to harness performance gains provided by N1169 fixed-point types through explicit template specialization. This is likely to be a valuable proposition to potential users of the library who find themselves targeting platforms which support fixed-point arithmetic at the hardware level. ### N3352 There are many other C++ libraries available which fall into the latter category of continuous-range fixed-point arithmetic [\[3\]](https://github.com/mizvekov/fp) [\[6\]](http://www.codeproject.com/Articles/37636/Fixed-Point-Class) [\[7\]](https://github.com/viboes/fixed_point). In particular, an existing library proposal, N3352 [\[8\]](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3352.html), aims to achieve very similar goals through similar means and warrants closer comparison than N1169. N3352 introduces four class templates covering the quadrant of signed versus unsigned and fractional versus integer numeric types. It is intended to replace built-in types in a wide variety of situations and accordingly, is highly compile-time configurable in terms of how rounding and overflow are handled. Parameters to these four class templates include the storage in bits and - for fractional types - the resolution. The `fixed_point` class template could probably - with a few caveats - be generated using the two fractional types, `nonnegative` and `negatable`, replacing the `ReprType` parameter with the integer bit count of `ReprType`, specifying `fastest` for the rounding mode and specifying `undefined` as the overflow mode. However, fixed_point more closely and concisely caters to the needs of users who already use integer types and simply desire a more concise, less error-prone form. It more closely follows the four design aims of the library and - it can be argued - more closely follows the spirit of the standard in its pursuit of zero-cost abstraction. Some aspects of the design of the N3352 API which back up these conclusion are that: * the result of arithmetic operations closely resemble the `trunc_` function templates and are potentially more costly at run-time; * the nature of the range-specifying template parameters - through careful framing in mathematical terms - abstracts away valuable information regarding machine-critical type size information; * the breaking up of duties amongst four separate class templates introduces four new concepts and incurs additional mental load for relatively little gain while further detaching the interface from vital machine-level details and * the absence of the most negative number from signed types reduces the capacity of all types by one. The added versatility that the N3352 API provides regarding rounding and overflow handling are of relatively low priority to users who already bear the scars of battles with raw integer types. Nevertheless, providing them as options to be turned on or off at compile time is an ideal way to leave the choice in the hands of the user. Many high-performance applications - in which fixed-point is of potential value - favor run-time checks during development which are subsequently deactivated in production builds. The N3352 interface is highly conducive to this style of development. It is an aim of the fixed_point design to be similarly extensible in future revisions. ## VIII. Acknowledgements Subgroup: Guy Davidson, Michael Wong Contributors: Ed Ainsley, Billy Baker, Lance Dyson, Marco Foco, Clément Grégoire, Nicolas Guillemot, Matt Kinzelman, Joël Lamotte, Sean Middleditch, Patrice Roy, Peter Schregle, Ryhor Spivak ## IX. References 1. Why Integer Coordinates?, 2. C++ bounded::integer library, 3. fp, C++14 Fixed Point Library, 4. Boost Constrained Value Libarary, 5. N1169, Extensions to support embedded processors, 6. fpmath, Fixed Point Math Library, 7. Boost fixed_point (proposed), Fixed point integral and fractional types, 8. N3352, C++ Binary Fixed-Point Arithmetic, 9. fixed_point, Reference Implementation of P0037, ## X. Appendix 1: Reference Implementation An in-development implementation of the fixed_point class template and its essential supporting functions and types is available [\[9\]](https://github.com/johnmcfarlane/fixed_point). It includes a utility header containing such things as math and trigonometric functions and a partial `numeric_limits` specialization. Compile-time and run-time tests are included as well as benchmarking support. It is the source of examples and measurements cited here. ## XI. Appendix 2: Performance Despite a focus on usable interface and direct translation from integer-based fixed-point operations, there is an overwhelming expectation that the source code result in minimal instructions and clock cycles. A few preliminary numbers are presented to give a very early idea of how the API might perform. Some notes: * A few test functions were run, ranging from single arithmetic operations to basic geometric functions, performed against integer, floating-point and fixed-point types for comparison. * Figures were taken from a single CPU, OS and compiler, namely: Debian clang version 3.5.0-10 (tags/RELEASE_350/final) (based on LLVM 3.5.0) Target: x86_64-pc-linux-gnu Thread model: posix * Fixed inputs were provided to each function, meaning that branch prediction rarely fails. Results may also not represent the full range of inputs. * Details of the test harness used can be found in the source project mentioned in Appendix 1; * Times are in nanoseconds; * Code has not yet been optimized for performance. ### Types Where applicable various combinations of integer, floating-point and fixed-point types were tested with the following identifiers: * `uint8_t`, `int8_t`, `uint16_t`, `int16_t`, `uint32_t`, `int32_t`, `uint64_t` and `int64_t` built-in integer types; * `float`, `double` and `long double` built-in floating-point types; * s3:4, u4:4, s7:8, u8:8, s15:16, u16:16, s31:32 and u32:32 format fixed-point types. ### Basic Arithmetic Plus, minus, multiplication and division were tested in isolation using a number of different numeric types with the following results: name cpu_time add(float) 1.78011 add(double) 1.73966 add(long double) 3.46011 add(u4_4) 1.87726 add(s3_4) 1.85051 add(u8_8) 1.85417 add(s7_8) 1.82057 add(u16_16) 1.94194 add(s15_16) 1.93463 add(u32_32) 1.94674 add(s31_32) 1.94446 add(int8_t) 2.14857 add(uint8_t) 2.12571 add(int16_t) 1.9936 add(uint16_t) 1.88229 add(int32_t) 1.82126 add(uint32_t) 1.76 add(int64_t) 1.76 add(uint64_t) 1.83223 sub(float) 1.96617 sub(double) 1.98491 sub(long double) 3.55474 sub(u4_4) 1.77006 sub(s3_4) 1.72983 sub(u8_8) 1.72983 sub(s7_8) 1.72983 sub(u16_16) 1.73966 sub(s15_16) 1.85051 sub(u32_32) 1.88229 sub(s31_32) 1.87063 sub(int8_t) 1.76 sub(uint8_t) 1.74994 sub(int16_t) 1.82126 sub(uint16_t) 1.83794 sub(int32_t) 1.89074 sub(uint32_t) 1.85417 sub(int64_t) 1.83703 sub(uint64_t) 2.04914 mul(float) 1.9376 mul(double) 1.93097 mul(long double) 102.446 mul(u4_4) 2.46583 mul(s3_4) 2.09189 mul(u8_8) 2.08 mul(s7_8) 2.18697 mul(u16_16) 2.12571 mul(s15_16) 2.10789 mul(u32_32) 2.10789 mul(s31_32) 2.10789 mul(int8_t) 1.76 mul(uint8_t) 1.78011 mul(int16_t) 1.8432 mul(uint16_t) 1.76914 mul(int32_t) 1.78011 mul(uint32_t) 2.19086 mul(int64_t) 1.7696 mul(uint64_t) 1.79017 div(float) 5.12 div(double) 7.64343 div(long double) 8.304 div(u4_4) 3.82171 div(s3_4) 3.82171 div(u8_8) 3.84 div(s7_8) 3.8 div(u16_16) 9.152 div(s15_16) 11.232 div(u32_32) 30.8434 div(s31_32) 34 div(int8_t) 3.82171 div(uint8_t) 3.82171 div(int16_t) 3.8 div(uint16_t) 3.82171 div(int32_t) 3.82171 div(uint32_t) 3.81806 div(int64_t) 10.2286 div(uint64_t) 8.304 Among the slowest types are `long double`. It is likely that they are emulated in software. The next slowest operations are fixed-point multiply and divide operations - especially with 64-bit types. This is because values need to be promoted temporarily to double-width types. This is a known fixed-point technique which inevitably experiences slowdown where a 128-bit type is required on a 64-bit system. Here is a section of the disassembly of the s15:16 multiply call: 30: mov %r14,%rax mov %r15,%rax movslq -0x28(%rbp),%rax movslq -0x30(%rbp),%rcx imul %rax,%rcx shr $0x10,%rcx mov %ecx,-0x38(%rbp) mov %r12,%rax 4c: movzbl (%rbx),%eax cmp $0x1,%eax ↓ jne 68 54: mov 0x8(%rbx),%rax lea 0x1(%rax),%rcx mov %rcx,0x8(%rbx) cmp 0x38(%rbx),%rax ↑ jb 30 The two 32-bit numbers are multiplied together and the result shifted down - much as it would if raw `int` values were used. The efficiency of this operation varies with the exponent. An exponent of zero should mean no shift at all. ### 3-Dimensional Magnitude Squared A fast `sqrt` implementation has not yet been tested with `fixed_point`. (The naive implementation takes over 300ns.) For this reason, a magnitude-squared function is measured, combining multiply and add operations: template constexpr FP magnitude_squared(const FP & x, const FP & y, const FP & z) { return x * x + y * y + z * z; } Only real number formats are tested: float 2.42606 double 2.08 long double 4.5056 s3_4 2.768 s7_8 2.77577 s15_16 2.752 s31_32 4.10331 Again, the size of the type seems to have the largest impact. ### Circle Intersection A similar operation includes a comparison and branch: template bool circle_intersect_generic(Real x1, Real y1, Real r1, Real x2, Real y2, Real r2) { auto x_diff = x2 - x1; auto y_diff = y2 - y1; auto distance_squared = x_diff * x_diff + y_diff * y_diff; auto touch_distance = r1 + r2; auto touch_distance_squared = touch_distance * touch_distance; return distance_squared <= touch_distance_squared; } float 3.46011 double 3.48 long double 6.4 s3_4 3.88 s7_8 4.5312 s15_16 3.82171 s31_32 5.92 Again, fixed-point and native performance are comparable. ================================================ FILE: Docs/Proposals/p0037.html ================================================ Fixed_Point_Library_Proposal

Document number: LEWG, EWG, SG14, SG6: P0037R0
Date: 2015-09-28
Project: Programming Language C++, Library Evolution WG, SG14
Reply-to: John McFarlane, fixed-point@john.mcfarlane.name

Fixed-Point Real Numbers

I. Introduction

This proposal introduces a system for performing binary fixed-point arithmetic using built-in integral types.

II. Motivation

Floating-point types are an exceedingly versatile and widely supported method of expressing real numbers on modern architectures.

However, there are certain situations where fixed-point arithmetic is preferable. Some systems lack native floating-point registers and must emulate them in software. Many others are capable of performing some or all operations more efficiently using integer arithmetic. Certain applications can suffer from the variability in precision which comes from a dynamic radix point [1]. In situations where a variable exponent is not desired, it takes valuable space away from the significand and reduces precision.

Built-in integer types provide the basis for an efficient representation of binary fixed-point real numbers. However, laborious, error-prone steps are required to normalize the results of certain operations and to convert to and from fixed-point types.

A set of tools for defining and manipulating fixed-point types is proposed. These tools are designed to make work easier for those who traditionally use integers to perform low-level, high-performance fixed-point computation.

III. Impact On the Standard

This proposal is a pure library extension. It does not require changes to any standard classes, functions or headers.

IV. Design Decisions

The design is driven by the following aims in roughly descending order:

  1. to automate the task of using integer types to perform low-level binary fixed-point arithmetic;
  2. to facilitate a style of code that is intuitive to anyone who is comfortable with integer and floating-point arithmetic;
  3. to avoid type promotion, implicit conversion or other behavior that might lead to surprising results and
  4. to preserve significant digits at the expense of insignificant digits, i.e. to prefer underflow to overflow.

Class Template

Fixed-point numbers are specializations of

template <class ReprType, int Exponent>
class fixed_point;

where the template parameters are described as follows.

ReprType Type Template Parameter

This parameter identifies the capacity and signedness of the underlying type used to represent the value. In other words, the size of the resulting type will be sizeof(ReprType) and it will be signed iff is_signed<ReprType>::value is true. The default is int.

ReprType must be a fundamental integral type and should not be the largest size. Suitable types include: std::int8_t, std::uint8_t, std::int16_t, std::uint16_t, std::int32_t and std::uint32_t. In limited situations, std::int64_t and std::uint64_t can be used. The reasons for these limitations relate to the difficulty in finding a type that is suitable for performing lossless integer multiplication.

Exponent Non-Type Template Parameter

The exponent of a fixed-point type is the equivalent of the exponent field in a floating-point type and shifts the stored value by the requisite number of bits necessary to produce the desired range. The default value of Exponent is zero, giving fixed_point<T> the same range as T.

The resolution of a specialization of fixed_point is

pow(2, Exponent)

and the minimum and maximum values are

std::numeric_limits<ReprType>::min() * pow(2, Exponent)

and

std::numeric_limits<ReprType>::max() * pow(2, Exponent)

respectively.

Any usage that results in values of Exponent which lie outside the range, (INT_MIN / 2, INT_MAX / 2), may result in undefined behavior and/or overflow or underflow. This range of exponent values is far in excess of the largest built-in floting-point type and should be adequate for all intents and purposes.

make_fixed and make_ufixed Helper Type

The Exponent template parameter is versatile and concise. It is an intuitive scale to use when considering the full range of positive and negative exponents a fixed-point type might possess. It also corresponds to the exponent field of built-in floating-point types.

However, most fixed-point formats can be described more intuitively by the cardinal number of integer and/or fractional digits they contain. Most users will prefer to distinguish fixed-point types using these parameters.

For this reason, two aliases are defined in the style of make_signed.

These aliases are declared as:

template <unsigned IntegerDigits, unsigned FractionalDigits = 0, bool IsSigned = true>
  using make_fixed;

and

template <unsigned IntegerDigits, unsigned FractionalDigits = 0>
using make_ufixed;

They resolve to a fixed_point specialization with the given signedness and number of integer and fractional digits. They may contain additional integer and fractional digits.

For example, one could define and initialize an 8-bit, unsigned, fixed-point variable with four integer digits and four fractional digits:

make_ufixed<4, 4> value { 15.9375 };

or a 32-bit, signed, fixed-point number with two integer digits and 29 fractional digits:

make_fixed<2, 29> value { 3.141592653 };

Conversion

Fixed-point numbers can be explicitly converted to and from built-in arithmetic types.

While effort is made to ensure that significant digits are not lost during conversion, no effort is made to avoid rounding errors. Whatever would happen when converting to and from an integer type largely applies to fixed_point objects also. For example:

make_ufixed<4, 4>(.006) == make_ufixed<4, 4>(0)

...equates to true and is considered a acceptable rounding error.

Operator Overloads

Any operators that might be applied to integer types can also be applied to fixed-point types. A guiding principle of operator overloads is that they perform as little run-time computation as is practically possible.

With the exception of shift and comparison operators, binary operators can take any combination of:

  • one or two fixed-point arguments and
  • zero or one arguments of any arithmetic type, i.e. a type for which is_arithmetic is true.

Where the inputs are not identical fixed-point types, a simple set of promotion-like rules are applied to determine the return type:

  1. If both arguments are fixed-point, a type is chosen which is the size of the larger type, is signed if either input is signed and has the maximum integer bits of the two inputs, i.e. cannot lose high-significance bits through conversion alone.
  2. If one of the arguments is a floating-point type, then the type of the result is the smallest floating-point type of equal or greater size than the inputs.
  3. If one of the arguments is an integral type, then the result is the other, fixed-point type.

Some examples:

make_ufixed<5, 3>{8} + make_ufixed<4, 4>{3} == make_ufixed<5, 3>{11};  
make_ufixed<5, 3>{8} + 3 == make_ufixed<5, 3>{11};  
make_ufixed<5, 3>{8} + float{3} == float{11};  

The reasoning behind this choice is a combination of predictability and performance. It is explained for each rule as follows:

  1. ensures that the least computation is performed where fixed-point types are used exclusively. Aside from multiplication and division requiring shift operations, should require similar computational costs to equivalent integer operations;
  2. loosely follows the promotion rules for mixed-mode arithmetic, ensures values with exponents far beyond the range of the fixed-point type are catered for and avoids costly conversion from floating-point to integer and
  3. preserves the input fixed-point type whose range is far more likely to be of deliberate importance to the operation.

Shift operator overloads require an integer type as the right-hand parameter and return a type which is adjusted to accommodate the new value without risk of overflow or underflow.

Comparison operators convert the inputs to a common result type following the rules above before performing a comparison and returning true or false.

Overflow

Because arithmetic operators return a result of equal capacity to their inputs, they carry a risk of overflow. For instance,

make_fixed<4, 3>(15) + make_fixed<4, 3>(1)

causes overflow because because a type with 4 integer bits cannot store a value of 16.

Overflow of any bits in a signed or unsigned fixed-point type is classed as undefined behavior. This is a minor deviation from built-in integer arithmetic where only signed overflow results in undefined behavior.

Underflow

The other typical cause of lost bits is underflow where, for example,

make_fixed<7, 0>(15) / make_fixed<7, 0>(2)

results in a value of 7. This results in loss of precision but is generally considered acceptable.

However, when all bits are lost due to underflow, the value is said to be flushed and this is classed as undefined behavior.

Dealing With Overflow and Flushes

Errors resulting from overflow and flushes are two of the biggest headaches related to fixed-point arithmetic. Integers suffer the same kinds of errors but are somewhat easier to reason about as they lack fractional digits. Floating-point numbers are largely shielded from these errors by their variable exponent and implicit bit.

Three strategies for avoiding overflow in fixed-point types are presented:

  1. simply leave it to the user to avoid overflow;
  2. promote the result to a larger type to ensure sufficient capacity or
  3. adjust the exponent of the result upward to ensure that the top limit of the type is sufficient to preserve the most significant digits at the expense of the less significant digits.

For arithmetic operators, choice 1) is taken because it most closely follows the behavior of integer types. Thus it should cause the least surprise to the fewest users. This makes it far easier to reason about in code where functions are written with a particular type in mind. It also requires the least computation in most cases.

Choices 2) and 3) are more robust to overflow events. However, they represent different trade-offs and neither one is the best fit in all situations. For these reasons, they are presented as named functions.

Type Promotion

Function template, promote, borrows a term from the language feature which avoids integer overflow prior to certain operations. It takes a fixed_point object and returns the same value represented by a larger fixed_point specialization.

For example,

promote(make_fixed<5, 2>(15.5))

is equivalent to

make_fixed<11, 4>(15.5)

Complimentary function template, demote, reverses the process, returning a value of a smaller type.

Named Arithmetic Functions

The following named function templates can be used as a hassle-free alternative to arithmetic operators in situations where the aim is to avoid overflow.

Unary functions:

trunc_reciprocal, trunc_square, trunc_sqrt,
promote_reciprocal, promote_square

Binary functions:

trunc_add, trunc_subtract, trunc_multiply, trunc_divide
trunc_shift_left, trunc_shift_right,
promote_add, promote_sub, promote_multiply, promote_divide

Some notes:

  1. The trunc_ functions return the result as a type no larger than the inputs and with an exponent adjusted to avoid overflow;
  2. the promote_ functions return the result as a type large enough to avoid overflow and underflow;
  3. the _multiply and _square functions are not guaranteed to be available for 64-bit types;
  4. the _multiply and _square functions produce undefined behavior when all input parameters are the most negative number;
  5. the _square functions return an unsigned type;
  6. the _add, _subtract, _multiply and _divide functions take heterogeneous fixed_point specializations;
  7. the _divide and _reciprocal functions in no way guard against divide-by-zero errors;
  8. the trunc_shift_ functions return results of the same type as their first input parameter and
  9. the list is by no means complete.

Example

The following example calculates the magnitude of a 3-dimensional vector.

template <class Fp>
constexpr auto magnitude(const Fp & x, const Fp & y, const Fp & z)
-> decltype(trunc_sqrt(trunc_add(trunc_square(x), trunc_square(y), trunc_square(z))))
{
    return trunc_sqrt(trunc_add(trunc_square(x), trunc_square(y), trunc_square(z)));
}

Calling the above function as follows

static_cast<double>(magnitude(
    make_ufixed<4, 12>(1),
    make_ufixed<4, 12>(4),
    make_ufixed<4, 12>(9)));

returns the value, 9.890625.

V. Technical Specification

Header \ Synopsis

namespace std {
  template <class ReprType, int Exponent> class fixed_point;
 
  template <unsigned IntegerDigits, unsigned FractionalDigits = 0, bool IsSigned = true>
    using make_fixed;
  template <unsigned IntegerDigits, unsigned FractionalDigits = 0>
    using make_ufixed;
  template <class ReprType, int IntegerDigits>
    using make_fixed_from_repr;
 
  template <class FixedPoint>
    using promote_result;
  template <class FixedPoint>
    promote_result<FixedPoint>
      constexpr promote(const FixedPoint & from) noexcept;
 
  template <class FixedPoint>
    using demote_result;
  template <class FixedPoint>
    demote_result<FixedPoint>
      constexpr demote(const FixedPoint & from) noexcept;
 
  template <class ReprType, int Exponent>
      constexpr bool operator==(
        const fixed_point<ReprType, Exponent> & lhs,
        const fixed_point<ReprType, Exponent> & rhs) noexcept;
  template <class ReprType, int Exponent>
    constexpr bool operator!=(
        const fixed_point<ReprType, Exponent> & lhs,
      const fixed_point<ReprType, Exponent> & rhs) noexcept;
  template <class ReprType, int Exponent>
      constexpr bool operator<(
        const fixed_point<ReprType, Exponent> & lhs,
        const fixed_point<ReprType, Exponent> & rhs) noexcept;
  template <class ReprType, int Exponent>
      constexpr bool operator>(
        const fixed_point<ReprType, Exponent> & lhs,
        const fixed_point<ReprType, Exponent> & rhs) noexcept;
  template <class ReprType, int Exponent>
      constexpr bool operator>=(
        const fixed_point<ReprType, Exponent> & lhs,
        const fixed_point<ReprType, Exponent> & rhs) noexcept;
  template <class ReprType, int Exponent>
      constexpr bool operator<=(
        const fixed_point<ReprType, Exponent> & lhs,
        const fixed_point<ReprType, Exponent> & rhs) noexcept;
 
  template <class ReprType, int Exponent>
      constexpr fixed_point<ReprType, Exponent> operator-(
        const fixed_point<ReprType, Exponent> & rhs) noexcept;
  template <class ReprType, int Exponent>
      constexpr fixed_point<ReprType, Exponent> operator+(
        const fixed_point<ReprType, Exponent> & lhs,
        const fixed_point<ReprType, Exponent> & rhs) noexcept;
  template <class ReprType, int Exponent>
      constexpr fixed_point<ReprType, Exponent> operator-(
        const fixed_point<ReprType, Exponent> & lhs,
        const fixed_point<ReprType, Exponent> & rhs) noexcept;
  template <class ReprType, int Exponent>
      fixed_point<ReprType, Exponent> & operator+=(
        fixed_point<ReprType, Exponent> & lhs,
        const fixed_point<ReprType, Exponent> & rhs) noexcept;
  template <class ReprType, int Exponent>
      fixed_point<ReprType, Exponent> & operator-=(
        fixed_point<ReprType, Exponent> & lhs,
        const fixed_point<ReprType, Exponent> & rhs) noexcept;
  template <class ReprType, int Exponent>
      fixed_point<ReprType, Exponent> & operator*=(
        fixed_point<ReprType, Exponent> & lhs,
        const fixed_point<ReprType, Exponent> & rhs) noexcept;
  template <class ReprType, int Exponent>
      fixed_point<ReprType, Exponent> & operator/=(
        fixed_point<ReprType, Exponent> & lhs,
        const fixed_point<ReprType, Exponent> & rhs) noexcept;
 
  template <class Lhs, class Rhs>
      constexpr auto operator==(const Lhs & lhs, const Rhs & rhs) noexcept;
  template <class Lhs, class Rhs>
      constexpr auto operator!=(const Lhs & lhs, const Rhs & rhs) noexcept;
  template <class Lhs, class Rhs>
      constexpr auto operator<(const Lhs & lhs, const Rhs & rhs) noexcept;
  template <class Lhs, class Rhs>
      constexpr auto operator>(const Lhs & lhs, const Rhs & rhs) noexcept;
  template <class Lhs, class Rhs>
      constexpr auto operator>=(const Lhs & lhs, const Rhs & rhs) noexcept;
  template <class Lhs, class Rhs>
      constexpr auto operator<=(const Lhs & lhs, const Rhs & rhs) noexcept;
 
  template <class Lhs, class Rhs>
      constexpr auto operator+(
        const Lhs & lhs,
        const Rhs & rhs) noexcept;
  template <class Lhs, class Rhs>
      constexpr auto operator-(
        const Lhs & lhs,
        const Rhs & rhs) noexcept;
  template <class LhsReprType, int LhsExponent, class RhsReprType, int RhsExponent>
      constexpr auto operator*(
        const fixed_point<LhsReprType, LhsExponent> & lhs,
        const fixed_point<RhsReprType, RhsExponent> & rhs) noexcept;
  template <class LhsReprType, int LhsExponent, class RhsReprType, int RhsExponent>
      constexpr auto operator/(
        const fixed_point<LhsReprType, LhsExponent> & lhs,
        const fixed_point<RhsReprType, RhsExponent> & rhs) noexcept;
  template <class LhsReprType, int LhsExponent, class Integer>
      constexpr auto operator*(
        const fixed_point<LhsReprType, LhsExponent> & lhs,
        const Integer & rhs) noexcept;
  template <class LhsReprType, int LhsExponent, class Integer>
      constexpr auto operator/(
        const fixed_point<LhsReprType, LhsExponent> & lhs,
        const Integer & rhs) noexcept;
  template <class Integer, class RhsReprType, int RhsExponent>
      constexpr auto operator*(
        const Integer & lhs,
        const fixed_point<RhsReprType, RhsExponent> & rhs) noexcept;
  template <class Integer, class RhsReprType, int RhsExponent>
      constexpr auto operator/(
        const Integer & lhs,
        const fixed_point<RhsReprType, RhsExponent> & rhs) noexcept;
  template <class LhsReprType, int LhsExponent, class Float>
      constexpr auto operator*(
        const fixed_point<LhsReprType, LhsExponent> & lhs,
        const Float & rhs) noexcept;
  template <class LhsReprType, int LhsExponent, class Float>
      constexpr auto operator/(
        const fixed_point<LhsReprType, LhsExponent> & lhs,
        const Float & rhs) noexcept;
  template <class Float, class RhsReprType, int RhsExponent>
      constexpr auto operator*(
        const Float & lhs,
        const fixed_point<RhsReprType, RhsExponent> & rhs) noexcept;
  template <class Float, class RhsReprType, int RhsExponent>
      constexpr auto operator/(
        const Float & lhs,
        const fixed_point<RhsReprType, RhsExponent> & rhs) noexcept;
  template <class LhsReprType, int Exponent, class Rhs>
      fixed_point<LhsReprType, Exponent> & operator+=(fixed_point<LhsReprType, Exponent> & lhs, const Rhs & rhs) noexcept;
  template <class LhsReprType, int Exponent, class Rhs>
      fixed_point<LhsReprType, Exponent> & operator-=(fixed_point<LhsReprType, Exponent> & lhs, const Rhs & rhs) noexcept;
  template <class LhsReprType, int Exponent>
  template <class Rhs, typename std::enable_if<std::is_arithmetic<Rhs>::value, int>::type Dummy>
      fixed_point<LhsReprType, Exponent> &
      fixed_point<LhsReprType, Exponent>::operator*=(const Rhs & rhs) noexcept;
  template <class LhsReprType, int Exponent>
  template <class Rhs, typename std::enable_if<std::is_arithmetic<Rhs>::value, int>::type Dummy>
      fixed_point<LhsReprType, Exponent> &
      fixed_point<LhsReprType, Exponent>::operator/=(const Rhs & rhs) noexcept;
  template <class ReprType, int Exponent>
      constexpr fixed_point<ReprType, Exponent>
        sqrt(const fixed_point<ReprType, Exponent> & x) noexcept;
  template <class FixedPoint, unsigned N = 2>
      using trunc_add_result;
 
  template <class FixedPoint, class ... Tail>
      trunc_add_result<FixedPoint, sizeof...(Tail) + 1>
      constexpr trunc_add(const FixedPoint & addend1, const Tail & ... addend_tail);
  template <class Lhs, class Rhs = Lhs>
      using trunc_subtract_result;
  template <class Lhs, class Rhs>
      trunc_subtract_result<Lhs, Rhs>
        constexpr trunc_subtract(const Lhs & minuend, const Rhs & subtrahend);
  template <class Lhs, class Rhs = Lhs>
      using trunc_multiply_result;
 
  template <class Lhs, class Rhs>
      trunc_multiply_result<Lhs, Rhs>
        constexpr trunc_multiply(const Lhs & lhs, const Rhs & rhs) noexcept;
  template <class FixedPointDividend, class FixedPointDivisor = FixedPointDividend>
      using trunc_divide_result;
  template <class FixedPointDividend, class FixedPointDivisor>
      trunc_divide_result<FixedPointDividend, FixedPointDivisor>
        constexpr trunc_divide(const FixedPointDividend & lhs, const FixedPointDivisor & rhs) noexcept;
  template <class FixedPoint>
      using trunc_reciprocal_result;
  template <class FixedPoint>
      trunc_reciprocal_result<FixedPoint>
        constexpr trunc_reciprocal(const FixedPoint & fixed_point) noexcept;
  template <class FixedPoint>
      using trunc_square_result;
 
  template <class FixedPoint>
      trunc_square_result<FixedPoint>
        constexpr trunc_square(const FixedPoint & root) noexcept;
  template <class FixedPoint>
      using trunc_sqrt_result;
  template <class FixedPoint>
      trunc_sqrt_result<FixedPoint>
        constexpr trunc_sqrt(const FixedPoint & square) noexcept;
  template <int Integer, class ReprType, int Exponent>
      constexpr fixed_point<ReprType, Exponent + Integer>
        trunc_shift_left(const fixed_point<ReprType, Exponent> & fp) noexcept;
  template <int Integer, class ReprType, int Exponent>
      constexpr fixed_point<ReprType, Exponent - Integer>
        trunc_shift_right(const fixed_point<ReprType, Exponent> & fp) noexcept;
  template <class FixedPoint, unsigned N = 2>
      using promote_add_result;
 
  template <class FixedPoint, class ... Tail>
      promote_add_result<FixedPoint, sizeof...(Tail) + 1>
        constexpr promote_add(const FixedPoint & addend1, const Tail & ... addend_tail);
  template <class Lhs, class Rhs = Lhs>
      using promote_subtract_result
  template <class Lhs, class Rhs>
      promote_subtract_result<Lhs, Rhs>
        constexpr promote_subtract(const Lhs & lhs, const Rhs & rhs) noexcept;
  template <class Lhs, class Rhs = Lhs>
      using promote_multiply_result;
  template <class Lhs, class Rhs>
      promote_multiply_result<Lhs, Rhs>
        constexpr promote_multiply(const Lhs & lhs, const Rhs & rhs) noexcept;
  template <class Lhs, class Rhs = Lhs>
      using promote_divide_result;
  template <class Lhs, class Rhs>
      promote_divide_result<Lhs, Rhs>
        constexpr promote_divide(const Lhs & lhs, const Rhs & rhs) noexcept;
  template <class FixedPoint>
      using promote_square_result;
  template <class FixedPoint>
      promote_square_result<FixedPoint>
        constexpr promote_square(const FixedPoint & root) noexcept;
}

fixed_point<> Class Template

template <class ReprType = int, int Exponent = 0>
class fixed_point
{
public:
  using repr_type = ReprType;
 
  constexpr static int exponent;
  constexpr static int digits;
  constexpr static int integer_digits;
  constexpr static int fractional_digits;
 
  fixed_point() noexcept;
  template <class S, typename std::enable_if<_impl::is_integral<S>::value, int>::type Dummy = 0>
    explicit constexpr fixed_point(S s) noexcept;
  template <class S, typename std::enable_if<std::is_floating_point<S>::value, int>::type Dummy = 0>
    explicit constexpr fixed_point(S s) noexcept;
  template <class FromReprType, int FromExponent>
    explicit constexpr fixed_point(const fixed_point<FromReprType, FromExponent> & rhs) noexcept;
  template <class S, typename std::enable_if<_impl::is_integral<S>::value, int>::type Dummy = 0>
    fixed_point & operator=(S s) noexcept;
  template <class S, typename std::enable_if<std::is_floating_point<S>::value, int>::type Dummy = 0>
    fixed_point & operator=(S s) noexcept;
  template <class FromReprType, int FromExponent>
    fixed_point & operator=(const fixed_point<FromReprType, FromExponent> & rhs) noexcept;
 
  template <class S, typename std::enable_if<_impl::is_integral<S>::value, int>::type Dummy = 0>
    explicit constexpr operator S() const noexcept;
  template <class S, typename std::enable_if<std::is_floating_point<S>::value, int>::type Dummy = 0>
    explicit constexpr operator S() const noexcept;
  explicit constexpr operator bool() const noexcept;
 
  template <class Rhs, typename std::enable_if<std::is_arithmetic<Rhs>::value, int>::type Dummy = 0>
    fixed_point &operator*=(const Rhs & rhs) noexcept;
  template <class Rhs, typename std::enable_if<std::is_arithmetic<Rhs>::value, int>::type Dummy = 0>
    fixed_point & operator/=(const Rhs & rhs) noexcept;
 
  constexpr repr_type data() const noexcept;
  static constexpr fixed_point from_data(repr_type repr) noexcept;
};

VI. Future Issues

Library Support

Because the aim is to provide an alternative to existing arithmetic types which are supported by the standard library, it is conceivable that a future proposal might specialize existing class templates and overload existing functions.

Possible candidates for overloading include the functions defined in \ and a templated specialization of numeric_limits. A new type trait, is_fixed_point, would also be useful.

While fixed_point is intended to provide drop-in replacements to existing built-ins, it may be preferable to deviate slightly from the behavior of certain standard functions. For example, overloads of functions from \ will be considerably less concise, efficient and versatile if they obey rules surrounding error cases. In particular, the guarantee of setting errno in the case of an error prevents a function from being defined as pure. This highlights a wider issue surrounding the adoption of the functional approach and compile-time computation that is beyond the scope of this document.

Alternatives to Built-in Integer Types

The reason that ReprType is restricted to built-in integer types is that a number of features require the use of a higher - or lower-capacity type. Supporting alias templates are defined to provide fixed_point with the means to invoke integer types of specific capacity and signedness at compile time.

There is no general purpose way of deducing a higher or lower-capacity type given a source type in the same manner as make_signed and make_unsigned. If there were, this might be adequate to allow alternative choices for ReprType.

Bounded Integers

The bounded::integer library [2] exemplifies the benefits of keeping track of ranges of values in arithmetic types at compile time.

To a limited extent, the trunc_ functions defined here also keep track of - and modify - the limits of values. However, a combination of techniques is capable of producing superior results.

For instance, consider the following expression:

make_ufixed<2, 6> three(3);
auto n = trunc_square(trunc_square(three));

The type of n is make_ufixed<8, 0> but its value does not exceed 81. Hence, an integer bit has been wasted. It may be possible to track more accurate limits in the same manner as the bounded::integer library in order to improve the precision of types returned by trunc_ functions. For this reason, the exact value of the exponents of these return types is not given.

Notes:

  • Bounded::integer is already supported by fixed-point library, fp [3].
  • A similar library is the boost constrained_value library [4].

Alternative Policies

The behavior of the types specialized from fixed_point represent one sub-set of all potentially desirable behaviors. Alternative characteristics include:

  • different rounding strategies - other than truncation;
  • overflow and underflow checks - possibly throwing exceptions;
  • operator return type - adopting trunc_ or promote_ behavior;
  • default-initialize to zero - currently uninitialized and
  • saturation arithmetic - as opposed to modular arithmetic.

One way to extend fixed_point to cover these alternatives would be to add non-type template parameters containing bit flags or enumerated types. The default set of values would reflect fixed_point as it stands currently.

VII. Prior Art

Many examples of fixed-point support in C and C++ exist. While almost all of them aim for low run-time cost and expressive alternatives to raw integer manipulation, they vary greatly in detail and in terms of their interface.

One especially interesting dichotomy is between solutions which offer a discrete selection of fixed-point types and libraries which contain a continuous range of exponents through type parameterization.

N1169

One example of the former is found in proposal N1169 [5], the intent of which is to expose features found in certain embedded hardware. It introduces a succinct set of language-level fixed-point types and impose constraints on the number of integer or fractional digits each can possess.

As with all examples of discrete-type fixed-point support, the limited choice of exponents is a considerable restriction on the versatility and expressiveness of the API.

Nevertheless, it may be possible to harness performance gains provided by N1169 fixed-point types through explicit template specialization. This is likely to be a valuable proposition to potential users of the library who find themselves targeting platforms which support fixed-point arithmetic at the hardware level.

N3352

There are many other C++ libraries available which fall into the latter category of continuous-range fixed-point arithmetic [3] [6] [7]. In particular, an existing library proposal, N3352 [8], aims to achieve very similar goals through similar means and warrants closer comparison than N1169.

N3352 introduces four class templates covering the quadrant of signed versus unsigned and fractional versus integer numeric types. It is intended to replace built-in types in a wide variety of situations and accordingly, is highly compile-time configurable in terms of how rounding and overflow are handled. Parameters to these four class templates include the storage in bits and - for fractional types - the resolution.

The fixed_point class template could probably - with a few caveats - be generated using the two fractional types, nonnegative and negatable, replacing the ReprType parameter with the integer bit count of ReprType, specifying fastest for the rounding mode and specifying undefined as the overflow mode.

However, fixed_point more closely and concisely caters to the needs of users who already use integer types and simply desire a more concise, less error-prone form. It more closely follows the four design aims of the library and - it can be argued - more closely follows the spirit of the standard in its pursuit of zero-cost abstraction.

Some aspects of the design of the N3352 API which back up these conclusion are that:

  • the result of arithmetic operations closely resemble the trunc_ function templates and are potentially more costly at run-time;
  • the nature of the range-specifying template parameters - through careful framing in mathematical terms - abstracts away valuable information regarding machine-critical type size information;
  • the breaking up of duties amongst four separate class templates introduces four new concepts and incurs additional mental load for relatively little gain while further detaching the interface from vital machine-level details and
  • the absence of the most negative number from signed types reduces the capacity of all types by one.

The added versatility that the N3352 API provides regarding rounding and overflow handling are of relatively low priority to users who already bear the scars of battles with raw integer types. Nevertheless, providing them as options to be turned on or off at compile time is an ideal way to leave the choice in the hands of the user.

Many high-performance applications - in which fixed-point is of potential value - favor run-time checks during development which are subsequently deactivated in production builds. The N3352 interface is highly conducive to this style of development. It is an aim of the fixed_point design to be similarly extensible in future revisions.

VIII. Acknowledgements

Subgroup: Guy Davidson, Michael Wong
Contributors: Ed Ainsley, Billy Baker, Lance Dyson, Marco Foco, Clément Grégoire, Nicolas Guillemot, Matt Kinzelman, Joël Lamotte, Sean Middleditch, Patrice Roy, Peter Schregle, Ryhor Spivak

IX. References

  1. Why Integer Coordinates?, http://www.pathengine.com/Contents/Overview/FundamentalConcepts/WhyIntegerCoordinates/page.php
  2. C++ bounded::integer library, http://doublewise.net/c++/bounded/
  3. fp, C++14 Fixed Point Library, https://github.com/mizvekov/fp
  4. Boost Constrained Value Libarary, http://rk.hekko.pl/constrained_value/
  5. N1169, Extensions to support embedded processors, http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1169.pdf
  6. fpmath, Fixed Point Math Library, http://www.codeproject.com/Articles/37636/Fixed-Point-Class
  7. Boost fixed_point (proposed), Fixed point integral and fractional types, https://github.com/viboes/fixed_point
  8. N3352, C++ Binary Fixed-Point Arithmetic, http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3352.html
  9. fixed_point, Reference Implementation of P0037, https://github.com/johnmcfarlane/fixed_point

X. Appendix 1: Reference Implementation

An in-development implementation of the fixed_point class template and its essential supporting functions and types is available [9]. It includes a utility header containing such things as math and trigonometric functions and a partial numeric_limits specialization. Compile-time and run-time tests are included as well as benchmarking support. It is the source of examples and measurements cited here.

XI. Appendix 2: Performance

Despite a focus on usable interface and direct translation from integer-based fixed-point operations, there is an overwhelming expectation that the source code result in minimal instructions and clock cycles. A few preliminary numbers are presented to give a very early idea of how the API might perform.

Some notes:

  • A few test functions were run, ranging from single arithmetic operations to basic geometric functions, performed against integer, floating-point and fixed-point types for comparison.
  • Figures were taken from a single CPU, OS and compiler, namely:

    Debian clang version 3.5.0-10 (tags/RELEASE_350/final) (based on LLVM 3.5.0)
    Target: x86_64-pc-linux-gnu
    Thread model: posix
  • Fixed inputs were provided to each function, meaning that branch prediction rarely fails. Results may also not represent the full range of inputs.

  • Details of the test harness used can be found in the source project mentioned in Appendix 1;
  • Times are in nanoseconds;
  • Code has not yet been optimized for performance.

Types

Where applicable various combinations of integer, floating-point and fixed-point types were tested with the following identifiers:

  • uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, uint64_t and int64_t built-in integer types;
  • float, double and long double built-in floating-point types;
  • s3:4, u4:4, s7:8, u8:8, s15:16, u16:16, s31:32 and u32:32 format fixed-point types.

Basic Arithmetic

Plus, minus, multiplication and division were tested in isolation using a number of different numeric types with the following results:

name cpu_time
add(float) 1.78011
add(double) 1.73966
add(long double) 3.46011
add(u4_4) 1.87726
add(s3_4) 1.85051
add(u8_8) 1.85417
add(s7_8) 1.82057
add(u16_16) 1.94194
add(s15_16) 1.93463
add(u32_32) 1.94674
add(s31_32) 1.94446
add(int8_t) 2.14857
add(uint8_t) 2.12571
add(int16_t) 1.9936
add(uint16_t) 1.88229
add(int32_t) 1.82126
add(uint32_t) 1.76
add(int64_t) 1.76
add(uint64_t) 1.83223
sub(float) 1.96617
sub(double) 1.98491
sub(long double) 3.55474
sub(u4_4) 1.77006
sub(s3_4) 1.72983
sub(u8_8) 1.72983
sub(s7_8) 1.72983
sub(u16_16) 1.73966
sub(s15_16) 1.85051
sub(u32_32) 1.88229
sub(s31_32) 1.87063
sub(int8_t) 1.76
sub(uint8_t) 1.74994
sub(int16_t) 1.82126
sub(uint16_t) 1.83794
sub(int32_t) 1.89074
sub(uint32_t) 1.85417
sub(int64_t) 1.83703
sub(uint64_t) 2.04914
mul(float) 1.9376
mul(double) 1.93097
mul(long double) 102.446
mul(u4_4) 2.46583
mul(s3_4) 2.09189
mul(u8_8) 2.08
mul(s7_8) 2.18697
mul(u16_16) 2.12571
mul(s15_16) 2.10789
mul(u32_32) 2.10789
mul(s31_32) 2.10789
mul(int8_t) 1.76
mul(uint8_t) 1.78011
mul(int16_t) 1.8432
mul(uint16_t) 1.76914
mul(int32_t) 1.78011
mul(uint32_t) 2.19086
mul(int64_t) 1.7696
mul(uint64_t) 1.79017
div(float) 5.12
div(double) 7.64343
div(long double) 8.304
div(u4_4) 3.82171
div(s3_4) 3.82171
div(u8_8) 3.84
div(s7_8) 3.8
div(u16_16) 9.152
div(s15_16) 11.232
div(u32_32) 30.8434
div(s31_32) 34
div(int8_t) 3.82171
div(uint8_t) 3.82171
div(int16_t) 3.8
div(uint16_t) 3.82171
div(int32_t) 3.82171
div(uint32_t) 3.81806
div(int64_t) 10.2286
div(uint64_t) 8.304

Among the slowest types are long double. It is likely that they are emulated in software. The next slowest operations are fixed-point multiply and divide operations - especially with 64-bit types. This is because values need to be promoted temporarily to double-width types. This is a known fixed-point technique which inevitably experiences slowdown where a 128-bit type is required on a 64-bit system.

Here is a section of the disassembly of the s15:16 multiply call:

30:   mov    %r14,%rax  
      mov    %r15,%rax  
      movslq -0x28(%rbp),%rax  
      movslq -0x30(%rbp),%rcx  
      imul   %rax,%rcx  
      shr    $0x10,%rcx  
      mov    %ecx,-0x38(%rbp)  
      mov    %r12,%rax  
4c:   movzbl (%rbx),%eax  
      cmp    $0x1,%eax  
    ↓ jne    68  
54:   mov    0x8(%rbx),%rax  
      lea    0x1(%rax),%rcx  
      mov    %rcx,0x8(%rbx)  
      cmp    0x38(%rbx),%rax  
    ↑ jb     30

The two 32-bit numbers are multiplied together and the result shifted down - much as it would if raw int values were used. The efficiency of this operation varies with the exponent. An exponent of zero should mean no shift at all.

3-Dimensional Magnitude Squared

A fast sqrt implementation has not yet been tested with fixed_point. (The naive implementation takes over 300ns.) For this reason, a magnitude-squared function is measured, combining multiply and add operations:

template <typename FP>
constexpr FP magnitude_squared(const FP & x, const FP & y, const FP & z)
{
    return x * x + y * y + z * z;
}

Only real number formats are tested:

float 2.42606
double 2.08
long double 4.5056
s3_4 2.768
s7_8 2.77577
s15_16 2.752
s31_32 4.10331

Again, the size of the type seems to have the largest impact.

Circle Intersection

A similar operation includes a comparison and branch:

template <typename Real>
bool circle_intersect_generic(Real x1, Real y1, Real r1, Real x2, Real y2, Real r2)
{
    auto x_diff = x2 - x1;
    auto y_diff = y2 - y1;
    auto distance_squared = x_diff * x_diff + y_diff * y_diff;
 
    auto touch_distance = r1 + r2;
    auto touch_distance_squared = touch_distance * touch_distance;
 
    return distance_squared <= touch_distance_squared;
}

float 3.46011
double 3.48
long double 6.4
s3_4 3.88
s7_8 4.5312
s15_16 3.82171
s31_32 5.92

Again, fixed-point and native performance are comparable.

================================================ FILE: Docs/Proposals/rawstorage.html ================================================ Extending raw_storage_iterator LEWG, SG14: D0039R0
11-9-2015
Brent Friedman
fourthgeek@gmail.com

Extending raw_storage_iterator

I. Motivation

Management of uninitialized memory is an important topic for those implementing containers, allocators, and similar library facilities. This paper seeks to modernize raw storage iterator, bringing important missing features to this utility class.

II. Summary

Move construction

raw_storage_iterator lacks support for move construction of elements. Currently users will be faced with the surprising behavior of copy construction in all circumstances.

*it = std::move(x); //copy constructs using x

Factory function

raw_storage_iterator requires two template parameters which make its usage fairly verbose. We propose a factory function similar to make_shared for improving readability and making its use less error prone.

Placement new support

The primary use of raw_storage_iterator is to serve as a helper for constructing objects in place. Despite this, it does not support placement new syntax. Support for placement new into a raw storage iterator makes this iterator useful in new contexts.

III. Discussion

Comments at Lenexa stated that raw_storage_iterator is obscure and underused. Fixing these holes should at least open room for this class to be utilized more frequently and to exhibit expected behavior.

No facilities are provided for conditional move, as with move_if_noexcept. The structure of this class would require an understanding of move_if_noexcept to be built-in to the type system and so seems to have no good avenue for pursuit. Users of raw_storage_iterator should use move_if_noexcept at the callsite as they would with any other iterator:
*it = move_if_noexcept(v);

IV. Proposed Text

Make the following changes in [storage.iterator]:


  template<class T>
  auto make_storage_iterator( T&& iterator)
  {
    return raw_storage_iterator<std::remove_reference<T>::type, decltype(*iterator)>( std::forward<T>(iterator));
  }

  template<class T, class U>
  void* operator new(size_t s, raw_storage_iterator<T,U> it) noexcept
  {
    return ::operator new(s, it.base() );
  }

  template<class T, class U>
  void operator delete ( void* m, raw_storage_iterator<T,U> it) noexcept
  {
    return ::operator delete(m, it.base() );
  }

Add to raw_storage_iterator:

  raw_storage_iterator& operator=(T&& element);
Effects: Move-constructs a value from element at the location to which the iterator points.
Returns: A reference to the iterator.

Amend operator=(const T& element) as follows:

Effects: Copy-constructs a value from element ...

================================================ FILE: Docs/Proposals/ring_proposal_r5.tex ================================================ Document number: P0059R4 Date: 2017-05-15 Reply-to: Guy Davidson, guy@hatcat.com Reply-to: Arthur O’Dwyer, arthur.j.odwyer@gmail.com Audience: Library Evolution (LEWG), Game dev and low latency (SG14) A proposal to add a ring span to the standard library 0. Contents Introduction Motivation Impact on the standard Design decisions Header synopsis 5.1 Function specifications: *_popper 5.2 Function specifications: ring_span Sample use Future work Acknowledgements 1. Introduction This proposal introduces a ring to the standard library operating on a span, named ring_span. The ring_span offers similar facilities to std::queue with the additional feature of storing the elements in contiguous memory and being of a fixed size. It is an update to P0059R2 to withdraw the addition of a concurrent ring span, at the request of SG1, and to remove iterator semantics, which are considered inappropriate for containers of unowned objects, at the request of LEWG. The authors seek feedback on the design of the ring before submitting wording for the standard. 2. Motivation Queues are widely used containers for collecting data prior to processing in order of entry to the queue (first in, first out). The std::queue container adaptor acts as a wrapper to an underlying container, typically std::deque or std::list. These containers are not of a fixed size and may grow as they fill, which means that each item that is added to a std::queue may prompt an allocation, which will lead to memory fragmentation. The ring_span operates on elements in contiguous non-owned memory, so memory allocation is eliminated. The most common uses for the ring_span would be: Storing the last n events for later recovery Communicating between threads in an allocation-constrained environment Both of these use cases demand a single producer and a single consumer of elements. 3. Impact on the standard This proposal is a pure library extension. It does not require changes to any standard classes, functions or headers. 4. Design decisions Naming In an earlier version of this paper the name ring_buffer was proposed, but given the new implementation the proposed name is ring_span. The name ring still remains the preferred choice of the authors. Look like std::queue There is already an object that offers FIFO support: the std::queue container. The queue grows to accommodate new entries, allocating new memory as necessary. The ring interface can therefore be similar to that of the queue with the addition of try_push, try_emplace and try_pop functions: these must now fail if they are called when the ring is full (or empty in the case of try_pop), and should therefore signal that condition by returning a success/fail value. push_back and pop_front Pushing items is a simple matter of assigning to a pre-existing element. The user can decide what to do on filling up the ring: for the synchronous ring it is possible to overwrite unpopped items, which would be desirable for the use case of storing the last n events for later recovery. Popping items is a more complicated matter than in other containers. If an item is popped from a std::queue it is destroyed and the memory is released. In the case of a ring_span however, it does not own the memory so a different strategy must be pursued. There are four things that could happen when an object is popped from a ring_span, besides the usual container housekeeping: The object is destroyed via the class destructor and the memory is left in an undefined state. The object is replaced with a default-constructed object. The object is replaced with a copy of a user-specified object. The object is not replaced at all. This is a choice that will depend on the type being contained. For example, if the type is not default-constructible, option 2 is unavailable. If the type is not assignable, options 2 and 3 are unavailable. There is no single solution that covers all these situations, so as part of the definition of ring_span a number of pop strategy objects are defined. A strategy can be chosen at the point of declaration of an instance of a ring_span as a template parameter. Although pop could theoretically safely be called on an empty ring with the implementation supplied below, it should yield undefined behaviour. 5. Header synopsis This section contains the header declarations. Example definitions are also provided for clarity and to aid specification of the definitions. namespace std::experimental { template struct null_popper { void operator()(T&) const noexcept; }; template struct default_popper { T operator()(T& t) const; }; template struct copy_popper { explicit copy_popper(T&& t); T operator()(T& t) const; T copy; }; template> class ring_span { public: using type = ring_span; using size_type = std::size_t; using value_type = T; using pointer = T*; using reference = T&; using const_reference = const T&; template ring_span(ContiguousIterator begin, ContiguousIterator end, Popper p = Popper()) noexcept; template ring_span(ContiguousIterator begin, ContiguousIterator end, ContiguousIterator first, size_type size, Popper p = Popper()) noexcept; ring_span(ring_span&&) = default; ring_span& operator=(ring_span&&) = default; bool empty() const noexcept; bool full() const noexcept; size_type size() const noexcept; size_type capacity() const noexcept; reference front() noexcept; const_reference front() const noexcept; reference back() noexcept; const_reference back() const noexcept; template::value>> void push_back(const value_type& from_value) noexcept(std::is_nothrow_copy_assignable::value); template::value>> void push_back(value_type&& from_value) noexcept(std::is_nothrow_move_assignable::value); template void emplace_back(FromType&&... from_value) noexcept(std::is_nothrow_constructible::value && std::is_nothrow_move_assignable::value); T pop_front(); void swap(type& rhs) noexcept (std::is_nothrow_swappable::value); // Example implementation private: reference at(size_type idx) noexcept; const_reference at(size_type idx) const noexcept; size_type back_idx() const noexcept; void increase_size() noexcept; T* m_data; size_type m_size; size_type m_capacity; size_type m_front_idx; Popper m_popper; }; 5.1. Function specifications: *_popper The null_popper object does nothing to the item being popped from the ring. template void null_popper::operator()(T&) const noexcept {}; The default_popper object moves the item being popped from the ring into the return value. template T default_popper::operator()(T& t) const { return std:move(t); } The copy_popper object replaces the item being popped from the ring with a copy of an item of the contained type, chosen at the declaration site. template explicit copy_popper::copy_popper(T&& t) : copy(std::move(t)) {} template T copy_popper::operator()(T& t) const { T old = std::move(t); t = copy; return old; } 5.2 Function specifications: ring_span The first constructor takes a range delimited by two contiguous iterators and an instance of a popper. After this constructor is executed, the capacity of the ring is the distance between the two iterators and the size of the ring is its capacity. A typical implementation would be template template ring_span::ring_span(ContiguousIterator begin, ContiguousIterator end, Popper p) noexcept : m_data(&*begin) , m_size(0) , m_capacity(end - begin) , m_front_idx(0) , m_popper(std::move(p)) {} The second constructor creates a partially full ring. It takes a range delimited by two contiguous iterators, a third iterator which points to the oldest item of the ring, a size parameter which indicates how many items are in the ring, and an instance of a popper. After this constructor is executed, the capacity of the ring is the distance between the first two iterators and the size of the ring is the size parameter. A typical implementation would be template template ring_span::ring_span(ContiguousIterator begin, ContiguousIterator end, ContiguousIterator first, size_type size, Popper p = Popper()) noexcept : m_data(&*begin) , m_size(size) , m_capacity(end - begin) , m_front_idx(first - begin) , m_popper(std::move(p)) {} empty(), full(), size() and capacity() behave as expected. Typical implementations would be: template bool ring_span::empty() const noexcept { return m_size == 0; } template bool ring_span::full() const noexcept { return m_size == m_capacity; } template ring_span::size_type ring_span::size() const noexcept { return m_size; } template ring_span::size_type ring_span::capacity() const noexcept { return m_capacity; } front() and back() return the oldest and newest items in the ring. Typical implementations would be: template ring_span::reference ring_span::front() noexcept { return *begin(); } template ring_span::reference ring_span::back() noexcept { return *(--end()); } template ring_span::const_reference ring_span::front() const noexcept { return *begin(); } template ring_span::const_reference ring_span::back() const noexcept { return *(--end()); } The push_back() functions add an item after the most recently added item. The emplace_back() function creates an item after the most recently added item. If the size of the ring equals the capacity of the ring, then the oldest item is replaced. Otherwise, the size of the ring is increased by one. Typical implementations would be: template template::value>> void ring_span::push_back(const T& value) noexcept(std::is_nothrow_copy_assignable::value) { m_data[back_idx()] = value; increase_size(); } template template::value>> void ring_span::push_back(T&& value) noexcept(std::is_nothrow_move_assignable::value) { m_data[back_idx()] = std::move(value); increase_size(); } template template void ring_span::emplace_back(FromType&&... from_value) noexcept(std::is_nothrow_constructible::value && std::is_nothrow_move_assignable::value); { m_data[back_idx()] = T(std::forward(from_value)...); increase_size(); } The pop_front() function checks the size of the ring, asserting if it is zero. If it is non-zero, it passes a reference to the oldest item to the Popper for transformation, reduces the size and advances the front of the ring. By returning the item from pop, we are able to contain smart pointers. A typical implementation might be: template auto ring_span::pop_front() { assert(m_size != 0); auto old_front_idx = m_front_idx; m_front_idx = (m_front_idx + 1) % m_capacity; --m_size; return m_popper(m_data[old_front_idx]); } The swap() function is trivial. A typical implementation might be: template void ring_span::swap(ring_span& rhs) noexcept(std::__is_nothrow_swappable::value) { using std::swap; swap(m_data, rhs.m_data); swap(m_size, rhs.m_size); swap(m_capacity, rhs.m_capacity); swap(m_front_idx, rhs.m_front_idx); swap(m_popper, rhs.m_popper); } For the sake of clarity, the private implementation used to describe these functions is as follows: template ring_span::reference ring_span::at(size_type i) noexcept { return m_data[i % m_capacity]; } template ring_span::const_reference ring_span::at(size_type i) const noexcept { return m_data[i % m_capacity]; } template ring_span::size_type ring_span::back_idx() const noexcept { return (m_front_idx + m_size) % m_capacity; } template void ring_span::increase_size() noexcept { if (++m_size > m_capacity) { m_size = m_capacity; } } 6. Sample use #include #include using std::experimental::ring_span; void ring_test() { std::array A; std::array B; ring_span Q(std::begin(A), std::end(A)); Q.push_back(7); Q.push_back(3); assert(Q.size() == 2); assert(Q.front() == 7); Q.pop_front(); assert(Q.size() == 1); Q.push_back(18); auto Q3 = std::move(Q); assert(Q3.front() == 3); assert(Q3.back() == 18); sg14::ring_span Q5(std::move(Q3)); assert(Q5.front() == 3); assert(Q5.back() == 18); assert(Q5.size() == 2); Q5.pop_front(); Q5.pop_front(); assert(Q5.empty()); sg14::ring_span Q6(std::begin(B), std::end(B)); Q6.push_back(6); Q6.push_back(7); Q6.push_back(8); Q6.push_back(9); Q6.emplace_back(10); Q6.swap(Q5); assert(Q6.empty()); assert(Q5.size() == 5); assert(Q5.front() == 6); assert(Q5.back() == 10); puts("Ring test completed.\n"); } 7. Future work n3353 describes a proposal for a concurrent queue. The interface is quite different from ring. A concurrent ring could be adapted from the interface specified therein should n3353 be accepted into the standard. Considerable demand has been expressed for such an entity by the embedded development community, but at the presentation of revision 2 of this paper to SG1 such a feature was turned down since it overlapped with n3353. Feedback from developers in the embedded community suggests that a concurrent queue would not be used in their domain because of the contingent unpredictable memory allocation, and a fixed size container such as this would be preferable, even one with the size as a template parameter. The popper class templates are defined at an overly broad scope, rather than in the scope of the ring_span. However, no way of doing this is immediately apparent, beyond the obvious solution of creating a ring namespace and defining the poppers and the span inside it. Since this is somewhat counterintuitive in the context of the remainder of the standard library, the authors remain open to suggestions. If the popper class templates might have use in other container spans, then they could remain in the broader scope. Requests have been made for a mechanism of providing notification when an item has been pushed. This could be achieved by creating a pusher policy, analogous to the popper objects. If this is deemed valuable then this proposal can be modified accordingly. 8. Acknowledgements Thanks to Jonathan Wakely for sprucing up the first draft of the ring interface. Thanks to the SG14 forum contributors: Nicolas Guillemot, John McFarlane, Scott Wardle, Chris Gascoyne, Matt Newport. Thanks to the SG14 meeting contributors: Charles Beattie, Brittany Friedman, Billy Baker, Bob, Sean Middleditch, Ville Voutilainen. Thanks to Michael McLaughlin for commentary on the draft of the text. Thanks to Lawrence Crowl for pointing me to his paper on concurrent queues, n3353. Special thanks also to Michael Wong for convening and shepherding SG14. ================================================ FILE: Docs/Proposals/uninitialized.html ================================================ Extending memory management tools LEWG, SG14: D0040R0
11-9-2015
Brent Friedman
fourthgeek@gmail.com

Extending memory management tools

I. Motivation

When implementing containers that do not rely on standard allocators it is often necessary to manage memory directly. This paper seeks to fill gaps in the standard library's memory management utilities.

II. Summary

The function template destroy calls the destructor for specified elements.
The function template uninitialized_move performs move construction of elements over a range of memory, similar to uninitialized_copy. uninitialized_move_n is also provided.
The function template uninitialized_value_construct performs value-construction of objects over a range of memory.
The function template uninitialized_default_construct performs default-construction of objects over a range of memory.

III. Discussion

Interface changes proposed in the "range" proposals should be mirrored if both are accepted.

destroy first appeared in SGI's Standard Template Library. It is not known by the author why this algorithm was not inherited into the C++ Standard Library in its initial stages. Several responses have preferred that the algorithm be called destruct, however, destroy maintains convention with SGI and appears to be considered more appropriate use of English.

It is not possible to implement the "no effects" policy for destroy so it is specifically excluded from that rule.

The names uninitialized_value_construct and uninitialized_default_construct explicitly reflect their effects but do not clearly match terminology in other standard library functions. Proposal N3939 has chosen the _noinit suffix to denote value vs. default construction. If LEWG prefers this direction then the algorithms could be renamed to uninitialized_construct and uninitialized_construct_noinit.

Some concern is raised about exception handling with respect to uninitialized_move. If a move-constructor throws, moved-from objects may be left in a poorly defined state. Given that algorithm move has no special support for this case, it is believed that throwing constructors for this algorithm can be treated similarly. It is believed that the "no effects" wording of this section is sufficient as is.
An additional algorithm, uninitialized_move_if_noexcept, could be considered as a resolution to this concern. Given that there is currently no range-based move_if_noexcept algorithm, such a solution is not considered here. It is however trivial to implement such an algorithm -- simply forwarding to copy or move as appropriate. The same would hold true for uninitialized algorithms.

IV. Proposed Text

Make the following changes in [specialized.algorithm]:

Modify: In the algorithms uninitialized_copy and uninitialized_move, the template parameter InputIterator is required...

Modify: In the following algorithms other than destroy, if an exception is thrown there are no effects.

Add:

	template<class ForwardIterator>
	void destroy(ForwardIterator begin, ForwardIterator end);
	
	Effects:
		typedef typename iterator_traits<ForwardIterator>::value_type __t;
		while (begin != end)
		{
			begin->~__t();
			++begin;
		}

		
	template <class InputIterator, class ForwardIterator>
	ForwardIterator uninitialized_move(InputIterator first, InputIterator last, ForwardIterator result);

	Effects:
		for (; first != last; ++result, ++first)
			::new (static_cast<void*>(addressof(*result)))
				typename iterator_traits<ForwardIterator>::value_type(std::move(*first));
		return result;
		
	template <class InputIterator, class ForwardIterator>
	ForwardIterator uninitialized_move_n(InputIterator first, size_t count, ForwardIterator result);	
	
	Effects:
		for ( ; count>0; ++result, ++first, --count)
			::new (static_cast<void*>(addressof(*result)))
				typename iterator_traits<ForwardIterator>::value_type(std::move(*first));
		return result;
	
	template<class ForwardIterator>
	FwdIt uninitialized_value_construct(ForwardIterator first, ForwardIterator last);
	
	Effects:
		for (; first != last; ++first)
			::new (static_cast<void*>(addressof(*first)))
				typename iterator_traits<ForwardIterator>::value_type();
		return first;
	
	template<class ForwardIterator>
	FwdIt uninitialized_default_construct(ForwardIterator first, ForwardIterator last);
	
	Effects:
		for (; first != last; ++first)
			::new (static_cast<void*>(addressof(*first)))
				typename iterator_traits<ForwardIterator>::value_type;
		return first;
	
================================================ FILE: Docs/Proposals/unstable_remove.html ================================================ LEWG, SG14: D0041R0
11-9-2015
Brent Friedman
fourthgeek@gmail.com

Unstable remove algorithms

I. Summary

This proposal covers new algorithms for removing elements from a range without the stability guarantees of existing algorithms.

II. Motivation

The stability requirements of existing remove algorithms impose overhead on users, especially for types which are expensive to move. For cases where element order need not be preserved, an unstable algorithm can prove beneficial for efficiency. unstable_remove has complexity proportional to the number of elements to be removed, whereas stable removal has complexity proportional to the number of elements that need to be moved into the "holes".

The following URL demonstrates generated assembly for implementations of similar algorithms:
https://goo.gl/xfCxzL

It is observed that unstable_remove generates less code than remove_if and partition. In particular we may note that swapping two elements, as with partition, can be much more expensive than move-assignment.

The following URL demonstrates performance tests for these same implementations:
https://github.com/WG21-SG14/SG14

It is observed that unstable_remove_if can outperform both remove_if and partition by a measurable degree.

These examples suggest that unstable_remove algorithms can be both smaller and faster than existing solutions.

III. Additional work

Algorithmic changes proposed in the "range" proposals should be applied to these algorithms if both are accepted.

The value of unstable_remove can be applied to containers directly, implying unstable_erase* algorithms or member functions. The following pseudocode signatures are informally provided here for reference and discussion but are not proposed in this paper.


//1.
It unstable_erase(Cont& C, It at);
//2.
It unstable_erase(Cont& C, It begin, It end);
//3.
It unstable_erase_if(Cont& C, Pred p); //unstable_remove_if + erase
//4.
It unstable_erase(Cont& C, const T& value); //unstable_remove + erase

IV. Discussion

Some skepticism is levied against the utility of creating unstable variants for all erase and remove features. The cost and value of each variant may be difficult to evaluate individually, which is why this proposal covers only the most fundamental functionality of unstable_remove and unstable_remove_if. This author does believe that all removal and erasure features with stability guarantees should have variants without those stability guarantees.

Some see unstable container erasure as even more important than unstable_remove. It is in the author's opinion that unstable_remove algorithms remain independently useful in many contexts (such as fixed sized containers) and constitute more fundamental functionality than erasure.

For linked lists, the best efficiency guarantees for unstable_erase are provided by forwarding to existing, stable erase functions. It is believed that no additional wording for this case would be necessary, but some clarification may be desirable.

It is noted that for vector<int> x, and the following code samples,


x.unstable_erase( unstable_remove( x.begin(), x.end(), 0), x.end()); //A.
x.erase( unstable_remove(x.begin(), x.end(), 0), x.end()); //B.

A provides no efficiency benefits over B. unstable_erase provides benefits only when the range to be removed occurs within the middle of the container. This may lead to some confusion among users, though A and B would provide the same results.

V. Proposed Wording

In [alg.remove]
First section:
template<class ForwardIterator, class T> ForwardIterator unstable_remove(ForwardIterator first, ForwardIterator last, const T& value);
template<class ForwardIterator, class Predicate> ForwardIterator unstable_remove_if(ForwardIterator first, ForwardIterator last,Predicate pred);


Remarks (remove, remove_if): Stable

Second section:
template<class InputIterator, class OutputIterator, class T> OutputIterator unstable_remove_copy(InputIterator first, InputIterator last, OutputIterator result, const T& value);
template<class InputIterator, class OutputIterator, class Predicate> OutputIterator unstable_remove_copy_if(InputIterator first, InputIterator last, OutputIterator result, Predicate pred);


Remarks (remove_copy, remove_copy_if): Stable

================================================ FILE: Docs/fixed_point.md ================================================ # Fixed-Point Real Numbers [John McFarlane](https://groups.google.com/a/isocpp.org/forum/#!profile/sg14/APn2wQdoie4ys78eOuHNF35KKYtO4LHy0d1z8jGXJ0cr5tzmqGbDbD7BFUzrqWrU7tJiBi_UnRqo) Support for fixed-point arithmetic in the standard library is proposed. It is described in paper, [LEWG, EWG, SG14, SG6: D0037](Proposals/Fixed_Point_Library_Proposal.md). An [experimental implementation](https://github.com/johnmcfarlane/fixed_point) is in development. Potential users are invited to provide feedback on the [SG14 forum](https://groups.google.com/a/isocpp.org/forum/#!forum/sg14). ================================================ FILE: Docs/plf_licensing.txt ================================================ All plf:: modules are provided under a zlib license: This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgement in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Copyright (c) 2015 Matthew Bentley ================================================ FILE: README.md ================================================ # SG14 [![Build Status](https://travis-ci.org/WG21-SG14/SG14.svg?branch=master)](https://travis-ci.org/WG21-SG14/SG14) A library for Study Group 14 of Working Group 21 (C++) /docs - Documentation for implementations without proposals, or supplementary documentation which does not easily fit within a proposal. /docs/proposals - C++ standard proposals. /SG14 - Source files for implementations. /SG14_test - Individual tests for implementations. http://lists.isocpp.org/mailman/listinfo.cgi/sg14 for more information ## Build Instructions Clone the repo. Navigate to the folder in your favorite terminal. `mkdir build && cd build` ### Windows `cmake .. -A x64 && cmake --build . && bin\sg14_tests.exe` ### Unixes `cmake .. && cmake --build . && ./bin/sg14_tests` ### Alternatively `cd SG14_test && g++ -std=c++14 -DTEST_MAIN -I../SG14 whatever_test.cpp && ./a.out` ================================================ FILE: SG14/algorithm_ext.h ================================================ #pragma once #include #include #include #include namespace stdext { template void destruct(ForwardIterator begin, ForwardIterator end) { typedef typename std::iterator_traits::value_type _T; while (begin != end) { begin->~_T(); ++begin; } } template FwdIt uninitialized_move(SrcIt SrcBegin, Sentinel SrcEnd, FwdIt Dst) { FwdIt current = Dst; try { while (SrcBegin != SrcEnd) { ::new (static_cast(std::addressof(*current))) typename std::iterator_traits::value_type(std::move(*SrcBegin)); ++current; ++SrcBegin; } return current; } catch (...) { destruct(Dst, current); throw; } } template FwdIt uninitialized_value_construct(FwdIt first, Sentinel last) { FwdIt current = first; try { while (current != last) { ::new (static_cast(std::addressof(*current))) typename std::iterator_traits::value_type(); ++current; } return current; } catch (...) { destruct(first, current); throw; } } template FwdIt uninitialized_default_construct(FwdIt first, Sentinel last) { FwdIt current = first; try { while (current != last) { ::new (static_cast(std::addressof(*current))) typename std::iterator_traits::value_type; ++current; } return current; } catch (...) { destruct(first, current); throw; } } template BidirIt unstable_remove_if(BidirIt first, BidirIt last, UnaryPredicate p) { while (true) { // Find the first instance of "p"... while (true) { if (first == last) { return first; } if (p(*first)) { break; } ++first; } // ...and the last instance of "not p"... while (true) { --last; if (first == last) { return first; } if (!p(*last)) { break; } } // ...and move the latter over top of the former. *first = std::move(*last); ++first; } } template BidirIt unstable_remove(BidirIt first, BidirIt last, const Val& v) { while (true) { // Find the first instance of "v"... while (true) { if (first == last) { return first; } if (*first == v) { break; } ++first; } // ...and the last instance of "not v"... while (true) { --last; if (first == last) { return first; } if (!(*last == v)) { break; } } // ...and move the latter over top of the former. *first = std::move(*last); ++first; } } //this exists as a point of reference for providing a stable comparison vs unstable_remove_if template BidirIt partition(BidirIt first, BidirIt last, UnaryPredicate p) { while (true) { while ((first != last) && p(*first)) { ++first; } if (first == last) break; --last; while ((first != last) && !p(*last)) { --last; } if (first == last) break; std::iter_swap(first, last); ++first; } return first; } //this exists as a point of reference for providing a stable comparison vs unstable_remove_if template ForwardIt remove_if(ForwardIt first, ForwardIt last, UnaryPredicate p) { first = std::find_if(first, last, p); if (first != last) { for (ForwardIt i = first; ++i != last; ) { if (!p(*i)) { *first = std::move(*i); ++first; } } } return first; } } ================================================ FILE: SG14/flat_map.h ================================================ /* * Boost Software License - Version 1.0 - August 17th, 2003 * * Permission is hereby granted, free of charge, to any person or organization * obtaining a copy of the software and accompanying documentation covered by * this license (the "Software") to use, reproduce, display, distribute, * execute, and transmit the Software, and to prepare derivative works of the * Software, and to permit third-parties to whom the Software is furnished to * do so, all subject to the following: * * The copyright notices in the Software and this entire statement, including * the above license grant, this restriction and the following disclaimer, * must be included in all copies of the Software, in whole or in part, and * all derivative works of the Software, unless such copies or derivative * works are solely in the form of machine-executable object code generated by * a source language processor. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #pragma once // This is an implementation of the proposed "std::flat_map" as specified in // http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0429r6.pdf #include #include #include #include #include #include namespace stdext { namespace flatmap_detail { template struct qualifies_as_range : std::false_type {}; template struct qualifies_as_range()() ), void(), std::end( std::declval()() ), void() )> : std::true_type {}; template using is_random_access_iterator = std::is_convertible< typename std::iterator_traits::iterator_category, std::random_access_iterator_tag >; template struct priority_tag : priority_tag {}; template<> struct priority_tag<0> {}; // As proposed in P0591R4. Guaranteed copy elision makes this do the right thing. template::value && std::is_constructible::value >::type> T make_obj_using_allocator_(priority_tag<3>, const Alloc& alloc, Args&&... args) { return T(std::allocator_arg, alloc, static_cast(args)...); } template::value && std::is_constructible::value >::type> T make_obj_using_allocator_(priority_tag<2>, const Alloc& alloc, Args&&... args) { return T(static_cast(args)..., alloc); } template::value && std::is_constructible::value >::type> T make_obj_using_allocator_(priority_tag<1>, const Alloc&, Args&&... args) { return T(static_cast(args)...); } template T make_obj_using_allocator_(priority_tag<0>, const Alloc&, Args&&...) { static_assert(sizeof(T)==0, "this request for uses-allocator construction is ill-formed"); } template T make_obj_using_allocator(const Alloc& alloc, Args&&... args) { return make_obj_using_allocator_(priority_tag<3>(), alloc, static_cast(args)...); } template using cont_key_type = typename std::remove_const::type; template using cont_mapped_type = typename Container::value_type::second_type; template using iter_key_type = typename std::remove_const::value_type::first_type>::type; template using iter_mapped_type = typename std::iterator_traits::value_type::second_type; template using void_t = void; template struct qualifies_as_allocator : std::false_type {}; template struct qualifies_as_allocator().allocate(size_t{})) >> : std::true_type {}; template using qualifies_as_input_iterator = std::integral_constant::value>; template void swap_together(size_t i, size_t j, Its... its) { using std::swap; int dummy[] = { 0, (std::iter_swap(its + i, its + j), 0) ... }; (void)dummy; } template size_t partition_together(Predicate& pred, size_t left, size_t right, Head head, const Rest... rest) { while (left < right) { while (left != right && pred(*(head + left))) ++left; while (left != right && !pred(*(head + (right-1)))) --right; if (left + 1 < right) { flatmap_detail::swap_together(left, right-1, head, rest...); ++left; --right; } } return right; } template void sort_together(Compare& less, size_t left, size_t right, Head head, Rest... rest) { if (right - left >= 3) { size_t pivot_idx = left + (right - left) / 2; // Swap the pivot element all the way to the right. if (pivot_idx != right - 1) { flatmap_detail::swap_together(pivot_idx, right-1, head, rest...); } const auto& pivot_elt = *(head + (right-1)); auto less_than_pivot = [&](const auto& x) -> bool { return less(x, pivot_elt); }; size_t correct_pivot_idx = flatmap_detail::partition_together(less_than_pivot, left, right-1, head, rest...); if (correct_pivot_idx != right-1) { flatmap_detail::swap_together(correct_pivot_idx, right-1, head, rest...); } flatmap_detail::sort_together(less, left, correct_pivot_idx, head, rest...); flatmap_detail::sort_together(less, correct_pivot_idx+1, right, head, rest...); } else if (right - left == 2) { if (less(*(head + left), *(head + (left+1)))) { // nothing to do } else { flatmap_detail::swap_together(left, left+1, head, rest...); } } } template void sort_together(Compare less, Head& head, Rest&... rest) { flatmap_detail::sort_together(less, 0, head.size(), head.begin(), rest.begin()...); } template It unique_helper(It first, It last, It2 mapped, Compare& compare) { It dfirst = first; It2 dmapped = mapped; while (first != last) { It next = first; ++next; if ((next != last) && !bool(compare(*first, *next))) { // "next" is a duplicate of "first", so do not preserve "first" } else { // do preserve "first" if (first != dfirst) { *dfirst = std::move(*first); *dmapped = std::move(*mapped); } ++dfirst; ++dmapped; } first = next; ++mapped; } return dfirst; } template class iter; template iter make_iterator(K, V); template struct arrow_proxy { Reference *operator->() { return std::addressof(data_); } template friend class iter; Reference data_; }; template class iter { public: using difference_type = ptrdiff_t; using value_type = std::pair::value_type, typename std::iterator_traits::value_type>; using reference = std::pair::reference, typename std::iterator_traits::reference>; using pointer = arrow_proxy; using iterator_category = std::random_access_iterator_tag; iter() = default; iter(iter&&) = default; iter(const iter&) = default; iter& operator=(iter&&) = default; iter& operator=(const iter&) = default; ~iter() = default; // This is the iterator-to-const_iterator implicit conversion. template::value>::type, class = typename std::enable_if::value>::type> iter(const iter& other) : kit_(other.private_impl_getkey()), vit_(other.private_impl_getmapped()) {} reference operator*() const { return reference{*kit_, *vit_}; } pointer operator->() const { return arrow_proxy{reference{*kit_, *vit_}}; } iter& operator++() { ++kit_; ++vit_; return *this; } iter& operator--() { --kit_; --vit_; return *this; } iter operator++(int) { iter result(*this); ++*this; return result; } iter operator--(int) { iter result(*this); --*this; return result; } iter& operator+=(ptrdiff_t n) { kit_ += n; vit_ += n; return *this; } iter& operator-=(ptrdiff_t n) { kit_ -= n; vit_ -= n; return *this; } reference operator[](ptrdiff_t n) const { return *(*this + n); } friend iter operator+(iter it, ptrdiff_t n) { it += n; return it; } friend iter operator+(ptrdiff_t n, iter it) { it += n; return it; } friend iter operator-(iter it, ptrdiff_t n) { it -= n; return it; } friend ptrdiff_t operator-(const iter& it, const iter& jt) { return ptrdiff_t(it.kit_ - jt.kit_); } friend bool operator==(const iter& a, const iter& b) { return a.kit_ == b.kit_; } friend bool operator!=(const iter& a, const iter& b) { return !(a.kit_ == b.kit_); } friend bool operator<(const iter& a, const iter& b) { return a.kit_ < b.kit_; } friend bool operator<=(const iter& a, const iter& b) { return !(b.kit_ < a.kit_); } friend bool operator>(const iter& a, const iter& b) { return b.kit_ < a.kit_; } friend bool operator>=(const iter& a, const iter& b) { return !(a.kit_ < b.kit_); } KeyIt private_impl_getkey() const { return kit_; } MappedIt private_impl_getmapped() const { return vit_; } private: template friend iter make_iterator(K, V); explicit iter(KeyIt&& kit, MappedIt&& vit) : kit_(static_cast(kit)), vit_(static_cast(vit)) {} KeyIt kit_; MappedIt vit_; }; template iter make_iterator(K kit, V vit) { return iter(static_cast(kit), static_cast(vit)); } } // namespace flatmap_detail #ifndef STDEXT_HAS_SORTED_UNIQUE #define STDEXT_HAS_SORTED_UNIQUE struct sorted_unique_t { explicit sorted_unique_t() = default; }; #if defined(__cpp_inline_variables) inline #endif constexpr sorted_unique_t sorted_unique {}; #endif // STDEXT_HAS_SORTED_UNIQUE template< class Key, class Mapped, class Compare = std::less, class KeyContainer = std::vector, class MappedContainer = std::vector > class flat_map { static_assert(flatmap_detail::is_random_access_iterator::value, ""); static_assert(flatmap_detail::is_random_access_iterator::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(!std::is_const::value && !std::is_const::value, ""); static_assert(!std::is_const::value && !std::is_const::value, ""); static_assert(!std::is_reference::value && !std::is_reference::value, ""); static_assert(!std::is_reference::value && !std::is_reference::value, ""); static_assert(std::is_convertible()(std::declval(), std::declval())), bool>::value, ""); #if defined(__cpp_lib_is_swappable) static_assert(std::is_nothrow_swappable::value, ""); static_assert(std::is_nothrow_swappable::value, ""); #endif public: using key_type = Key; using mapped_type = Mapped; using value_type = std::pair; using key_compare = Compare; using const_key_reference = typename KeyContainer::const_reference; using mapped_reference = typename MappedContainer::reference; using const_mapped_reference = typename MappedContainer::const_reference; using reference = std::pair; using const_reference = std::pair; using size_type = size_t; // TODO: this should be KeyContainer::size_type using difference_type = ptrdiff_t; // TODO: this should be KeyContainer::difference_type using iterator = flatmap_detail::iter; using const_iterator = flatmap_detail::iter; using reverse_iterator = std::reverse_iterator; using const_reverse_iterator = std::reverse_iterator; using key_container_type = KeyContainer; using mapped_container_type = MappedContainer; class value_compare { friend class flat_map; protected: // TODO: this should be private Compare comp; // TODO: this constructor should be explicit value_compare(Compare c): comp(c) {} public: bool operator()(const_reference x, const_reference y) const { return comp(x.first, y.first); } }; struct containers { KeyContainer keys; MappedContainer values; }; // =========================================================== CONSTRUCTORS // This is all one massive overload set! flat_map() : flat_map(Compare()) {} flat_map(KeyContainer keys, MappedContainer values) : c_{static_cast(keys), static_cast(values)}, compare_() { this->sort_and_unique_impl(); } // TODO: surely this should use uses-allocator construction template::value && std::uses_allocator::value, int>::type = 0> flat_map(KeyContainer keys, MappedContainer values, const Alloc& a) : flat_map(KeyContainer(static_cast(keys), a), MappedContainer(static_cast(values), a)) {} template::value, int>::type = 0> explicit flat_map(const Container& cont) : flat_map(std::begin(cont), std::end(cont), Compare()) {} template::value, int>::type = 0> explicit flat_map(const Container& cont, const Compare& comp) : flat_map(std::begin(cont), std::end(cont), comp) {} template::value>::type, class = typename std::enable_if::value>::type, class = typename std::enable_if::value>::type> flat_map(const Container& cont, const Alloc& a) : flat_map(std::begin(cont), std::end(cont), Compare(), a) {} template::value>::type, class = typename std::enable_if::value>::type, class = typename std::enable_if::value>::type> flat_map(const Container& cont, const Compare& comp, const Alloc& a) : flat_map(std::begin(cont), std::end(cont), comp, a) {} flat_map(sorted_unique_t, KeyContainer keys, MappedContainer values) : c_{static_cast(keys), static_cast(values)}, compare_() {} template::value && std::uses_allocator::value, int>::type = 0> flat_map(sorted_unique_t s, KeyContainer keys, MappedContainer values, const Alloc& a) : flat_map(s, KeyContainer(static_cast(keys), a), MappedContainer(static_cast(values), a)) {} template::value>::type> flat_map(sorted_unique_t s, const Container& cont) : flat_map(s, std::begin(cont), std::end(cont), Compare()) {} template::value>::type> flat_map(sorted_unique_t s, const Container& cont, const Compare& comp) : flat_map(s, std::begin(cont), std::end(cont), comp) {} template::value>::type, class = typename std::enable_if::value>::type, class = typename std::enable_if::value>::type> flat_map(sorted_unique_t s, const Container& cont, const Alloc& a) : flat_map(s, std::begin(cont), std::end(cont), Compare(), a) {} template::value>::type, class = typename std::enable_if::value>::type, class = typename std::enable_if::value>::type> flat_map(sorted_unique_t s, const Container& cont, const Compare& comp, const Alloc& a) : flat_map(s, std::begin(cont), std::end(cont), comp, a) {} explicit flat_map(const Compare& comp) : c_{}, compare_(comp) {} template::value && std::uses_allocator::value, int>::type = 0> flat_map(const Compare& comp, const Alloc& a) : c_{flatmap_detail::make_obj_using_allocator(a), flatmap_detail::make_obj_using_allocator(a)}, compare_(comp) {} template::value && std::uses_allocator::value, int>::type = 0> explicit flat_map(const Alloc& a) : flat_map(Compare(), a) {} // TODO: shouldn't InputIterator be constrained to point to something with "first" and "second" members? template::value>::type> flat_map(InputIterator first, InputIterator last, const Compare& comp = Compare()) : compare_(comp) { for (; first != last; ++first) { c_.keys.insert(c_.keys.end(), first->first); // TODO: we must make this exception-safe if the container insert throws c_.values.insert(c_.values.end(), first->second); } this->sort_and_unique_impl(); } template::value>::type, class = typename std::enable_if::value>::type, class = typename std::enable_if::value>::type> flat_map(InputIterator first, InputIterator last, const Compare& comp, const Alloc& a) : c_{flatmap_detail::make_obj_using_allocator(a), flatmap_detail::make_obj_using_allocator(a)}, compare_(comp) { for (; first != last; ++first) { c_.keys.insert(c_.keys.end(), first->first); c_.values.insert(c_.values.end(), first->second); } this->sort_and_unique_impl(); } template::value>::type, class = typename std::enable_if::value>::type, class = typename std::enable_if::value>::type> flat_map(InputIterator first, InputIterator last, const Alloc& a) : flat_map(first, last, Compare(), a) {} template::value>::type> flat_map(sorted_unique_t, InputIterator first, InputIterator last, const Compare& comp = Compare()) : compare_(comp) { for (; first != last; ++first) { c_.keys.insert(c_.keys.end(), first->first); c_.values.insert(c_.values.end(), first->second); } } template::value>::type, class = typename std::enable_if::value>::type, class = typename std::enable_if::value>::type> flat_map(sorted_unique_t, InputIterator first, InputIterator last, const Compare& comp, const Alloc& a) : c_{flatmap_detail::make_obj_using_allocator(a), flatmap_detail::make_obj_using_allocator(a)}, compare_(comp) { for (; first != last; ++first) { c_.keys.insert(c_.keys.end(), first->first); c_.values.insert(c_.values.end(), first->second); } } template::value>::type, class = typename std::enable_if::value>::type, class = typename std::enable_if::value>::type> flat_map(sorted_unique_t s, InputIterator first, InputIterator last, const Alloc& a) : flat_map(s, first, last, Compare(), a) {} // TODO: should this be conditionally noexcept? // TODO: surely this should use uses-allocator construction template::value && std::uses_allocator::value, int>::type = 0> flat_map(flat_map&& m, const Alloc& a) : c_{KeyContainer(static_cast(m.c_.keys), a), MappedContainer(static_cast(m.c_.values), a)}, compare_(std::move(m.compare_)) {} // TODO: surely this should use uses-allocator construction template::value && std::uses_allocator::value, int>::type = 0> flat_map(const flat_map& m, const Alloc& a) : c_{KeyContainer(m.c_.keys, a), MappedContainer(m.c_.values, a)}, compare_{m.compare_} {} flat_map(std::initializer_list&& il, const Compare& comp = Compare()) : flat_map(il, comp) {} template::value && std::uses_allocator::value, int>::type = 0> flat_map(std::initializer_list&& il, const Compare& comp, const Alloc& a) : flat_map(il, comp, a) {} template::value && std::uses_allocator::value, int>::type = 0> flat_map(std::initializer_list&& il, const Alloc& a) : flat_map(il, Compare(), a) {} flat_map(sorted_unique_t s, std::initializer_list&& il, const Compare& comp = Compare()) : flat_map(s, il, comp) {} template::value && std::uses_allocator::value, int>::type = 0> flat_map(sorted_unique_t s, std::initializer_list&& il, const Compare& comp, const Alloc& a) : flat_map(s, il, comp, a) {} template::value && std::uses_allocator::value, int>::type = 0> flat_map(sorted_unique_t s, std::initializer_list&& il, const Alloc& a) : flat_map(s, il, Compare(), a) {} // ========================================================== OTHER MEMBERS flat_map& operator=(std::initializer_list il) { this->clear(); this->insert(il); return *this; } iterator begin() noexcept { return flatmap_detail::make_iterator(c_.keys.begin(), c_.values.begin()); } const_iterator begin() const noexcept { return flatmap_detail::make_iterator(c_.keys.begin(), c_.values.begin()); } iterator end() noexcept { return flatmap_detail::make_iterator(c_.keys.end(), c_.values.end()); } const_iterator end() const noexcept { return flatmap_detail::make_iterator(c_.keys.end(), c_.values.end()); } const_iterator cbegin() const noexcept { return flatmap_detail::make_iterator(c_.keys.begin(), c_.values.begin()); } const_iterator cend() const noexcept { return flatmap_detail::make_iterator(c_.keys.end(), c_.values.end()); } reverse_iterator rbegin() noexcept { return reverse_iterator(end()); } const_reverse_iterator rbegin() const noexcept { return const_reverse_iterator(end()); } reverse_iterator rend() noexcept { return reverse_iterator(begin()); } const_reverse_iterator rend() const noexcept { return const_reverse_iterator(begin()); } const_reverse_iterator crbegin() const noexcept { return const_reverse_iterator(begin()); } const_reverse_iterator crend() const noexcept { return const_reverse_iterator(end()); } #if __cplusplus >= 201703L [[nodiscard]] #endif bool empty() const noexcept { return c_.keys.empty(); } size_type size() const noexcept { return c_.keys.size(); } size_type max_size() const noexcept { return std::min(c_.keys.max_size(), c_.values.max_size()); } mapped_reference operator[](const Key& x) { return try_emplace(x).first->second; } mapped_reference operator[](Key&& x) { return try_emplace(static_cast(x)).first->second; } mapped_reference at(const Key& k) { auto it = this->find(k); if (it == end()) { throw std::out_of_range("flat_map::at"); } return it->second; } const_mapped_reference at(const Key& k) const { auto it = this->find(k); if (it == end()) { throw std::out_of_range("flat_map::at"); } return it->second; } template(std::declval()...), void())> std::pair emplace(Args&&... args) { std::pair t(static_cast(args)...); auto it = this->lower_bound(t.first); if (it == end() || compare_(t.first, it->first)) { auto kit = it.private_impl_getkey(); auto vit = it.private_impl_getmapped(); // TODO: we must make this exception-safe kit = c_.keys.emplace(kit, static_cast(t.first)); vit = c_.values.emplace(vit, static_cast(t.second)); auto result = flatmap_detail::make_iterator(kit, vit); return {std::move(result), true}; } else { return {it, false}; } } template iterator emplace_hint(const_iterator, Args&&... args) { return this->emplace(static_cast(args)...).first; } std::pair insert(const value_type& x) { return this->emplace(x); } std::pair insert(value_type&& x) { return this->emplace(static_cast(x)); } iterator insert(const_iterator position, const value_type& x) { return this->emplace_hint(position, x); } iterator insert(const_iterator position, value_type&& x) { return this->emplace_hint(position, static_cast(x)); } template(std::declval()))> std::pair insert(P&& x) { return this->emplace(static_cast(x)); } template(std::declval()))> iterator insert(const_iterator position, P&& x) { return this->emplace_hint(position, static_cast(x)); } template::value>::type> void insert(InputIterator first, InputIterator last) { // TODO: if we're inserting lots of elements, stick them at the end and then sort while (first != last) { this->insert(*first); ++first; } } template::value>::type> void insert(stdext::sorted_unique_t, InputIterator first, InputIterator last) { // TODO: if InputIterator is bidirectional, this loop should (go backward??) // TODO: if we're inserting lots of elements, stick them at the end and then sort auto it = begin(); while (first != last) { std::pair t(*first); it = std::partition_point(it, this->end(), [&](const auto& elt) { return bool(compare_(elt.first, t.first)); }); if (it == this->end() || bool(compare_(t.first, it->first))) { it = this->emplace(it, std::move(t)); } ++it; ++first; } } void insert(std::initializer_list il) { this->insert(il.begin(), il.end()); } void insert(stdext::sorted_unique_t s, std::initializer_list il) { this->insert(s, il.begin(), il.end()); } // TODO: as specified, this function fails to preserve the allocator, and has UB for std::pmr containers containers extract() && { try { containers result{ static_cast(c_.keys), static_cast(c_.values) }; this->clear(); return result; } catch (...) { this->clear(); throw; } } // TODO: why by rvalue reference and not by-value? void replace(KeyContainer&& keys, MappedContainer&& values) { try { c_.keys = static_cast(keys); c_.values = static_cast(values); } catch (...) { this->clear(); throw; } } template std::pair try_emplace(const Key& k, Args&&... args) { auto kit = std::lower_bound(c_.keys.begin(), c_.keys.end(), k, std::ref(compare_)); auto vit = c_.values.begin() + (kit - c_.keys.begin()); if (kit == c_.keys.end() || compare_(k, *kit)) { kit = c_.keys.insert(kit, k); // TODO: we must make this exception-safe if the container throws vit = c_.values.emplace(vit, static_cast(args)...); return {flatmap_detail::make_iterator(kit, vit), true}; } else { return {flatmap_detail::make_iterator(kit, vit), false}; } } template std::pair try_emplace(Key&& k, Args&&... args) { auto kit = std::lower_bound(c_.keys.begin(), c_.keys.end(), k, std::ref(compare_)); auto vit = c_.values.begin() + (kit - c_.keys.begin()); if (kit == c_.keys.end() || compare_(k, *kit)) { kit = c_.keys.insert(kit, static_cast(k)); // TODO: we must make this exception-safe if the container throws vit = c_.values.emplace(vit, static_cast(args)...); return {flatmap_detail::make_iterator(kit, vit), true}; } else { return {flatmap_detail::make_iterator(kit, vit), false}; } } // TODO: use the hint, here template iterator try_emplace(const_iterator, const Key& k, Args&&... args) { return try_emplace(k, static_cast(args)...).first; } // TODO: use the hint, here template iterator try_emplace(const_iterator, Key&& k, Args&&... args) { return try_emplace(static_cast(k), static_cast(args)...).first; } template std::pair insert_or_assign(const Key& k, M&& obj) { static_assert(std::is_assignable::value, ""); static_assert(sizeof( Mapped(static_cast(obj)) ) != 0, ""); auto result = try_emplace(k, static_cast(obj)); if (!result.second) { result.first->second = static_cast(obj); } return result; } template std::pair insert_or_assign(Key&& k, M&& obj) { static_assert(std::is_assignable::value, ""); static_assert(sizeof( Mapped(static_cast(obj)) ) != 0, ""); auto result = try_emplace(static_cast(k), static_cast(obj)); if (!result.second) { result.first->second = static_cast(obj); } return result; } // TODO: use the hint, here template iterator insert_or_assign(const_iterator, const Key& k, M&& obj) { static_assert(std::is_assignable::value, ""); static_assert(sizeof( Mapped(static_cast(obj)) ) != 0, ""); auto result = try_emplace(k, static_cast(obj)); if (!result.second) { result.first->second = static_cast(obj); } return result.first; } // TODO: use the hint, here template iterator insert_or_assign(const_iterator, Key&& k, M&& obj) { static_assert(std::is_assignable::value, ""); static_assert(sizeof( Mapped(static_cast(obj)) ) != 0, ""); auto result = try_emplace(static_cast(k), static_cast(obj)); if (!result.second) { result.first->second = static_cast(obj); } return result.first; } iterator erase(iterator position) { auto kit = position.private_impl_getkey(); auto vit = position.private_impl_getmapped(); // TODO: what if either of these next two lines throws an exception? auto kitmut = c_.keys.erase(kit); auto vitmut = c_.values.erase(vit); return flatmap_detail::make_iterator(kitmut, vitmut); } iterator erase(const_iterator position) { auto kit = position.private_impl_getkey(); auto vit = position.private_impl_getmapped(); // TODO: what if either of these next two lines throws an exception? auto kitmut = c_.keys.erase(kit); auto vitmut = c_.values.erase(vit); return flatmap_detail::make_iterator(kitmut, vitmut); } size_type erase(const Key& k) { auto it = this->find(k); if (it != this->end()) { this->erase(it); return 1; } return 0; } iterator erase(const_iterator first, const_iterator last) { auto kfirst = first.private_impl_getkey(); auto vfirst = first.private_impl_getmapped(); auto klast = last.private_impl_getkey(); auto vlast = last.private_impl_getmapped(); // TODO: what if either of these next two lines throws an exception? auto kitmut = c_.keys.erase(kfirst, klast); auto vitmut = c_.values.erase(vfirst, vlast); return flatmap_detail::make_iterator(kitmut, vitmut); } void swap(flat_map& fm) noexcept #if defined(__cpp_lib_is_swappable) (std::is_nothrow_swappable::value) #endif { using std::swap; swap(compare_, fm.compare_); swap(c_.keys, fm.c_.keys); swap(c_.values, fm.c_.values); } void clear() noexcept { c_.keys.clear(); c_.values.clear(); } key_compare key_comp() const { return compare_; } value_compare value_comp() const { return value_compare(compare_); } const KeyContainer& keys() const { return c_.keys; } const MappedContainer& values() const { return c_.values; } iterator find(const Key& k) { auto it = this->lower_bound(k); if (it == end() || compare_(k, it->first)) { return end(); } return it; } const_iterator find(const Key& k) const { auto it = this->lower_bound(k); if (it == end() || compare_(k, it->first)) { return end(); } return it; } template iterator find(const K& x) { auto it = this->lower_bound(x); if (it == end() || compare_(x, it->first)) { return end(); } return it; } template const_iterator find(const K& x) const { auto it = this->lower_bound(x); if (it == end() || compare_(x, it->first)) { return end(); } return it; } size_type count(const Key& k) const { return this->contains(k) ? 1 : 0; } template size_type count(const K& x) const { return this->contains(x) ? 1 : 0; } bool contains(const Key& k) const { return this->find(k) != this->end(); } template bool contains(const K& x) const { return this->find(x) != this->end(); } iterator lower_bound(const Key& k) { auto kit = std::partition_point(c_.keys.begin(), c_.keys.end(), [&](const auto& elt) { return bool(compare_(elt, k)); }); auto vit = c_.values.begin() + (kit - c_.keys.begin()); return flatmap_detail::make_iterator(kit, vit); } const_iterator lower_bound(const Key& k) const { auto kit = std::partition_point(c_.keys.begin(), c_.keys.end(), [&](const auto& elt) { return bool(compare_(elt, k)); }); auto vit = c_.values.begin() + (kit - c_.keys.begin()); return flatmap_detail::make_iterator(kit, vit); } template iterator lower_bound(const K& x) { auto kit = std::partition_point(c_.keys.begin(), c_.keys.end(), [&](const auto& elt) { return bool(compare_(elt, x)); }); auto vit = c_.values.begin() + (kit - c_.keys.begin()); return flatmap_detail::make_iterator(kit, vit); } template const_iterator lower_bound(const K& x) const { auto kit = std::partition_point(c_.keys.begin(), c_.keys.end(), [&](const auto& elt) { return bool(compare_(elt, x)); }); auto vit = c_.values.begin() + (kit - c_.keys.begin()); return flatmap_detail::make_iterator(kit, vit); } iterator upper_bound(const Key& k) { auto kit = std::partition_point(c_.keys.begin(), c_.keys.end(), [&](const auto& elt) { return !bool(compare_(k, elt)); }); auto vit = c_.values.begin() + (kit - c_.keys.begin()); return flatmap_detail::make_iterator(kit, vit); } const_iterator upper_bound(const Key& k) const { auto kit = std::partition_point(c_.keys.begin(), c_.keys.end(), [&](const auto& elt) { return !bool(compare_(k, elt)); }); auto vit = c_.values.begin() + (kit - c_.keys.begin()); return flatmap_detail::make_iterator(kit, vit); } template iterator upper_bound(const K& x) { auto kit = std::partition_point(c_.keys.begin(), c_.keys.end(), [&](const auto& elt) { return !bool(compare_(x, elt)); }); auto vit = c_.values.begin() + (kit - c_.keys.begin()); return flatmap_detail::make_iterator(kit, vit); } template const_iterator upper_bound(const K& x) const { auto kit = std::partition_point(c_.keys.begin(), c_.keys.end(), [&](const auto& elt) { return !bool(compare_(x, elt)); }); auto vit = c_.values.begin() + (kit - c_.keys.begin()); return flatmap_detail::make_iterator(kit, vit); } std::pair equal_range(const Key& k) { auto kit1 = std::partition_point(c_.keys.begin(), c_.keys.end(), [&](const auto& elt) { return bool(compare_(elt, k)); }); auto kit2 = std::partition_point(kit1, c_.keys.end(), [&](const auto& elt) { return !bool(compare_(k, elt)); }); auto vit1 = c_.values.begin() + (kit1 - c_.keys.begin()); auto vit2 = c_.values.begin() + (kit2 - c_.keys.begin()); return { flatmap_detail::make_iterator(kit1, vit1), flatmap_detail::make_iterator(kit2, vit2) }; } std::pair equal_range(const Key& k) const { auto kit1 = std::partition_point(c_.keys.begin(), c_.keys.end(), [&](const auto& elt) { return bool(compare_(elt, k)); }); auto kit2 = std::partition_point(kit1, c_.keys.end(), [&](const auto& elt) { return !bool(compare_(k, elt)); }); auto vit1 = c_.values.begin() + (kit1 - c_.keys.begin()); auto vit2 = c_.values.begin() + (kit2 - c_.keys.begin()); return { flatmap_detail::make_iterator(kit1, vit1), flatmap_detail::make_iterator(kit2, vit2) }; } template std::pair equal_range(const K& x) { auto kit1 = std::partition_point(c_.keys.begin(), c_.keys.end(), [&](const auto& elt) { return bool(compare_(elt, x)); }); auto kit2 = std::partition_point(kit1, c_.keys.end(), [&](const auto& elt) { return !bool(compare_(x, elt)); }); auto vit1 = c_.values.begin() + (kit1 - c_.keys.begin()); auto vit2 = c_.values.begin() + (kit2 - c_.keys.begin()); return { flatmap_detail::make_iterator(kit1, vit1), flatmap_detail::make_iterator(kit2, vit2) }; } template std::pair equal_range(const K& x) const { auto kit1 = std::partition_point(c_.keys.begin(), c_.keys.end(), [&](const auto& elt) { return bool(compare_(elt, x)); }); auto kit2 = std::partition_point(kit1, c_.keys.end(), [&](const auto& elt) { return !bool(compare_(x, elt)); }); auto vit1 = c_.values.begin() + (kit1 - c_.keys.begin()); auto vit2 = c_.values.begin() + (kit2 - c_.keys.begin()); return { flatmap_detail::make_iterator(kit1, vit1), flatmap_detail::make_iterator(kit2, vit2) }; } private: void sort_and_unique_impl() { flatmap_detail::sort_together(compare_, c_.keys, c_.values); auto kit = flatmap_detail::unique_helper(c_.keys.begin(), c_.keys.end(), c_.values.begin(), compare_); auto vit = c_.values.begin() + (kit - c_.keys.begin()); auto it = flatmap_detail::make_iterator(kit, vit); this->erase(it, end()); } containers c_; Compare compare_; }; // TODO: all six comparison operators should be invisible friends template bool operator==(const flat_map& x, const flat_map& y) { return std::equal(x.begin(), x.end(), y.begin(), y.end()); } template bool operator!=(const flat_map& x, const flat_map& y) { return !(x == y); } template bool operator<(const flat_map& x, const flat_map& y) { return std::lexicographical_compare(x.begin(), x.end(), y.begin(), y.end()); } template bool operator>(const flat_map& x, const flat_map& y) { return (y < x); } template bool operator<=(const flat_map& x, const flat_map& y) { return !(y < x); } template bool operator>=(const flat_map& x, const flat_map& y) { return !(x < y); } template void swap(flat_map& x, flat_map& y) noexcept(noexcept(x.swap(y))) { return x.swap(y); } #if defined(__cpp_deduction_guides) // TODO: this deduction guide should maybe be constrained by qualifies_as_range template::value>> flat_map(Container) -> flat_map, flatmap_detail::cont_mapped_type>; template::value && !flatmap_detail::qualifies_as_allocator::value>> flat_map(KeyContainer, MappedContainer) -> flat_map, KeyContainer, MappedContainer>; // TODO: all these deduction guides that ignore the Allocator parameter are wrong, but especially this one template::value && flatmap_detail::qualifies_as_allocator::value && std::uses_allocator::value>> flat_map(Container, Allocator, int=0/*to please MSVC*/) -> flat_map, flatmap_detail::cont_mapped_type>; template::value && !flatmap_detail::qualifies_as_allocator::value && flatmap_detail::qualifies_as_allocator::value && std::uses_allocator::value && std::uses_allocator::value>> flat_map(KeyContainer, MappedContainer, Allocator) -> flat_map, KeyContainer, MappedContainer>; template::value>> flat_map(sorted_unique_t, Container) -> flat_map, flatmap_detail::cont_mapped_type>; template::value && !flatmap_detail::qualifies_as_allocator::value>> flat_map(sorted_unique_t, KeyContainer, MappedContainer) -> flat_map, KeyContainer, MappedContainer>; template::value && flatmap_detail::qualifies_as_allocator::value && std::uses_allocator::value>> flat_map(sorted_unique_t, Container, Allocator, int=0/*to please MSVC*/) -> flat_map, flatmap_detail::cont_mapped_type>; template::value && !flatmap_detail::qualifies_as_allocator::value && flatmap_detail::qualifies_as_allocator::value && std::uses_allocator::value && std::uses_allocator::value>> flat_map(sorted_unique_t, KeyContainer, MappedContainer, Allocator) -> flat_map, KeyContainer, MappedContainer>; template>, class = std::enable_if_t::value && !flatmap_detail::qualifies_as_allocator::value>> flat_map(InputIterator, InputIterator, Compare = Compare()) -> flat_map, flatmap_detail::iter_mapped_type, Compare>; template::value && !flatmap_detail::qualifies_as_allocator::value && flatmap_detail::qualifies_as_allocator::value>> flat_map(InputIterator, InputIterator, Compare, Allocator) -> flat_map, flatmap_detail::iter_mapped_type, Compare>; template::value && flatmap_detail::qualifies_as_allocator::value>> flat_map(InputIterator, InputIterator, Allocator, int=0/*to please MSVC*/) -> flat_map, flatmap_detail::iter_mapped_type>; template>, class = std::enable_if_t::value && !flatmap_detail::qualifies_as_allocator::value>> flat_map(sorted_unique_t, InputIterator, InputIterator, Compare = Compare()) -> flat_map, flatmap_detail::iter_mapped_type, Compare>; template::value && !flatmap_detail::qualifies_as_allocator::value && flatmap_detail::qualifies_as_allocator::value>> flat_map(sorted_unique_t, InputIterator, InputIterator, Compare, Allocator) -> flat_map, flatmap_detail::iter_mapped_type, Compare>; template::value && flatmap_detail::qualifies_as_allocator::value>> flat_map(sorted_unique_t, InputIterator, InputIterator, Allocator, int=0/*to please MSVC*/) -> flat_map, flatmap_detail::iter_mapped_type>; template, class = std::enable_if_t::value>> flat_map(std::initializer_list>, Compare = Compare()) -> flat_map; template::value && flatmap_detail::qualifies_as_allocator::value>> flat_map(std::initializer_list>, Compare, Allocator) -> flat_map; template::value>> flat_map(std::initializer_list>, Allocator, int=0/*to please MSVC*/) -> flat_map; template, class = std::enable_if_t::value>> flat_map(sorted_unique_t, std::initializer_list>, Compare = Compare()) -> flat_map; template::value && flatmap_detail::qualifies_as_allocator::value>> flat_map(sorted_unique_t, std::initializer_list>, Compare, Allocator) -> flat_map; template::value>> flat_map(sorted_unique_t, std::initializer_list>, Allocator, int=0/*to please MSVC*/) -> flat_map; #endif } // namespace stdext ================================================ FILE: SG14/flat_set.h ================================================ /* * Boost Software License - Version 1.0 - August 17th, 2003 * * Permission is hereby granted, free of charge, to any person or organization * obtaining a copy of the software and accompanying documentation covered by * this license (the "Software") to use, reproduce, display, distribute, * execute, and transmit the Software, and to prepare derivative works of the * Software, and to permit third-parties to whom the Software is furnished to * do so, all subject to the following: * * The copyright notices in the Software and this entire statement, including * the above license grant, this restriction and the following disclaimer, * must be included in all copies of the Software, in whole or in part, and * all derivative works of the Software, unless such copies or derivative * works are solely in the form of machine-executable object code generated by * a source language processor. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #pragma once // This is an implementation of the proposed "std::flat_set" as specified in // http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1222r1.pdf #include #include #include #include #include #include namespace stdext { namespace flatset_detail { template struct qualifies_as_range : std::false_type {}; template struct qualifies_as_range()() ), void(), std::end( std::declval()() ), void() )> : std::true_type {}; template using is_random_access_iterator = std::is_convertible< typename std::iterator_traits::iterator_category, std::random_access_iterator_tag >; template struct priority_tag : priority_tag {}; template<> struct priority_tag<0> {}; // As proposed in P0591R4. Guaranteed copy elision makes this do the right thing. template::value && std::is_constructible::value >::type> T make_obj_using_allocator_(priority_tag<3>, const Alloc& alloc, Args&&... args) { return T(std::allocator_arg, alloc, static_cast(args)...); } template::value && std::is_constructible::value >::type> T make_obj_using_allocator_(priority_tag<2>, const Alloc& alloc, Args&&... args) { return T(static_cast(args)..., alloc); } template::value && std::is_constructible::value >::type> T make_obj_using_allocator_(priority_tag<1>, const Alloc&, Args&&... args) { return T(static_cast(args)...); } template T make_obj_using_allocator_(priority_tag<0>, const Alloc&, Args&&...) { static_assert(sizeof(T)==0, "this request for uses-allocator construction is ill-formed"); } template T make_obj_using_allocator(const Alloc& alloc, Args&&... args) { return make_obj_using_allocator_(priority_tag<3>(), alloc, static_cast(args)...); } template It unique_helper(It first, It last, Compare& compare) { It dfirst = first; while (first != last) { It next = first; ++next; if ((next != last) && !bool(compare(*first, *next))) { // "next" is a duplicate of "first", so do not preserve "first" } else { // do preserve "first" if (first != dfirst) { *dfirst = std::move(*first); } ++dfirst; } first = next; } return dfirst; } template using cont_value_type = typename Container::value_type; template using iter_value_type = typename std::remove_const::value_type>::type; template using void_t = void; template struct qualifies_as_allocator : std::false_type {}; template struct qualifies_as_allocator().allocate(size_t{})) >> : std::true_type {}; template using qualifies_as_input_iterator = std::integral_constant::value>; } // namespace flatset_detail #ifndef STDEXT_HAS_SORTED_UNIQUE #define STDEXT_HAS_SORTED_UNIQUE struct sorted_unique_t { explicit sorted_unique_t() = default; }; #if defined(__cpp_inline_variables) inline #endif constexpr sorted_unique_t sorted_unique {}; #endif // STDEXT_HAS_SORTED_UNIQUE template< class Key, class Compare = std::less, class KeyContainer = std::vector > class flat_set { static_assert(flatset_detail::is_random_access_iterator::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_convertible()(std::declval(), std::declval())), bool>::value, ""); public: using key_type = Key; using key_compare = Compare; using value_type = Key; using value_compare = Compare; using reference = Key&; using const_reference = const Key&; using size_type = size_t; // TODO: this should be KeyContainer::size_type using difference_type = ptrdiff_t; // TODO: this should be KeyContainer::difference_type using iterator = typename KeyContainer::iterator; using const_iterator = typename KeyContainer::const_iterator; using reverse_iterator = std::reverse_iterator; using const_reverse_iterator = std::reverse_iterator; using container_type = KeyContainer; // =========================================================== CONSTRUCTORS // This is all one massive overload set! flat_set() : flat_set(Compare()) {} explicit flat_set(KeyContainer ctr) : c_(static_cast(ctr)), compare_() { this->sort_and_unique_impl(); } template::value>::type> flat_set(KeyContainer&& ctr, const Alloc& a) : c_(flatset_detail::make_obj_using_allocator(a, static_cast(ctr))), compare_() { this->sort_and_unique_impl(); } template::value>::type> flat_set(const KeyContainer& ctr, const Alloc& a) : c_(flatset_detail::make_obj_using_allocator(a, ctr)), compare_() { this->sort_and_unique_impl(); } template::value, int>::type = 0> explicit flat_set(const Container& cont) : flat_set(std::begin(cont), std::end(cont), Compare()) {} template::value>::type> flat_set(const Container& cont, const Compare& comp) : flat_set(std::begin(cont), std::end(cont), comp) {} template::value>::type, class = typename std::enable_if::value>::type> flat_set(const Container& cont, const Alloc& a) : flat_set(std::begin(cont), std::end(cont), Compare(), a) {} template::value>::type, class = typename std::enable_if::value>::type> flat_set(const Container& cont, const Compare& comp, const Alloc& a) : flat_set(std::begin(cont), std::end(cont), comp, a) {} flat_set(sorted_unique_t, KeyContainer ctr) : c_(static_cast(ctr)), compare_() {} template::value>::type> flat_set(sorted_unique_t, KeyContainer&& ctr, const Alloc& a) : c_(flatset_detail::make_obj_using_allocator(a, static_cast(ctr))), compare_() {} template::value>::type> flat_set(sorted_unique_t, const KeyContainer& ctr, const Alloc& a) : c_(flatset_detail::make_obj_using_allocator(a, ctr)), compare_() {} template::value>::type> flat_set(sorted_unique_t s, const Container& cont) : flat_set(s, std::begin(cont), std::end(cont), Compare()) {} template::value>::type> flat_set(sorted_unique_t s, const Container& cont, const Compare& comp) : flat_set(s, std::begin(cont), std::end(cont), comp) {} template::value>::type, class = typename std::enable_if::value>::type> flat_set(sorted_unique_t s, const Container& cont, const Alloc& a) : flat_set(s, std::begin(cont), std::end(cont), Compare(), a) {} template::value>::type, class = typename std::enable_if::value>::type> flat_set(sorted_unique_t s, const Container& cont, const Compare& comp, const Alloc& a) : flat_set(s, std::begin(cont), std::end(cont), comp, a) {} explicit flat_set(const Compare& comp) : c_(), compare_(comp) {} template::value>::type> flat_set(const Compare& comp, const Alloc& a) : c_(flatset_detail::make_obj_using_allocator(a)), compare_(comp) {} template::value, int>::type = 0> explicit flat_set(const Alloc& a) : flat_set(Compare(), a) {} // TODO: all templates taking InputIterator probably need to be constrained template flat_set(InputIterator first, InputIterator last, const Compare& comp = Compare()) : c_(first, last), compare_(comp) { this->sort_and_unique_impl(); } // TODO: this constructor should conditionally use KeyContainer's iterator-pair constructor template::value>::type> flat_set(InputIterator first, InputIterator last, const Compare& comp, const Alloc& a) : c_(flatset_detail::make_obj_using_allocator(a)), compare_(comp) { while (first != last) { c_.insert(c_.end(), *first); ++first; } this->sort_and_unique_impl(); } template::value>::type> flat_set(InputIterator first, InputIterator last, const Alloc& a) : flat_set(first, last, Compare(), a) {} template flat_set(sorted_unique_t, InputIterator first, InputIterator last, const Compare& comp = Compare()) : c_(first, last), compare_(comp) {} // TODO: this constructor should conditionally use KeyContainer's iterator-pair constructor template::value>::type> flat_set(sorted_unique_t, InputIterator first, InputIterator last, const Compare& comp, const Alloc& a) : c_(flatset_detail::make_obj_using_allocator(a)), compare_(comp) { while (first != last) { c_.insert(c_.end(), *first); ++first; } } template::value>::type> flat_set(sorted_unique_t s, InputIterator first, InputIterator last, const Alloc& a) : flat_set(s, first, last, Compare(), a) {} // TODO: should this be conditionally noexcept? template::value>::type> flat_set(flat_set&& m, const Alloc& a) : c_(static_cast(m.c_), a), compare_(static_cast(m.compare_)) {} template::value>::type> flat_set(const flat_set& m, const Alloc& a) : c_(m.c_, a), compare_(m.compare_) {} flat_set(std::initializer_list&& il, const Compare& comp = Compare()) : flat_set(il, comp) {} template::value>::type> flat_set(std::initializer_list&& il, const Compare& comp, const Alloc& a) : flat_set(il, comp, a) {} template::value>::type> flat_set(std::initializer_list&& il, const Alloc& a) : flat_set(il, Compare(), a) {} flat_set(sorted_unique_t s, std::initializer_list&& il, const Compare& comp = Compare()) : flat_set(s, il, comp) {} template::value>::type> flat_set(sorted_unique_t s, std::initializer_list&& il, const Compare& comp, const Alloc& a) : flat_set(s, il, comp, a) {} template::value>::type> flat_set(sorted_unique_t s, std::initializer_list&& il, const Alloc& a) : flat_set(s, il, Compare(), a) {} // ========================================================== OTHER MEMBERS flat_set& operator=(std::initializer_list il) { this->clear(); this->insert(il); return *this; } iterator begin() noexcept { return c_.begin(); } const_iterator begin() const noexcept { return c_.begin(); } iterator end() noexcept { return c_.end(); } const_iterator end() const noexcept { return c_.end(); } const_iterator cbegin() const noexcept { return c_.begin(); } const_iterator cend() const noexcept { return c_.end(); } reverse_iterator rbegin() noexcept { return reverse_iterator(end()); } const_reverse_iterator rbegin() const noexcept { return const_reverse_iterator(end()); } reverse_iterator rend() noexcept { return reverse_iterator(begin()); } const_reverse_iterator rend() const noexcept { return const_reverse_iterator(begin()); } const_reverse_iterator crbegin() const noexcept { return const_reverse_iterator(end()); } const_reverse_iterator crend() const noexcept { return const_reverse_iterator(begin()); } #if __cplusplus >= 201703L [[nodiscard]] #endif bool empty() const noexcept { return c_.empty(); } size_type size() const noexcept { return c_.size(); } size_type max_size() const noexcept { return c_.max_size(); } template std::pair emplace(Args&&... args) { Key t(static_cast(args)...); auto it = this->lower_bound(t); if (it == end() || compare_(t, *it)) { it = c_.emplace(it, static_cast(t)); return {it, true}; } else { return {it, false}; } } // TODO: this function cannot possibly meet its amortized-constant-complexity requirement template iterator emplace_hint(const_iterator, Args&&... args) { return this->emplace(static_cast(args)...).first; } std::pair insert(const Key& t) { auto it = this->lower_bound(t); if (it == c_.end() || compare_(t, *it)) { it = c_.emplace(it, t); return {it, true}; } else { return {it, false}; } } std::pair insert(Key&& t) { auto it = this->lower_bound(t); if (it == c_.end() || compare_(t, *it)) { it = c_.emplace(it, static_cast(t)); return {it, true}; } else { return {it, false}; } } // TODO: this function cannot possibly meet its amortized-constant-complexity requirement iterator insert(const_iterator, const Key& t) { return this->insert(t).first; } // TODO: this function cannot possibly meet its amortized-constant-complexity requirement iterator insert(const_iterator, Key&& t) { return this->insert(static_cast(t)).first; } template void insert(InputIterator first, InputIterator last) { while (first != last) { this->insert(*first); ++first; } } template void insert(sorted_unique_t, InputIterator first, InputIterator last) { auto it = begin(); while (first != last) { Key t(*first); it = std::partition_point(it, end(), [&](const Key& elt) { return bool(compare_(elt, t)); }); if (it == end() || compare_(t, *it)) { it = c_.emplace(it, static_cast(t)); } ++it; ++first; } } void insert(std::initializer_list il) { this->insert(il.begin(), il.end()); } void insert(sorted_unique_t s, std::initializer_list il) { this->insert(s, il.begin(), il.end()); } KeyContainer extract() && { KeyContainer result = static_cast(c_); clear(); return result; } void replace(KeyContainer&& ctr) { c_ = static_cast(ctr); } iterator erase(iterator position) { return c_.erase(position); } iterator erase(const_iterator position) { return c_.erase(position); } size_type erase(const Key& t) { auto it = this->find(t); if (it != this->end()) { this->erase(it); return 1; } return 0; } iterator erase(const_iterator first, const_iterator last) { c_.erase(first, last); } void swap(flat_set& m) noexcept #if defined(__cpp_lib_is_swappable) (std::is_nothrow_swappable::value && std::is_nothrow_swappable::value) #endif { using std::swap; swap(compare_, m.compare_); swap(c_, m.c_); } void clear() noexcept { c_.clear(); } Compare key_comp() const { return compare_; } Compare value_comp() const { return compare_; } iterator find(const Key& t) { auto it = this->lower_bound(t); if (it == this->end() || compare_(t, *it)) { return this->end(); } return it; } const_iterator find(const Key& t) const { auto it = this->lower_bound(t); if (it == this->end() || compare_(t, *it)) { return this->end(); } return it; } template iterator find(const K& x) { auto it = this->lower_bound(x); if (it == this->end() || compare_(x, *it)) { return this->end(); } return it; } template const_iterator find(const K& x) const { auto it = this->lower_bound(x); if (it == this->end() || compare_(x, *it)) { return this->end(); } return it; } size_type count(const Key& x) const { return this->contains(x) ? 1 : 0; } template size_type count(const K& x) const { return this->contains(x) ? 1 : 0; } bool contains(const Key& x) const { return this->find(x) != this->end(); } template bool contains(const K& x) const { return this->find(x) != this->end(); } iterator lower_bound(const Key& t) { return std::partition_point(this->begin(), this->end(), [&](const Key& elt) { return bool(compare_(elt, t)); }); } const_iterator lower_bound(const Key& t) const { return std::partition_point(this->begin(), this->end(), [&](const Key& elt) { return bool(compare_(elt, t)); }); } template iterator lower_bound(const K& x) { return std::partition_point(this->begin(), this->end(), [&](const Key& elt) { return bool(compare_(elt, x)); }); } template const_iterator lower_bound(const K& x) const { return std::partition_point(this->begin(), this->end(), [&](const Key& elt) { return bool(compare_(elt, x)); }); } iterator upper_bound(const Key& t) { return std::partition_point(this->begin(), this->end(), [&](const Key& elt) { return !bool(compare_(t, elt)); }); } const_iterator upper_bound(const Key& t) const { return std::partition_point(this->begin(), this->end(), [&](const Key& elt) { return !bool(compare_(t, elt)); }); } template iterator upper_bound(const K& x) { return std::partition_point(this->begin(), this->end(), [&](const Key& elt) { return !bool(compare_(x, elt)); }); } template const_iterator upper_bound(const K& x) const { return std::partition_point(this->begin(), this->end(), [&](const Key& elt) { return !bool(compare_(x, elt)); }); } std::pair equal_range(const Key& t) { auto lo = std::partition_point(this->begin(), this->end(), [&](const Key& elt) { return bool(compare_(elt, t)); }); auto hi = std::partition_point(lo, this->end(), [&](const Key& elt) { return !bool(compare_(t, elt)); }); return { lo, hi }; } std::pair equal_range(const Key& t) const { auto lo = std::partition_point(this->begin(), this->end(), [&](const Key& elt) { return bool(compare_(elt, t)); }); auto hi = std::partition_point(lo, this->end(), [&](const Key& elt) { return !bool(compare_(t, elt)); }); return { lo, hi }; } template std::pair equal_range(const K& x) { auto lo = std::partition_point(this->begin(), this->end(), [&](const Key& elt) { return bool(compare_(elt, x)); }); auto hi = std::partition_point(lo, this->end(), [&](const Key& elt) { return !bool(compare_(x, elt)); }); return { lo, hi }; } template std::pair equal_range(const K& x) const { auto lo = std::partition_point(this->begin(), this->end(), [&](const Key& elt) { return bool(compare_(elt, x)); }); auto hi = std::partition_point(lo, this->end(), [&](const Key& elt) { return !bool(compare_(x, elt)); }); return { lo, hi }; } private: void sort_and_unique_impl() { std::sort(c_.begin(), c_.end(), compare_); auto it = flatset_detail::unique_helper(c_.begin(), c_.end(), compare_); c_.erase(it, c_.end()); } KeyContainer c_; Compare compare_; }; // TODO: all six comparison operators should be invisible friends template bool operator==(const flat_set& x, const flat_set& y) { return std::equal(x.begin(), x.end(), y.begin(), y.end()); } template bool operator!=(const flat_set& x, const flat_set& y) { return !(x == y); } template bool operator<(const flat_set& x, const flat_set& y) { return std::lexicographical_compare(x.begin(), x.end(), y.begin(), y.end()); } template bool operator>(const flat_set& x, const flat_set& y) { return (y < x); } template bool operator<=(const flat_set& x, const flat_set& y) { return !(y < x); } template bool operator>=(const flat_set& x, const flat_set& y) { return !(x < y); } template void swap(flat_set& x, flat_set& y) noexcept(noexcept(x.swap(y))) { return x.swap(y); } #if defined(__cpp_deduction_guides) // TODO: this deduction guide should maybe be constrained by qualifies_as_range template::value>> flat_set(Container) -> flat_set>; template::value && flatset_detail::qualifies_as_allocator::value && std::uses_allocator::value>> flat_set(Container, Allocator) -> flat_set>; template::value>> flat_set(sorted_unique_t, Container) -> flat_set>; template::value && flatset_detail::qualifies_as_allocator::value && std::uses_allocator::value>> flat_set(sorted_unique_t, Container, Allocator) -> flat_set>; template>, class = std::enable_if_t::value && !flatset_detail::qualifies_as_allocator::value>> flat_set(InputIterator, InputIterator, Compare = Compare()) -> flat_set, Compare>; template::value && !flatset_detail::qualifies_as_allocator::value && flatset_detail::qualifies_as_allocator::value>> flat_set(InputIterator, InputIterator, Compare, Allocator) -> flat_set, Compare>; template::value && flatset_detail::qualifies_as_allocator::value>> flat_set(InputIterator, InputIterator, Allocator, int=0/*to please MSVC*/) -> flat_set>; #endif } // namespace stdext ================================================ FILE: SG14/inplace_function.h ================================================ /* * Boost Software License - Version 1.0 - August 17th, 2003 * * Permission is hereby granted, free of charge, to any person or organization * obtaining a copy of the software and accompanying documentation covered by * this license (the "Software") to use, reproduce, display, distribute, * execute, and transmit the Software, and to prepare derivative works of the * Software, and to permit third-parties to whom the Software is furnished to * do so, all subject to the following: * * The copyright notices in the Software and this entire statement, including * the above license grant, this restriction and the following disclaimer, * must be included in all copies of the Software, in whole or in part, and * all derivative works of the Software, unless such copies or derivative * works are solely in the form of machine-executable object code generated by * a source language processor. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #pragma once #include #include #include #ifndef SG14_INPLACE_FUNCTION_THROW #define SG14_INPLACE_FUNCTION_THROW(x) throw (x) #endif namespace stdext { namespace inplace_function_detail { static constexpr size_t InplaceFunctionDefaultCapacity = 32; #ifndef SG14_USE_STD_ALIGNED_STORAGE // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=61458 // MSVC 32-bit has the same bug. // libc++ and MSVC 64-bit seem to work fine right now, but why run the risk? template union aligned_storage_helper { struct double1 { double a; }; struct double4 { double a[4]; }; template using maybe = std::conditional_t<(Cap >= sizeof(T)), T, char>; char real_data[Cap]; maybe a; maybe b; maybe c; maybe d; maybe e; maybe f; maybe g; maybe h; }; template)> struct aligned_storage { using type = std::aligned_storage_t; }; template)> using aligned_storage_t = typename aligned_storage::type; static_assert(sizeof(aligned_storage_t) == sizeof(void*), "A"); static_assert(alignof(aligned_storage_t) == alignof(void*), "B"); #else using std::aligned_storage; using std::aligned_storage_t; static_assert(sizeof(std::aligned_storage_t) == sizeof(void*), "C"); static_assert(alignof(std::aligned_storage_t) == alignof(void*), "D"); #endif template struct wrapper { using type = T; }; template struct vtable { using storage_ptr_t = void*; using invoke_ptr_t = R(*)(storage_ptr_t, Args&&...); using process_ptr_t = void(*)(storage_ptr_t, storage_ptr_t); using destructor_ptr_t = void(*)(storage_ptr_t); const invoke_ptr_t invoke_ptr; const process_ptr_t copy_ptr; const process_ptr_t relocate_ptr; const destructor_ptr_t destructor_ptr; explicit constexpr vtable() noexcept : invoke_ptr{ [](storage_ptr_t, Args&&...) -> R { SG14_INPLACE_FUNCTION_THROW(std::bad_function_call()); } }, copy_ptr{ [](storage_ptr_t, storage_ptr_t) -> void {} }, relocate_ptr{ [](storage_ptr_t, storage_ptr_t) -> void {} }, destructor_ptr{ [](storage_ptr_t) -> void {} } {} template explicit constexpr vtable(wrapper) noexcept : invoke_ptr{ [](storage_ptr_t storage_ptr, Args&&... args) -> R { return (*static_cast(storage_ptr))( static_cast(args)... ); } }, copy_ptr{ [](storage_ptr_t dst_ptr, storage_ptr_t src_ptr) -> void { ::new (dst_ptr) C{ (*static_cast(src_ptr)) }; } }, relocate_ptr{ [](storage_ptr_t dst_ptr, storage_ptr_t src_ptr) -> void { ::new (dst_ptr) C{ std::move(*static_cast(src_ptr)) }; static_cast(src_ptr)->~C(); } }, destructor_ptr{ [](storage_ptr_t src_ptr) -> void { static_cast(src_ptr)->~C(); } } {} vtable(const vtable&) = delete; vtable(vtable&&) = delete; vtable& operator= (const vtable&) = delete; vtable& operator= (vtable&&) = delete; ~vtable() = default; }; template #if __cplusplus >= 201703L inline constexpr #endif vtable empty_vtable{}; template struct is_valid_inplace_dst : std::true_type { static_assert(DstCap >= SrcCap, "Can't squeeze larger inplace_function into a smaller one" ); static_assert(DstAlign % SrcAlign == 0, "Incompatible inplace_function alignments" ); }; // C++11 MSVC compatible implementation of std::is_invocable_r. template void accept(R); template struct is_invocable_r_impl : std::false_type {}; template struct is_invocable_r_impl< decltype(std::declval()(std::declval()...), void()), void, F, Args... > : std::true_type {}; template struct is_invocable_r_impl< decltype(std::declval()(std::declval()...), void()), const void, F, Args... > : std::true_type {}; template struct is_invocable_r_impl< decltype(accept(std::declval()(std::declval()...))), R, F, Args... > : std::true_type {}; template using is_invocable_r = is_invocable_r_impl< void, R, F, Args... >; } // namespace inplace_function_detail template< class Signature, size_t Capacity = inplace_function_detail::InplaceFunctionDefaultCapacity, size_t Alignment = alignof(inplace_function_detail::aligned_storage_t) > class inplace_function; // unspecified namespace inplace_function_detail { template struct is_inplace_function : std::false_type {}; template struct is_inplace_function> : std::true_type {}; } // namespace inplace_function_detail template< class R, class... Args, size_t Capacity, size_t Alignment > class inplace_function { using storage_t = inplace_function_detail::aligned_storage_t; using vtable_t = inplace_function_detail::vtable; using vtable_ptr_t = const vtable_t*; template friend class inplace_function; public: using capacity = std::integral_constant; using alignment = std::integral_constant; inplace_function() noexcept : vtable_ptr_{std::addressof(inplace_function_detail::empty_vtable)} {} template< class T, class C = std::decay_t, class = std::enable_if_t< !inplace_function_detail::is_inplace_function::value && inplace_function_detail::is_invocable_r::value > > inplace_function(T&& closure) { static_assert(std::is_copy_constructible::value, "inplace_function cannot be constructed from non-copyable type" ); static_assert(sizeof(C) <= Capacity, "inplace_function cannot be constructed from object with this (large) size" ); static_assert(Alignment % alignof(C) == 0, "inplace_function cannot be constructed from object with this (large) alignment" ); static const vtable_t vt{inplace_function_detail::wrapper{}}; vtable_ptr_ = std::addressof(vt); ::new (std::addressof(storage_)) C{std::forward(closure)}; } template inplace_function(const inplace_function& other) : inplace_function(other.vtable_ptr_, other.vtable_ptr_->copy_ptr, std::addressof(other.storage_)) { static_assert(inplace_function_detail::is_valid_inplace_dst< Capacity, Alignment, Cap, Align >::value, "conversion not allowed"); } template inplace_function(inplace_function&& other) noexcept : inplace_function(other.vtable_ptr_, other.vtable_ptr_->relocate_ptr, std::addressof(other.storage_)) { static_assert(inplace_function_detail::is_valid_inplace_dst< Capacity, Alignment, Cap, Align >::value, "conversion not allowed"); other.vtable_ptr_ = std::addressof(inplace_function_detail::empty_vtable); } inplace_function(std::nullptr_t) noexcept : vtable_ptr_{std::addressof(inplace_function_detail::empty_vtable)} {} inplace_function(const inplace_function& other) : vtable_ptr_{other.vtable_ptr_} { vtable_ptr_->copy_ptr( std::addressof(storage_), std::addressof(other.storage_) ); } inplace_function(inplace_function&& other) noexcept : vtable_ptr_{std::exchange(other.vtable_ptr_, std::addressof(inplace_function_detail::empty_vtable))} { vtable_ptr_->relocate_ptr( std::addressof(storage_), std::addressof(other.storage_) ); } inplace_function& operator= (std::nullptr_t) noexcept { vtable_ptr_->destructor_ptr(std::addressof(storage_)); vtable_ptr_ = std::addressof(inplace_function_detail::empty_vtable); return *this; } inplace_function& operator= (inplace_function other) noexcept { vtable_ptr_->destructor_ptr(std::addressof(storage_)); vtable_ptr_ = std::exchange(other.vtable_ptr_, std::addressof(inplace_function_detail::empty_vtable)); vtable_ptr_->relocate_ptr( std::addressof(storage_), std::addressof(other.storage_) ); return *this; } ~inplace_function() { vtable_ptr_->destructor_ptr(std::addressof(storage_)); } R operator() (Args... args) const { return vtable_ptr_->invoke_ptr( std::addressof(storage_), std::forward(args)... ); } constexpr bool operator== (std::nullptr_t) const noexcept { return !operator bool(); } constexpr bool operator!= (std::nullptr_t) const noexcept { return operator bool(); } explicit constexpr operator bool() const noexcept { return vtable_ptr_ != std::addressof(inplace_function_detail::empty_vtable); } void swap(inplace_function& other) noexcept { if (this == std::addressof(other)) return; storage_t tmp; vtable_ptr_->relocate_ptr( std::addressof(tmp), std::addressof(storage_) ); other.vtable_ptr_->relocate_ptr( std::addressof(storage_), std::addressof(other.storage_) ); vtable_ptr_->relocate_ptr( std::addressof(other.storage_), std::addressof(tmp) ); std::swap(vtable_ptr_, other.vtable_ptr_); } friend void swap(inplace_function& lhs, inplace_function& rhs) noexcept { lhs.swap(rhs); } private: vtable_ptr_t vtable_ptr_; mutable storage_t storage_; inplace_function( vtable_ptr_t vtable_ptr, typename vtable_t::process_ptr_t process_ptr, typename vtable_t::storage_ptr_t storage_ptr ) : vtable_ptr_{vtable_ptr} { process_ptr(std::addressof(storage_), storage_ptr); } }; } // namespace stdext ================================================ FILE: SG14/plf_colony.h ================================================ // Copyright (c) 2021, Matthew Bentley (mattreecebentley@gmail.com) www.plflib.org // zLib license (https://www.zlib.net/zlib_license.html): // This software is provided 'as-is', without any express or implied // warranty. In no event will the authors be held liable for any damages // arising from the use of this software. // // Permission is granted to anyone to use this software for any purpose, // including commercial applications, and to alter it and redistribute it // freely, subject to the following restrictions: // // 1. The origin of this software must not be misrepresented; you must not // claim that you wrote the original software. If you use this software // in a product, an acknowledgement in the product documentation would be // appreciated but is not required. // 2. Altered source versions must be plainly marked as such, and must not be // misrepresented as being the original software. // 3. This notice may not be removed or altered from any source distribution. #ifndef PLF_COLONY_H #define PLF_COLONY_H // Compiler-specific defines: #if defined(_MSC_VER) && !defined(__clang__) && !defined(__GNUC__) #define PLF_FORCE_INLINE __forceinline #if _MSC_VER >= 1600 #define PLF_MOVE_SEMANTICS_SUPPORT #define PLF_STATIC_ASSERT(check, message) static_assert(check, message) #else #define PLF_STATIC_ASSERT(check, message) assert(check) #endif #if _MSC_VER >= 1700 #define PLF_TYPE_TRAITS_SUPPORT #define PLF_ALLOCATOR_TRAITS_SUPPORT #endif #if _MSC_VER >= 1800 #define PLF_VARIADICS_SUPPORT // Variadics, in this context, means both variadic templates and variadic macros are supported #define PLF_INITIALIZER_LIST_SUPPORT #endif #if _MSC_VER >= 1900 #define PLF_ALIGNMENT_SUPPORT #define PLF_NOEXCEPT noexcept #define PLF_IS_ALWAYS_EQUAL_SUPPORT #else #define PLF_NOEXCEPT throw() #endif #if defined(_MSVC_LANG) && (_MSVC_LANG >= 201703L) #define PLF_CONSTEXPR constexpr #else #define PLF_CONSTEXPR #endif #if defined(_MSVC_LANG) && (_MSVC_LANG > 201703L) && _MSC_VER >= 1923 #define PLF_CPP20_SUPPORT #endif #elif defined(__cplusplus) && __cplusplus >= 201103L // C++11 support, at least #define PLF_FORCE_INLINE // note: GCC and clang create faster code without forcing inline #if defined(__GNUC__) && defined(__GNUC_MINOR__) && !defined(__clang__) // If compiler is GCC/G++ #if (__GNUC__ == 4 && __GNUC_MINOR__ >= 3) || __GNUC__ > 4 // 4.2 and below do not support variadic templates #define PLF_MOVE_SEMANTICS_SUPPORT #define PLF_VARIADICS_SUPPORT #define PLF_STATIC_ASSERT(check, message) static_assert(check, message) #else #define PLF_STATIC_ASSERT(check, message) assert(check) #endif #if (__GNUC__ == 4 && __GNUC_MINOR__ >= 4) || __GNUC__ > 4 // 4.3 and below do not support initializer lists #define PLF_INITIALIZER_LIST_SUPPORT #endif #if (__GNUC__ == 4 && __GNUC_MINOR__ >= 6) || __GNUC__ > 4 #define PLF_NOEXCEPT noexcept #else #define PLF_NOEXCEPT throw() #endif #if (__GNUC__ == 4 && __GNUC_MINOR__ >= 7) || __GNUC__ > 4 #define PLF_ALLOCATOR_TRAITS_SUPPORT #endif #if (__GNUC__ == 4 && __GNUC_MINOR__ >= 8) || __GNUC__ > 4 #define PLF_ALIGNMENT_SUPPORT #endif #if __GNUC__ >= 5 // GCC v4.9 and below do not support std::is_trivially_copyable #define PLF_TYPE_TRAITS_SUPPORT #endif #if __GNUC__ > 6 #define PLF_IS_ALWAYS_EQUAL_SUPPORT #endif #elif defined(__clang__) && !defined(__GLIBCXX__) && !defined(_LIBCPP_CXX03_LANG) #if __clang_major__ >= 3 // clang versions < 3 don't support __has_feature() or traits #define PLF_ALLOCATOR_TRAITS_SUPPORT #define PLF_TYPE_TRAITS_SUPPORT #if __has_feature(cxx_alignas) && __has_feature(cxx_alignof) #define PLF_ALIGNMENT_SUPPORT #endif #if __has_feature(cxx_noexcept) #define PLF_NOEXCEPT noexcept #define PLF_IS_ALWAYS_EQUAL_SUPPORT #else #define PLF_NOEXCEPT throw() #endif #if __has_feature(cxx_rvalue_references) && !defined(_LIBCPP_HAS_NO_RVALUE_REFERENCES) #define PLF_MOVE_SEMANTICS_SUPPORT #endif #if __has_feature(cxx_static_assert) #define PLF_STATIC_ASSERT(check, message) static_assert(check, message) #else #define PLF_STATIC_ASSERT(check, message) assert(check) #endif #if __has_feature(cxx_variadic_templates) && !defined(_LIBCPP_HAS_NO_VARIADICS) #define PLF_VARIADICS_SUPPORT #endif #if (__clang_major__ == 3 && __clang_minor__ >= 1) || __clang_major__ > 3 #define PLF_INITIALIZER_LIST_SUPPORT #endif #endif #elif defined(__GLIBCXX__) // Using another compiler type with libstdc++ - we are assuming full c++11 compliance for compiler - which may not be true #if __GLIBCXX__ >= 20080606 #define PLF_MOVE_SEMANTICS_SUPPORT #define PLF_VARIADICS_SUPPORT #define PLF_STATIC_ASSERT(check, message) static_assert(check, message) #else #define PLF_STATIC_ASSERT(check, message) assert(check) #endif #if __GLIBCXX__ >= 20090421 #define PLF_INITIALIZER_LIST_SUPPORT #endif #if __GLIBCXX__ >= 20120322 #define PLF_ALLOCATOR_TRAITS_SUPPORT #define PLF_NOEXCEPT noexcept #else #define PLF_NOEXCEPT throw() #endif #if __GLIBCXX__ >= 20130322 #define PLF_ALIGNMENT_SUPPORT #endif #if __GLIBCXX__ >= 20150422 // libstdc++ v4.9 and below do not support std::is_trivially_copyable #define PLF_TYPE_TRAITS_SUPPORT #endif #if __GLIBCXX__ >= 20160111 #define PLF_IS_ALWAYS_EQUAL_SUPPORT #endif #elif defined(_LIBCPP_CXX03_LANG) || defined(_LIBCPP_HAS_NO_RVALUE_REFERENCES) // Special case for checking C++11 support with libCPP #define PLF_STATIC_ASSERT(check, message) assert(check) #define PLF_NOEXCEPT throw() #if !defined(_LIBCPP_HAS_NO_VARIADICS) #define PLF_VARIADICS_SUPPORT #endif #else // Assume type traits and initializer support for other compilers and standard library implementations #define PLF_MOVE_SEMANTICS_SUPPORT #define PLF_STATIC_ASSERT(check, message) static_assert(check, message) #define PLF_VARIADICS_SUPPORT #define PLF_TYPE_TRAITS_SUPPORT #define PLF_ALLOCATOR_TRAITS_SUPPORT #define PLF_ALIGNMENT_SUPPORT #define PLF_INITIALIZER_LIST_SUPPORT #define PLF_NOEXCEPT noexcept #define PLF_IS_ALWAYS_EQUAL_SUPPORT #endif #if __cplusplus >= 201703L && ((defined(__clang__) && ((__clang_major__ == 3 && __clang_minor__ == 9) || __clang_major__ > 3)) || (defined(__GNUC__) && __GNUC__ >= 7) || (!defined(__clang__) && !defined(__GNUC__))) // assume correct C++17 implementation for non-gcc/clang compilers #define PLF_CONSTEXPR constexpr #else #define PLF_CONSTEXPR #endif #if __cplusplus > 201703L && ((defined(__clang__) && (__clang_major__ >= 13)) || (defined(__GNUC__) && __GNUC__ >= 10) || (!defined(__clang__) && !defined(__GNUC__))) #define PLF_CPP20_SUPPORT #endif #else #define PLF_FORCE_INLINE #define PLF_STATIC_ASSERT(check, message) assert(check) #define PLF_NOEXCEPT throw() #define PLF_CONSTEXPR #endif #if defined(PLF_IS_ALWAYS_EQUAL_SUPPORT) && defined(PLF_MOVE_SEMANTICS_SUPPORT) && defined(PLF_ALLOCATOR_TRAITS_SUPPORT) && (__cplusplus >= 201703L || (defined(_MSVC_LANG) && (_MSVC_LANG >= 201703L))) #define PLF_NOEXCEPT_MOVE_ASSIGN(the_allocator) noexcept(std::allocator_traits::propagate_on_container_move_assignment::value || std::allocator_traits::is_always_equal::value) #define PLF_NOEXCEPT_SWAP(the_allocator) noexcept(std::allocator_traits::propagate_on_container_swap::value || std::allocator_traits::is_always_equal::value) #else #define PLF_NOEXCEPT_MOVE_ASSIGN(the_allocator) #define PLF_NOEXCEPT_SWAP(the_allocator) #endif #undef PLF_IS_ALWAYS_EQUAL_SUPPORT #ifdef PLF_ALLOCATOR_TRAITS_SUPPORT #ifdef PLF_VARIADICS_SUPPORT #define PLF_CONSTRUCT(the_allocator, allocator_instance, location, ...) std::allocator_traits::construct(allocator_instance, location, __VA_ARGS__) #else #define PLF_CONSTRUCT(the_allocator, allocator_instance, location, data) std::allocator_traits::construct(allocator_instance, location, data) #endif #define PLF_DESTROY(the_allocator, allocator_instance, location) std::allocator_traits::destroy(allocator_instance, location) #define PLF_ALLOCATE(the_allocator, allocator_instance, size, hint) std::allocator_traits::allocate(allocator_instance, size, hint) #define PLF_DEALLOCATE(the_allocator, allocator_instance, location, size) std::allocator_traits::deallocate(allocator_instance, location, size) #else #ifdef PLF_VARIADICS_SUPPORT #define PLF_CONSTRUCT(the_allocator, allocator_instance, location, ...) (allocator_instance).construct(location, __VA_ARGS__) #else #define PLF_CONSTRUCT(the_allocator, allocator_instance, location, data) (allocator_instance).construct(location, data) #endif #define PLF_DESTROY(the_allocator, allocator_instance, location) (allocator_instance).destroy(location) #define PLF_ALLOCATE(the_allocator, allocator_instance, size, hint) (allocator_instance).allocate(size, hint) #define PLF_DEALLOCATE(the_allocator, allocator_instance, location, size) (allocator_instance).deallocate(location, size) #endif #include // std::fill_n, std::sort #include // assert #include // memset, memcpy, size_t #include // std::numeric_limits #include // std::allocator #include // std::bidirectional_iterator_tag, iterator_traits, make_move_iterator, std::distance for range insert #include // std::length_error #ifdef PLF_TYPE_TRAITS_SUPPORT #include // offsetof, used in blank() #include // std::is_trivially_destructible, etc #endif #ifdef PLF_MOVE_SEMANTICS_SUPPORT #include // std::move #endif #ifdef PLF_INITIALIZER_LIST_SUPPORT #include #endif #ifdef PLF_CPP20_SUPPORT #include #endif namespace plf { struct colony_limits // for use in block_capacity setting/getting functions and constructors { size_t min, max; colony_limits(const size_t minimum, const size_t maximum) PLF_NOEXCEPT : min(minimum), max(maximum) {} }; enum colony_priority { performance, memory_use }; template , plf::colony_priority priority = plf::performance> class colony : private allocator_type // Empty base class optimisation (EBCO) - inheriting allocator functions { // Type-switching pattern: template struct choose; template struct choose { typedef is_true type; }; template struct choose { typedef is_false type; }; typedef typename choose::type skipfield_type; // Note: unsigned short is equivalent to uint_least16_t ie. Using 16-bit unsigned integer in best-case scenario, greater-than-16-bit unsigned integer where platform doesn't support 16-bit types. unsigned char is always == 1 byte, as opposed to uint_8, which may not be public: // Standard container typedefs: typedef element_type value_type; #ifdef PLF_ALIGNMENT_SUPPORT typedef typename std::aligned_storage= (sizeof(skipfield_type) * 2) || alignof(element_type) >= (sizeof(skipfield_type) * 2)) ? alignof(element_type) : (sizeof(skipfield_type) * 2)>::type aligned_element_type; #else typedef element_type aligned_element_type; #endif #ifdef PLF_ALLOCATOR_TRAITS_SUPPORT typedef typename std::allocator_traits::size_type size_type; typedef typename std::allocator_traits::difference_type difference_type; typedef element_type & reference; typedef const element_type & const_reference; typedef typename std::allocator_traits::pointer pointer; typedef typename std::allocator_traits::const_pointer const_pointer; #else typedef typename allocator_type::size_type size_type; typedef typename allocator_type::difference_type difference_type; typedef typename allocator_type::reference reference; typedef typename allocator_type::const_reference const_reference; typedef typename allocator_type::pointer pointer; typedef typename allocator_type::const_pointer const_pointer; #endif // Iterator declarations: template class colony_iterator; typedef colony_iterator iterator; typedef colony_iterator const_iterator; friend class colony_iterator; // Using above typedef name here is illegal under C++03 friend class colony_iterator; template class colony_reverse_iterator; typedef colony_reverse_iterator reverse_iterator; typedef colony_reverse_iterator const_reverse_iterator; friend class colony_reverse_iterator; friend class colony_reverse_iterator; private: #ifdef PLF_ALIGNMENT_SUPPORT struct alignas(alignof(aligned_element_type)) aligned_allocation_struct { char data[alignof(aligned_element_type)]; // Using char as sizeof is always guaranteed to be 1 byte regardless of the number of bits in a byte on given computer, whereas for example, uint8_t would fail on machines where there are more than 8 bits in a byte eg. Texas Instruments C54x DSPs. }; #define PLF_GROUP_ALIGNED_BLOCK_SIZE(elements_per_group) ((((elements_per_group * (((sizeof(aligned_element_type) >= alignof(aligned_element_type)) ? sizeof(aligned_element_type) : alignof(aligned_element_type)) + sizeof(skipfield_type))) + sizeof(skipfield_type)) + sizeof(aligned_allocation_struct) - 1) / sizeof(aligned_allocation_struct)) // The size of a groups' memory block when expressed in multiples of the value_type's alignment. We also check to see if alignment is larger than sizeof value_type and use alignment size if so. #else struct aligned_allocation_struct { char data; }; #define PLF_GROUP_ALIGNED_BLOCK_SIZE(elements_per_group) ((elements_per_group * (sizeof(aligned_element_type) + sizeof(skipfield_type))) + sizeof(skipfield_type)) // The size of a groups' memory block when expressed in bytes, since no alignment available #endif // forward declarations for typedefs below struct group; struct item_index_tuple; // for use in sort() #ifdef PLF_ALLOCATOR_TRAITS_SUPPORT typedef typename std::allocator_traits::template rebind_alloc aligned_element_allocator_type; typedef typename std::allocator_traits::template rebind_alloc group_allocator_type; typedef typename std::allocator_traits::template rebind_alloc skipfield_allocator_type; typedef typename std::allocator_traits::template rebind_alloc aligned_struct_allocator_type; typedef typename std::allocator_traits::template rebind_alloc tuple_allocator_type; typedef typename std::allocator_traits::template rebind_alloc uchar_allocator_type; typedef typename std::allocator_traits::pointer aligned_pointer_type; // pointer to the overaligned element type, not the original element type typedef typename std::allocator_traits::pointer group_pointer_type; typedef typename std::allocator_traits::pointer skipfield_pointer_type; typedef typename std::allocator_traits::pointer aligned_struct_pointer_type; typedef typename std::allocator_traits::pointer tuple_pointer_type; #else typedef typename allocator_type::template rebind::other aligned_element_allocator_type; // In case compiler supports alignment but not allocator_traits typedef typename allocator_type::template rebind::other group_allocator_type; typedef typename allocator_type::template rebind::other skipfield_allocator_type; typedef typename allocator_type::template rebind::other aligned_struct_allocator_type; typedef typename allocator_type::template rebind::other tuple_allocator_type; typedef typename allocator_type::template rebind::other uchar_allocator_type; typedef typename aligned_element_allocator_type::pointer aligned_pointer_type; typedef typename group_allocator_type::pointer group_pointer_type; typedef typename skipfield_allocator_type::pointer skipfield_pointer_type; typedef typename aligned_struct_allocator_type::pointer aligned_struct_pointer_type; typedef typename tuple_allocator_type::pointer tuple_pointer_type; #endif // Colony groups: struct group : private aligned_struct_allocator_type // ebco - inherit allocator functions { aligned_pointer_type last_endpoint; // The address that is one past the highest cell number that's been used so far in this group - does not change with erase command but may change with insert (if no previously-erased locations are available) - is necessary because an iterator cannot access the colony's end_iterator. Most-used variable in colony use (operator ++, --) so first in struct. If the colony has been completely filled at some point, it will be == reinterpret_cast(skipfield) group_pointer_type next_group; // Next group in the intrusive list of all groups. NULL if no next group const aligned_pointer_type elements; // Element storage const skipfield_pointer_type skipfield; // Skipfield storage. The element and skipfield arrays are allocated contiguously in this implementation, hence the skipfield pointer also functions as a 'one-past-end' pointer for the elements array. There will always be one additional skipfield node allocated compared to the number of elements. This is to ensure a faster ++ iterator operation (fewer checks are required when this is present). The extra node is unused and always zero, but checked, and not having it will result in out-of-bounds memory errors. group_pointer_type previous_group; // previous group in the linked list of all groups. NULL if no preceding group skipfield_type free_list_head; // The index of the last erased element in the group. The last erased element will, in turn, contain the number of the index of the next erased element, and so on. If this is == maximum skipfield_type value then free_list is empty ie. no erasures have occurred in the group (or if they have, the erased locations have subsequently been reused via insert()). const skipfield_type capacity; // The element capacity of this particular group - can also be calculated from reinterpret_cast(group->skipfield) - group->elements, however this space is effectively free due to struct padding and the default sizeof skipfield_type, and calculating it once is cheaper skipfield_type size; // indicates total number of active elements in group - changes with insert and erase commands - used to check for empty group in erase function, as an indication to remove the group group_pointer_type erasures_list_next_group; // The next group in the singly-linked list of groups with erasures ie. with active erased-element free lists size_type group_number; // Used for comparison (> < >= <= <=>) iterator operators (used by distance function and user) #ifdef PLF_VARIADICS_SUPPORT group(const skipfield_type elements_per_group, group_pointer_type const previous): last_endpoint(reinterpret_cast(PLF_ALLOCATE(aligned_struct_allocator_type, *this, PLF_GROUP_ALIGNED_BLOCK_SIZE(elements_per_group), (previous == NULL) ? 0 : previous->elements))), next_group(NULL), elements(last_endpoint++), skipfield(reinterpret_cast(elements + elements_per_group)), previous_group(previous), free_list_head(std::numeric_limits::max()), capacity(elements_per_group), size(1), erasures_list_next_group(NULL), group_number((previous == NULL) ? 0 : previous->group_number + 1u) { // Static casts to unsigned int from short not necessary as C++ automatically promotes lesser types for arithmetic purposes. std::memset(&*skipfield, 0, sizeof(skipfield_type) * (static_cast(elements_per_group) + 1u)); // &* to avoid problems with non-trivial pointers } #else // This is a hack around the fact that allocator_type::construct only supports copy construction in C++03 and copy elision does not occur on the vast majority of compilers in this circumstance. So to avoid running out of memory (and losing performance) from allocating the same block twice, we're allocating in the 'copy' constructor. group(const skipfield_type elements_per_group, group_pointer_type const previous) PLF_NOEXCEPT: elements(NULL), skipfield(NULL), previous_group(previous), capacity(elements_per_group) {} // Not a real copy constructor ie. actually a constructor. Only used for allocator.construct in C++03 for reasons stated above: group(const group &source): aligned_struct_allocator_type(source), last_endpoint(reinterpret_cast(PLF_ALLOCATE(aligned_struct_allocator_type, *this, PLF_GROUP_ALIGNED_BLOCK_SIZE(source.capacity), (source.previous_group == NULL) ? 0 : source.previous_group->elements))), next_group(NULL), elements(last_endpoint++), skipfield(reinterpret_cast(elements + source.capacity)), previous_group(source.previous_group), free_list_head(std::numeric_limits::max()), capacity(source.capacity), size(1), erasures_list_next_group(NULL), group_number((source.previous_group == NULL) ? 0 : source.previous_group->group_number + 1u) { std::memset(&*skipfield, 0, sizeof(skipfield_type) * (static_cast(capacity) + 1u)); } #endif void reset(const skipfield_type increment, const group_pointer_type next, const group_pointer_type previous, const size_type group_num) PLF_NOEXCEPT { last_endpoint = elements + increment; next_group = next; free_list_head = std::numeric_limits::max(); previous_group = previous; size = increment; erasures_list_next_group = NULL; group_number = group_num; std::memset(&*skipfield, 0, sizeof(skipfield_type) * static_cast(capacity)); // capacity + 1 is not necessary here as the end skipfield is never written to after initialization } ~group() PLF_NOEXCEPT { // Null check not necessary (for copied group as above) as delete will also perform a null check. PLF_DEALLOCATE(aligned_struct_allocator_type, *this, reinterpret_cast(elements), PLF_GROUP_ALIGNED_BLOCK_SIZE(capacity)); } }; public: // Iterators: template class colony_iterator { private: group_pointer_type group_pointer; aligned_pointer_type element_pointer; skipfield_pointer_type skipfield_pointer; public: typedef std::bidirectional_iterator_tag iterator_category; typedef typename colony::value_type value_type; typedef typename colony::difference_type difference_type; typedef typename choose::type pointer; typedef typename choose::type reference; friend class colony; friend class colony_reverse_iterator; friend class colony_reverse_iterator; inline colony_iterator & operator = (const colony_iterator &source) PLF_NOEXCEPT { group_pointer = source.group_pointer; element_pointer = source.element_pointer; skipfield_pointer = source.skipfield_pointer; return *this; } inline colony_iterator & operator = (const colony_iterator &source) PLF_NOEXCEPT { group_pointer = source.group_pointer; element_pointer = source.element_pointer; skipfield_pointer = source.skipfield_pointer; return *this; } #ifdef PLF_MOVE_SEMANTICS_SUPPORT // Move assignment - only really necessary if the allocator uses non-standard ie. "smart" pointers inline colony_iterator & operator = (colony_iterator &&source) PLF_NOEXCEPT { assert(&source != this); group_pointer = std::move(source.group_pointer); element_pointer = std::move(source.element_pointer); skipfield_pointer = std::move(source.skipfield_pointer); return *this; } inline colony_iterator & operator = (colony_iterator &&source) PLF_NOEXCEPT { group_pointer = std::move(source.group_pointer); element_pointer = std::move(source.element_pointer); skipfield_pointer = std::move(source.skipfield_pointer); return *this; } #endif inline PLF_FORCE_INLINE bool operator == (const colony_iterator &rh) const PLF_NOEXCEPT { return (element_pointer == rh.element_pointer); } inline PLF_FORCE_INLINE bool operator == (const colony_iterator &rh) const PLF_NOEXCEPT { return (element_pointer == rh.element_pointer); } inline PLF_FORCE_INLINE bool operator != (const colony_iterator &rh) const PLF_NOEXCEPT { return (element_pointer != rh.element_pointer); } inline PLF_FORCE_INLINE bool operator != (const colony_iterator &rh) const PLF_NOEXCEPT { return (element_pointer != rh.element_pointer); } inline PLF_FORCE_INLINE reference operator * () const // may cause exception with uninitialized iterator { return *(reinterpret_cast(element_pointer)); } inline PLF_FORCE_INLINE pointer operator -> () const PLF_NOEXCEPT { return reinterpret_cast(element_pointer); } #if defined(_MSC_VER) && !defined(__clang__) && !defined(__GNUC__) && _MSC_VER <= 1600 // MSVC 2010 needs a bit of a helping hand when it comes to optimizing inline PLF_FORCE_INLINE colony_iterator & operator ++ () #else colony_iterator & operator ++ () #endif { assert(group_pointer != NULL); // covers uninitialised colony_iterator skipfield_type skip = *(++skipfield_pointer); if ((element_pointer += static_cast(skip) + 1u) == group_pointer->last_endpoint && group_pointer->next_group != NULL) // ie. beyond end of current memory block. Second condition allows iterator to reach end(), which may be 1 past end of block, if block has been fully used and another block is not allocated { group_pointer = group_pointer->next_group; const aligned_pointer_type elements = group_pointer->elements; const skipfield_pointer_type skipfield = group_pointer->skipfield; skip = *skipfield; element_pointer = elements + skip; skipfield_pointer = skipfield; } skipfield_pointer += skip; return *this; } inline colony_iterator operator ++(int) { const colony_iterator copy(*this); ++*this; return copy; } public: colony_iterator & operator -- () { assert(group_pointer != NULL); if (element_pointer != group_pointer->elements) // ie. not already at beginning of group { const skipfield_type skip = *(--skipfield_pointer); skipfield_pointer -= skip; if ((element_pointer -= static_cast(skip) + 1u) != group_pointer->elements - 1) // ie. iterator was not already at beginning of colony (with some previous consecutive deleted elements), and skipfield does not takes us into the previous group) { return *this; } } group_pointer = group_pointer->previous_group; const skipfield_pointer_type skipfield = group_pointer->skipfield + group_pointer->capacity - 1; const skipfield_type skip = *skipfield; element_pointer = (reinterpret_cast(group_pointer->skipfield) - 1) - skip; skipfield_pointer = skipfield - skip; return *this; } inline colony_iterator operator -- (int) { const colony_iterator copy(*this); --*this; return copy; } template inline bool operator > (const colony_iterator &rh) const PLF_NOEXCEPT { return ((group_pointer == rh.group_pointer) & (element_pointer > rh.element_pointer)) || (group_pointer != rh.group_pointer && group_pointer->group_number > rh.group_pointer->group_number); } template inline bool operator < (const colony_iterator &rh) const PLF_NOEXCEPT { return rh > *this; } template inline bool operator >= (const colony_iterator &rh) const PLF_NOEXCEPT { return !(rh > *this); } template inline bool operator <= (const colony_iterator &rh) const PLF_NOEXCEPT { return !(*this > rh); } #ifdef PLF_CPP20_SUPPORT template inline int operator <=> (const colony_iterator &rh) const PLF_NOEXCEPT { return (element_pointer == rh.element_pointer) ? 0 : ((*this > rh) ? 1 : -1); } #endif colony_iterator() PLF_NOEXCEPT: group_pointer(NULL), element_pointer(NULL), skipfield_pointer(NULL) {} private: // Used by cend(), erase() etc: colony_iterator(const group_pointer_type group_p, const aligned_pointer_type element_p, const skipfield_pointer_type skipfield_p) PLF_NOEXCEPT: group_pointer(group_p), element_pointer(element_p), skipfield_pointer(skipfield_p) {} public: // Friend functions: template friend inline void advance(colony_iterator &it, distance_type distance) { it.advance(static_cast(distance)); } friend inline colony_iterator next(const colony_iterator &it, const difference_type distance) { colony_iterator return_iterator(it); return_iterator.advance(static_cast(distance)); return return_iterator; } friend inline colony_iterator prev(const colony_iterator &it, const difference_type distance) { colony_iterator return_iterator(it); return_iterator.advance(-(static_cast(distance))); return return_iterator; } friend inline typename colony_iterator::difference_type distance(const colony_iterator &first, const colony_iterator &last) { return first.distance(last); } private: // Advance implementation: void advance(difference_type distance) // Cannot be noexcept due to the possibility of an uninitialized iterator { assert(group_pointer != NULL); // covers uninitialized colony_iterator && empty group // Now, run code based on the nature of the distance type - negative, positive or zero: if (distance > 0) // ie. += { // Code explanation: // For the initial state of the iterator, we don't know how what elements have been erased before that element in that group. // So for the first group, we follow the following logic: // 1. If no elements have been erased in the group, we do simple addition to progress either to within the group (if the distance is small enough) or the end of the group and subtract from distance accordingly. // 2. If any of the first group elements have been erased, we manually iterate, as we don't know whether the erased elements occur before or after the initial iterator position, and we subtract 1 from the distance amount each time. Iteration continues until either distance becomes zero, or we reach the end of the group. // For all subsequent groups, we follow this logic: // 1. If distance is larger than the total number of non-erased elements in a group, we skip that group and subtract the number of elements in that group from distance // 2. If distance is smaller than the total number of non-erased elements in a group, then: // a. if there're no erased elements in the group we simply add distance to group->elements to find the new location for the iterator // b. if there are erased elements in the group, we manually iterate and subtract 1 from distance on each iteration, until the new iterator location is found ie. distance = 0 // Note: incrementing element_pointer is avoided until necessary to avoid needless calculations assert(!(element_pointer == group_pointer->last_endpoint && group_pointer->next_group == NULL)); // Check that we're not already at end() // Special case for initial element pointer and initial group (we don't know how far into the group the element pointer is) if (element_pointer != group_pointer->elements + *(group_pointer->skipfield)) // ie. != first non-erased element in group { const difference_type distance_from_end = static_cast(group_pointer->last_endpoint - element_pointer); if (group_pointer->size == static_cast(distance_from_end)) // ie. if there are no erasures in the group (using endpoint - elements_start to determine number of elements in group just in case this is the last group of the colony, in which case group->last_endpoint != group->elements + group->capacity) { if (distance < distance_from_end) { element_pointer += distance; skipfield_pointer += distance; return; } else if (group_pointer->next_group == NULL) // either we've reached end() or gone beyond it, so bound to end() { element_pointer = group_pointer->last_endpoint; skipfield_pointer += distance_from_end; return; } else { distance -= distance_from_end; } } else { const skipfield_pointer_type endpoint = skipfield_pointer + distance_from_end; while(true) { ++skipfield_pointer; skipfield_pointer += *skipfield_pointer; --distance; if (skipfield_pointer == endpoint) { break; } else if (distance == 0) { element_pointer = group_pointer->elements + (skipfield_pointer - group_pointer->skipfield); return; } } if (group_pointer->next_group == NULL) // either we've reached end() or gone beyond it, so bound to end() { element_pointer = group_pointer->last_endpoint; return; } } group_pointer = group_pointer->next_group; if (distance == 0) { element_pointer = group_pointer->elements + *(group_pointer->skipfield); skipfield_pointer = group_pointer->skipfield + *(group_pointer->skipfield); return; } } // Intermediary groups - at the start of this code block and the subsequent block, the position of the iterator is assumed to be the first non-erased element in the current group: while (static_cast(group_pointer->size) <= distance) { if (group_pointer->next_group == NULL) // either we've reached end() or gone beyond it, so bound to end() { element_pointer = group_pointer->last_endpoint; skipfield_pointer = group_pointer->skipfield + (group_pointer->last_endpoint - group_pointer->elements); return; } else if ((distance -= group_pointer->size) == 0) { group_pointer = group_pointer->next_group; element_pointer = group_pointer->elements + *(group_pointer->skipfield); skipfield_pointer = group_pointer->skipfield + *(group_pointer->skipfield); return; } else { group_pointer = group_pointer->next_group; } } // Final group (if not already reached): if (group_pointer->free_list_head == std::numeric_limits::max()) // No erasures in this group, use straight pointer addition { element_pointer = group_pointer->elements + distance; skipfield_pointer = group_pointer->skipfield + distance; return; } else // ie. size > distance - safe to ignore endpoint check condition while incrementing: { skipfield_pointer = group_pointer->skipfield + *(group_pointer->skipfield); do { ++skipfield_pointer; skipfield_pointer += *skipfield_pointer; } while(--distance != 0); element_pointer = group_pointer->elements + (skipfield_pointer - group_pointer->skipfield); return; } return; } else if (distance < 0) // for negative change { // Code logic is very similar to += above assert(!((element_pointer == group_pointer->elements + *(group_pointer->skipfield)) && group_pointer->previous_group == NULL)); // check that we're not already at begin() distance = -distance; // Special case for initial element pointer and initial group (we don't know how far into the group the element pointer is) if (element_pointer != group_pointer->last_endpoint) // ie. != end() { if (group_pointer->free_list_head == std::numeric_limits::max()) // ie. no prior erasures have occurred in this group { const difference_type distance_from_beginning = static_cast(element_pointer - group_pointer->elements); if (distance <= distance_from_beginning) { element_pointer -= distance; skipfield_pointer -= distance; return; } else if (group_pointer->previous_group == NULL) // ie. we've gone before begin(), so bound to begin() { element_pointer = group_pointer->elements; skipfield_pointer = group_pointer->skipfield; return; } else { distance -= distance_from_beginning; } } else { const skipfield_pointer_type beginning_point = group_pointer->skipfield + *(group_pointer->skipfield); while(skipfield_pointer != beginning_point) { --skipfield_pointer; skipfield_pointer -= *skipfield_pointer; if (--distance == 0) { element_pointer = group_pointer->elements + (skipfield_pointer - group_pointer->skipfield); return; } } if (group_pointer->previous_group == NULL) { element_pointer = group_pointer->elements + *(group_pointer->skipfield); // This is first group, so bound to begin() (just in case final decrement took us before begin()) skipfield_pointer = group_pointer->skipfield + *(group_pointer->skipfield); return; } } group_pointer = group_pointer->previous_group; } // Intermediary groups - at the start of this code block and the subsequent block, the position of the iterator is assumed to be either the first non-erased element in the next group over, or end(): while(static_cast(group_pointer->size) < distance) { if (group_pointer->previous_group == NULL) // we've gone beyond begin(), so bound to it { element_pointer = group_pointer->elements + *(group_pointer->skipfield); skipfield_pointer = group_pointer->skipfield + *(group_pointer->skipfield); return; } distance -= group_pointer->size; group_pointer = group_pointer->previous_group; } // Final group (if not already reached): if (static_cast(group_pointer->size) == distance) { element_pointer = group_pointer->elements + *(group_pointer->skipfield); skipfield_pointer = group_pointer->skipfield + *(group_pointer->skipfield); return; } else if (group_pointer->free_list_head == std::numeric_limits::max()) // ie. no erased elements in this group { element_pointer = reinterpret_cast(group_pointer->skipfield) - distance; skipfield_pointer = (group_pointer->skipfield + group_pointer->capacity) - distance; return; } else // ie. no more groups to traverse but there are erased elements in this group { skipfield_pointer = group_pointer->skipfield + group_pointer->capacity; do { --skipfield_pointer; skipfield_pointer -= *skipfield_pointer; } while(--distance != 0); element_pointer = group_pointer->elements + (skipfield_pointer - group_pointer->skipfield); return; } } // Only distance == 0 reaches here } // distance implementation: difference_type distance(const colony_iterator &last) const { // Code logic: // If iterators are the same, return 0 // Otherwise, find which iterator is later in colony, copy that to iterator2. Copy the lower to iterator1. // If they are not pointing to elements in the same group, process the intermediate groups and add distances, // skipping manual incrementation in all but the initial and final groups. // In the initial and final groups, manual incrementation must be used to calculate distance, if there have been no prior erasures in those groups. // If there are no prior erasures in either of those groups, we can use pointer arithmetic to calculate the distances for those groups. assert(!(group_pointer == NULL) && !(last.group_pointer == NULL)); // Check that they are initialized if (last.element_pointer == element_pointer) { return 0; } difference_type distance = 0; colony_iterator iterator1 = *this, iterator2 = last; const bool swap = iterator1 > iterator2; if (swap) // Less common case { iterator1 = last; iterator2 = *this; } if (iterator1.group_pointer != iterator2.group_pointer) // if not in same group, process intermediate groups { // Process initial group: if (iterator1.group_pointer->free_list_head == std::numeric_limits::max()) // If no prior erasures have occured in this group we can do simple addition { distance += static_cast(iterator1.group_pointer->last_endpoint - iterator1.element_pointer); } else if (iterator1.element_pointer == iterator1.group_pointer->elements + *(iterator1.group_pointer->skipfield)) // ie. element is at start of group - rare case { distance += static_cast(iterator1.group_pointer->size); } else // Manually iterate to find distance to end of group: { const skipfield_pointer_type endpoint = iterator1.skipfield_pointer + (iterator1.group_pointer->last_endpoint - iterator1.element_pointer); while (iterator1.skipfield_pointer != endpoint) { ++iterator1.skipfield_pointer; iterator1.skipfield_pointer += *iterator1.skipfield_pointer; ++distance; } } // Process all other intermediate groups: iterator1.group_pointer = iterator1.group_pointer->next_group; while (iterator1.group_pointer != iterator2.group_pointer) { distance += static_cast(iterator1.group_pointer->size); iterator1.group_pointer = iterator1.group_pointer->next_group; } iterator1.skipfield_pointer = iterator1.group_pointer->skipfield; } if (iterator2.group_pointer->free_list_head == std::numeric_limits::max()) // ie. no erasures in this group, direct subtraction is possible { distance += iterator2.skipfield_pointer - iterator1.skipfield_pointer; } else if (iterator2.group_pointer->last_endpoint - 1 >= iterator2.element_pointer || iterator2.element_pointer + *(iterator2.skipfield_pointer + 1) == iterator2.group_pointer->last_endpoint) // ie. if iterator2 is .end() or the last element in the block { distance += static_cast(iterator2.group_pointer->size) - (iterator2.group_pointer->last_endpoint - iterator2.element_pointer); } else { while (iterator1.skipfield_pointer != iterator2.skipfield_pointer) { ++iterator1.skipfield_pointer; iterator1.skipfield_pointer += *iterator1.skipfield_pointer; ++distance; } } if (swap) { distance = -distance; } return distance; } public: inline colony_iterator (const colony_iterator &source) PLF_NOEXCEPT: group_pointer(source.group_pointer), element_pointer(source.element_pointer), skipfield_pointer(source.skipfield_pointer) {} inline colony_iterator(const colony_iterator &source) PLF_NOEXCEPT: group_pointer(source.group_pointer), element_pointer(source.element_pointer), skipfield_pointer(source.skipfield_pointer) {} #ifdef PLF_MOVE_SEMANTICS_SUPPORT // move constructors inline colony_iterator(colony_iterator &&source) PLF_NOEXCEPT: group_pointer(std::move(source.group_pointer)), element_pointer(std::move(source.element_pointer)), skipfield_pointer(std::move(source.skipfield_pointer)) {} inline colony_iterator(colony_iterator &&source) PLF_NOEXCEPT: group_pointer(std::move(source.group_pointer)), element_pointer(std::move(source.element_pointer)), skipfield_pointer(std::move(source.skipfield_pointer)) {} #endif }; // colony_iterator // Reverse iterators: template class colony_reverse_iterator { private: iterator it; public: typedef std::bidirectional_iterator_tag iterator_category; typedef typename colony::value_type value_type; typedef typename colony::difference_type difference_type; typedef typename choose::type pointer; typedef typename choose::type reference; friend class colony; inline colony_reverse_iterator& operator = (const colony_reverse_iterator &source) PLF_NOEXCEPT { it = source.it; return *this; } inline colony_reverse_iterator& operator = (const colony_reverse_iterator &source) PLF_NOEXCEPT { it = source.it; return *this; } template inline colony_reverse_iterator& operator = (const colony_iterator &source) PLF_NOEXCEPT { it = source; return *this; } #ifdef PLF_MOVE_SEMANTICS_SUPPORT // move assignment inline colony_reverse_iterator& operator = (colony_reverse_iterator &&source) PLF_NOEXCEPT { assert(&source != this); it = std::move(source.it); return *this; } inline colony_reverse_iterator& operator = (colony_reverse_iterator &&source) PLF_NOEXCEPT { it = std::move(source.it); return *this; } #endif inline PLF_FORCE_INLINE bool operator == (const colony_reverse_iterator &rh) const PLF_NOEXCEPT { return (it == rh.it); } inline PLF_FORCE_INLINE bool operator == (const colony_reverse_iterator &rh) const PLF_NOEXCEPT { return (it == rh.it); } inline PLF_FORCE_INLINE bool operator != (const colony_reverse_iterator &rh) const PLF_NOEXCEPT { return (it != rh.it); } inline PLF_FORCE_INLINE bool operator != (const colony_reverse_iterator &rh) const PLF_NOEXCEPT { return (it != rh.it); } inline PLF_FORCE_INLINE reference operator * () const PLF_NOEXCEPT { return *(reinterpret_cast(it.element_pointer)); } inline PLF_FORCE_INLINE pointer * operator -> () const PLF_NOEXCEPT { return reinterpret_cast(it.element_pointer); } // In this case we have to redefine the algorithm, rather than using the internal iterator's -- operator, in order for the reverse_iterator to be allowed to reach rend() ie. begin_iterator - 1 colony_reverse_iterator & operator ++ () { colony::group_pointer_type &group_pointer = it.group_pointer; colony::aligned_pointer_type &element_pointer = it.element_pointer; colony::skipfield_pointer_type &skipfield_pointer = it.skipfield_pointer; assert(group_pointer != NULL); if (element_pointer != group_pointer->elements) // ie. not already at beginning of group { element_pointer -= static_cast(*(--skipfield_pointer)) + 1u; skipfield_pointer -= *skipfield_pointer; if (!(element_pointer == group_pointer->elements - 1 && group_pointer->previous_group == NULL)) // ie. iterator is not == rend() { return *this; } } if (group_pointer->previous_group != NULL) // ie. not first group in colony { group_pointer = group_pointer->previous_group; skipfield_pointer = group_pointer->skipfield + group_pointer->capacity - 1; element_pointer = (reinterpret_cast(group_pointer->skipfield) - 1) - *skipfield_pointer; skipfield_pointer -= *skipfield_pointer; } else // necessary so that reverse_iterator can end up == rend(), if we were already at first element in colony { --element_pointer; --skipfield_pointer; } return *this; } inline colony_reverse_iterator operator ++ (int) { const colony_reverse_iterator copy(*this); ++*this; return copy; } inline PLF_FORCE_INLINE colony_reverse_iterator & operator -- () { ++it; return *this; } inline colony_reverse_iterator operator -- (int) { const colony_reverse_iterator copy(*this); --*this; return copy; } inline typename colony::iterator base() const { return ++(typename colony::iterator(it)); } template inline bool operator > (const colony_reverse_iterator &rh) const PLF_NOEXCEPT { return (rh.it > it); } template inline bool operator < (const colony_reverse_iterator &rh) const PLF_NOEXCEPT { return (it > rh.it); } template inline bool operator >= (const colony_reverse_iterator &rh) const PLF_NOEXCEPT { return !(it > rh.it); } template inline bool operator <= (const colony_reverse_iterator &rh) const PLF_NOEXCEPT { return !(rh.it > it); } #ifdef PLF_CPP20_SUPPORT template inline int operator <=> (const colony_reverse_iterator &rh) const PLF_NOEXCEPT { return (rh.it <=> it); } #endif colony_reverse_iterator () PLF_NOEXCEPT {} colony_reverse_iterator (const colony_reverse_iterator &source) PLF_NOEXCEPT: it(source.it) {} colony_reverse_iterator (const colony_reverse_iterator &source) PLF_NOEXCEPT: it(source.it) {} template explicit colony_reverse_iterator (const colony_iterator &source) PLF_NOEXCEPT: it(source) {} private: // Used by rend(), etc: colony_reverse_iterator(const group_pointer_type group_p, const aligned_pointer_type element_p, const skipfield_pointer_type skipfield_p) PLF_NOEXCEPT: it(group_p, element_p, skipfield_p) {} public: // Friend functions: template friend inline void advance(colony_reverse_iterator &it, distance_type distance) { it.advance(static_cast(distance)); } friend inline colony_reverse_iterator next(const colony_reverse_iterator &it, const difference_type distance) { colony_reverse_iterator return_iterator(it); return_iterator.advance(static_cast(distance)); return return_iterator; } template friend inline colony_reverse_iterator prev(const colony_reverse_iterator &it, const difference_type distance) { colony_reverse_iterator return_iterator(it); return_iterator.advance(-(static_cast(distance))); return return_iterator; } friend inline typename colony_reverse_iterator::difference_type distance(const colony_reverse_iterator &first, const colony_reverse_iterator &last) { return first.distance(last); } // distance implementation: inline difference_type distance(const colony_reverse_iterator &last) const { return last.it.distance(it); } private: // Advance for reverse_iterator and const_reverse_iterator - this needs to be implemented slightly differently to forward-iterator's advance, as it needs to be able to reach rend() (ie. begin() - 1) and to be bounded by rbegin(): void advance(difference_type distance) // could cause exception if iterator is uninitialized { group_pointer_type &group_pointer = it.group_pointer; aligned_pointer_type &element_pointer = it.element_pointer; skipfield_pointer_type &skipfield_pointer = it.skipfield_pointer; assert(element_pointer != NULL); if (distance > 0) { assert(!(element_pointer == group_pointer->elements - 1 && group_pointer->previous_group == NULL)); // Check that we're not already at rend() // Special case for initial element pointer and initial group (we don't know how far into the group the element pointer is) // Since a reverse_iterator cannot == last_endpoint (ie. before rbegin()) we don't need to check for that like with iterator if (group_pointer->free_list_head == std::numeric_limits::max()) { const difference_type distance_from_beginning = element_pointer - group_pointer->elements; if (distance <= distance_from_beginning) { element_pointer -= distance; skipfield_pointer -= distance; return; } else if (group_pointer->previous_group == NULL) // Either we've reached rend() or gone beyond it, so bound to rend() { element_pointer = group_pointer->elements - 1; skipfield_pointer = group_pointer->skipfield - 1; return; } else { distance -= distance_from_beginning; } } else { const skipfield_pointer_type beginning_point = group_pointer->skipfield + *(group_pointer->skipfield); while(skipfield_pointer != beginning_point) { --skipfield_pointer; skipfield_pointer -= *skipfield_pointer; if (--distance == 0) { element_pointer = group_pointer->elements + (skipfield_pointer - group_pointer->skipfield); return; } } if (group_pointer->previous_group == NULL) { element_pointer = group_pointer->elements - 1; // If we've reached rend(), bound to that skipfield_pointer = group_pointer->skipfield - 1; return; } } group_pointer = group_pointer->previous_group; // Intermediary groups - at the start of this code block and the subsequent block, the position of the iterator is assumed to be the first non-erased element in the next group: while(static_cast(group_pointer->size) < distance) { if (group_pointer->previous_group == NULL) // bound to rend() { element_pointer = group_pointer->elements - 1; skipfield_pointer = group_pointer->skipfield - 1; return; } distance -= static_cast(group_pointer->size); group_pointer = group_pointer->previous_group; } // Final group (if not already reached) if (static_cast(group_pointer->size) == distance) { element_pointer = group_pointer->elements + *(group_pointer->skipfield); skipfield_pointer = group_pointer->skipfield + *(group_pointer->skipfield); return; } else if (group_pointer->free_list_head == std::numeric_limits::max()) { element_pointer = reinterpret_cast(group_pointer->skipfield) - distance; skipfield_pointer = (group_pointer->skipfield + group_pointer->capacity) - distance; return; } else { skipfield_pointer = group_pointer->skipfield + group_pointer->capacity; do { --skipfield_pointer; skipfield_pointer -= *skipfield_pointer; } while(--distance != 0); element_pointer = group_pointer->elements + (skipfield_pointer - group_pointer->skipfield); return; } } else if (distance < 0) { assert(!((element_pointer == (group_pointer->last_endpoint - 1) - *(group_pointer->skipfield + (group_pointer->last_endpoint - group_pointer->elements) - 1)) && group_pointer->next_group == NULL)); // Check that we're not already at rbegin() if (element_pointer != group_pointer->elements + *(group_pointer->skipfield)) // ie. != first non-erased element in group { if (group_pointer->free_list_head == std::numeric_limits::max()) // ie. if there are no erasures in the group { const difference_type distance_from_end = group_pointer->last_endpoint - element_pointer; if (distance < distance_from_end) { element_pointer += distance; skipfield_pointer += distance; return; } else if (group_pointer->next_group == NULL) // bound to rbegin() { element_pointer = group_pointer->last_endpoint - 1; // no erasures so we don't have to subtract skipfield value as we do below skipfield_pointer += distance_from_end - 1; return; } else { distance -= distance_from_end; } } else { const skipfield_pointer_type endpoint = skipfield_pointer + (group_pointer->last_endpoint - element_pointer); while(true) { ++skipfield_pointer; skipfield_pointer += *skipfield_pointer; --distance; if (skipfield_pointer == endpoint) { break; } else if (distance == 0) { element_pointer = group_pointer->elements + (skipfield_pointer - group_pointer->skipfield); return; } } if (group_pointer->next_group == NULL) // bound to rbegin() { --skipfield_pointer; element_pointer = (group_pointer->last_endpoint - 1) - *skipfield_pointer; skipfield_pointer -= *skipfield_pointer; return; } } group_pointer = group_pointer->next_group; if (distance == 0) { element_pointer = group_pointer->elements + *(group_pointer->skipfield); skipfield_pointer = group_pointer->skipfield + *(group_pointer->skipfield); return; } } // Intermediary groups - at the start of this code block and the subsequent block, the position of the iterator is assumed to be the first non-erased element in the current group, as a result of the previous code blocks: while(static_cast(group_pointer->size) <= distance) { if (group_pointer->next_group == NULL) // bound to rbegin() { skipfield_pointer = group_pointer->skipfield + (group_pointer->last_endpoint - group_pointer->elements) - 1; element_pointer = (group_pointer->last_endpoint - 1) - *skipfield_pointer; skipfield_pointer -= *skipfield_pointer; return; } else if ((distance -= group_pointer->size) == 0) { group_pointer = group_pointer->next_group; element_pointer = group_pointer->elements + *(group_pointer->skipfield); skipfield_pointer = group_pointer->skipfield + *(group_pointer->skipfield); return; } else { group_pointer = group_pointer->next_group; } } // Final group (if not already reached): if (group_pointer->free_list_head == std::numeric_limits::max()) // No erasures in this group, use straight pointer addition { element_pointer = group_pointer->elements + distance; skipfield_pointer = group_pointer->skipfield + distance; return; } else // ie. size > distance - safe to ignore endpoint check condition while incrementing: { skipfield_pointer = group_pointer->skipfield + *(group_pointer->skipfield); do { ++skipfield_pointer; skipfield_pointer += *skipfield_pointer; } while(--distance != 0); element_pointer = group_pointer->elements + (skipfield_pointer - group_pointer->skipfield); return; } return; } } public: #ifdef PLF_MOVE_SEMANTICS_SUPPORT // move constructors colony_reverse_iterator (colony_reverse_iterator &&source) PLF_NOEXCEPT: it(std::move(source.it)) {} colony_reverse_iterator (colony_reverse_iterator &&source) PLF_NOEXCEPT: it(std::move(source.it)) {} #endif }; // colony_reverse_iterator private: // Used to prevent fill-insert/constructor calls being mistakenly resolved to range-insert/constructor calls template struct plf_enable_if_c { typedef T type; }; template struct plf_enable_if_c {}; // Colony Member variables: iterator end_iterator, begin_iterator; group_pointer_type groups_with_erasures_list_head, // Head of the singly-linked list of groups which have erased-element memory locations available for re-use unused_groups_head; // Head of singly-linked list of groups retained by erase() or created by reserve() size_type total_size, total_capacity; struct ebco_pair2 : tuple_allocator_type // Packaging the element pointer allocator with a lesser-used member variable, for empty-base-class optimisation { skipfield_type min_group_capacity; explicit ebco_pair2(const skipfield_type min_elements) PLF_NOEXCEPT: min_group_capacity(min_elements) {} } tuple_allocator_pair; struct ebco_pair : group_allocator_type { skipfield_type max_group_capacity; explicit ebco_pair(const skipfield_type max_elements) PLF_NOEXCEPT: max_group_capacity(max_elements) {} } group_allocator_pair; // An adaptive minimum based around sizeof(element_type), sizeof(group) and sizeof(colony): #define PLF_MIN_BLOCK_CAPACITY (sizeof(aligned_element_type) * 8 > (sizeof(plf::colony) + sizeof(group)) * 2) ? 8 : (((sizeof(plf::colony) + sizeof(group)) * 2) / sizeof(aligned_element_type)) inline void check_capacities_conformance(plf::colony_limits capacities) const { if (capacities.min < 2 || capacities.min > capacities.max || capacities.max > std::numeric_limits::max()) { throw std::length_error("Supplied memory block capacities outside of allowable ranges"); } } #ifndef PLF_ALIGNMENT_SUPPORT inline void check_skipfield_conformance() const PLF_NOEXCEPT { PLF_STATIC_ASSERT(sizeof(element_type) >= sizeof(skipfield_type) * 2, "Element type is not large enough to accomodate colony requirements under C++98/03. Change to C++11 or above, or use a larger type."); // eg. under C++98/03, aligned_storage is not available, so sizeof(skipfield type) * 2 must be larger or equal to sizeof(element_type), otherwise the doubly-linked free lists of erased element indexes will not work correctly. So if you're storing chars, for example, and the skipfield type is unsigned short (currently default for this implementation), the compiler will flag you with this assert. Which means you cannot store char or unsigned char in colony under C++03, and if storing short or unsigned short you must use the priority::memory_use parameter in your template instantiation. Or just use C++11 and above. } #endif public: // Default constructor: colony(): allocator_type(allocator_type()), groups_with_erasures_list_head(NULL), unused_groups_head(NULL), total_size(0), total_capacity(0), tuple_allocator_pair(PLF_MIN_BLOCK_CAPACITY), group_allocator_pair(std::numeric_limits::max()) { #ifndef PLF_ALIGNMENT_SUPPORT check_skipfield_conformance(); #endif } explicit colony(const plf::colony_limits capacities): allocator_type(allocator_type()), groups_with_erasures_list_head(NULL), unused_groups_head(NULL), total_size(0), total_capacity(0), tuple_allocator_pair(static_cast(capacities.min)), group_allocator_pair(static_cast(capacities.max)) { #ifndef PLF_ALIGNMENT_SUPPORT check_skipfield_conformance(); #endif check_capacities_conformance(capacities); } // Default constructor (allocator-extended): explicit colony(const allocator_type &alloc): allocator_type(alloc), groups_with_erasures_list_head(NULL), unused_groups_head(NULL), total_size(0), total_capacity(0), tuple_allocator_pair(PLF_MIN_BLOCK_CAPACITY), group_allocator_pair(std::numeric_limits::max()) { #ifndef PLF_ALIGNMENT_SUPPORT check_skipfield_conformance(); #endif } explicit colony(const plf::colony_limits capacities, const allocator_type &alloc): allocator_type(alloc), groups_with_erasures_list_head(NULL), unused_groups_head(NULL), total_size(0), total_capacity(0), tuple_allocator_pair(static_cast(capacities.min)), group_allocator_pair(static_cast(capacities.max)) { #ifndef PLF_ALIGNMENT_SUPPORT check_skipfield_conformance(); #endif check_capacities_conformance(capacities); } // Copy constructor: colony(const colony &source): allocator_type(source), groups_with_erasures_list_head(NULL), unused_groups_head(NULL), total_size(0), total_capacity(0), tuple_allocator_pair(static_cast((source.tuple_allocator_pair.min_group_capacity > source.total_size) ? source.tuple_allocator_pair.min_group_capacity : ((source.total_size > source.group_allocator_pair.max_group_capacity) ? source.group_allocator_pair.max_group_capacity : source.total_size))), // min group size is set to value closest to total number of elements in source colony in order to not create unnecessary small groups in the range-insert below, then reverts to the original min group size afterwards. This effectively saves a call to reserve. group_allocator_pair(source.group_allocator_pair.max_group_capacity) { // can skip checking for skipfield conformance here as the skipfields must be equal between the destination and source, and source will have already had theirs checked. Same applies for other copy and move constructors below range_assign(source.begin_iterator, source.total_size); tuple_allocator_pair.min_group_capacity = source.tuple_allocator_pair.min_group_capacity; // reset to correct value for future clear() or erasures } // Copy constructor (allocator-extended): colony(const colony &source, const allocator_type &alloc): allocator_type(alloc), groups_with_erasures_list_head(NULL), unused_groups_head(NULL), total_size(0), total_capacity(0), tuple_allocator_pair(static_cast((source.tuple_allocator_pair.min_group_capacity > source.total_size) ? source.tuple_allocator_pair.min_group_capacity : ((source.total_size > source.group_allocator_pair.max_group_capacity) ? source.group_allocator_pair.max_group_capacity : source.total_size))), group_allocator_pair(source.group_allocator_pair.max_group_capacity) { range_assign(source.begin_iterator, source.total_size); tuple_allocator_pair.min_group_capacity = source.tuple_allocator_pair.min_group_capacity; } private: inline void blank() PLF_NOEXCEPT { #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (std::is_trivial::value && std::is_trivial::value && std::is_trivial::value) // if all pointer types are trivial, we can just nuke it from orbit with memset (NULL is always 0 in C++): { std::memset(static_cast(this), 0, offsetof(colony, tuple_allocator_pair)); } else #endif { end_iterator.group_pointer = NULL; end_iterator.element_pointer = NULL; end_iterator.skipfield_pointer = NULL; begin_iterator.group_pointer = NULL; begin_iterator.element_pointer = NULL; begin_iterator.skipfield_pointer = NULL; groups_with_erasures_list_head = NULL; unused_groups_head = NULL; total_size = 0; total_capacity = 0; } } public: #ifdef PLF_MOVE_SEMANTICS_SUPPORT // Move constructor: colony(colony &&source) PLF_NOEXCEPT: allocator_type(source), end_iterator(std::move(source.end_iterator)), begin_iterator(std::move(source.begin_iterator)), groups_with_erasures_list_head(std::move(source.groups_with_erasures_list_head)), unused_groups_head(std::move(source.unused_groups_head)), total_size(source.total_size), total_capacity(source.total_capacity), tuple_allocator_pair(source.tuple_allocator_pair.min_group_capacity), group_allocator_pair(source.group_allocator_pair.max_group_capacity) { assert(&source != this); source.blank(); } // Move constructor (allocator-extended): colony(colony &&source, const allocator_type &alloc): allocator_type(alloc), end_iterator(std::move(source.end_iterator)), begin_iterator(std::move(source.begin_iterator)), groups_with_erasures_list_head(std::move(source.groups_with_erasures_list_head)), unused_groups_head(std::move(source.unused_groups_head)), total_size(source.total_size), total_capacity(source.total_capacity), tuple_allocator_pair(source.tuple_allocator_pair.min_group_capacity), group_allocator_pair(source.group_allocator_pair.max_group_capacity) { assert(&source != this); source.blank(); } #endif // Fill constructor: colony(const size_type fill_number, const element_type &element, const plf::colony_limits capacities = plf::colony_limits(PLF_MIN_BLOCK_CAPACITY, std::numeric_limits::max()), const allocator_type &alloc = allocator_type()): allocator_type(alloc), groups_with_erasures_list_head(NULL), unused_groups_head(NULL), total_size(0), total_capacity(0), tuple_allocator_pair(static_cast(capacities.min)), group_allocator_pair(static_cast(capacities.max)) { #ifndef PLF_ALIGNMENT_SUPPORT check_skipfield_conformance(); #endif check_capacities_conformance(capacities); assign(fill_number, element); } // Default-value fill constructor: explicit colony(const size_type fill_number, const plf::colony_limits capacities = plf::colony_limits(PLF_MIN_BLOCK_CAPACITY, std::numeric_limits::max()), const allocator_type &alloc = allocator_type()): allocator_type(alloc), groups_with_erasures_list_head(NULL), unused_groups_head(NULL), total_size(0), total_capacity(0), tuple_allocator_pair(static_cast(capacities.min)), group_allocator_pair(static_cast(capacities.max)) { #ifndef PLF_ALIGNMENT_SUPPORT check_skipfield_conformance(); #endif check_capacities_conformance(capacities); assign(fill_number, element_type()); } // Range constructor: template colony(const typename plf_enable_if_c::is_integer, iterator_type>::type &first, const iterator_type &last, const plf::colony_limits capacities = plf::colony_limits(PLF_MIN_BLOCK_CAPACITY, std::numeric_limits::max()), const allocator_type &alloc = allocator_type()): allocator_type(alloc), groups_with_erasures_list_head(NULL), unused_groups_head(NULL), total_size(0), total_capacity(0), tuple_allocator_pair(static_cast(capacities.min)), group_allocator_pair(static_cast(capacities.max)) { #ifndef PLF_ALIGNMENT_SUPPORT check_skipfield_conformance(); #endif check_capacities_conformance(capacities); assign(first, last); } // Initializer-list constructor: #ifdef PLF_INITIALIZER_LIST_SUPPORT colony(const std::initializer_list &element_list, const plf::colony_limits capacities = plf::colony_limits(PLF_MIN_BLOCK_CAPACITY, std::numeric_limits::max()), const allocator_type &alloc = allocator_type()): allocator_type(alloc), groups_with_erasures_list_head(NULL), unused_groups_head(NULL), total_size(0), total_capacity(0), tuple_allocator_pair(static_cast(capacities.min)), group_allocator_pair(static_cast(capacities.max)) { #ifndef PLF_ALIGNMENT_SUPPORT check_skipfield_conformance(); #endif check_capacities_conformance(capacities); range_assign(element_list.begin(), static_cast(element_list.size())); } #endif inline PLF_FORCE_INLINE iterator begin() PLF_NOEXCEPT { return begin_iterator; } inline PLF_FORCE_INLINE const_iterator begin() const PLF_NOEXCEPT { return begin_iterator; } inline PLF_FORCE_INLINE iterator end() PLF_NOEXCEPT { return end_iterator; } inline PLF_FORCE_INLINE const_iterator end() const PLF_NOEXCEPT { return end_iterator; } inline PLF_FORCE_INLINE const_iterator cbegin() const PLF_NOEXCEPT { return begin_iterator; } inline PLF_FORCE_INLINE const_iterator cend() const PLF_NOEXCEPT { return end_iterator; } inline reverse_iterator rbegin() PLF_NOEXCEPT { return (end_iterator.group_pointer != NULL) ? ++reverse_iterator(end_iterator) : reverse_iterator(begin_iterator.group_pointer, begin_iterator.element_pointer - 1, begin_iterator.skipfield_pointer - 1); } inline reverse_iterator rend() PLF_NOEXCEPT { return reverse_iterator(begin_iterator.group_pointer, begin_iterator.element_pointer - 1, begin_iterator.skipfield_pointer - 1); } inline const_reverse_iterator crbegin() const PLF_NOEXCEPT { return (end_iterator.group_pointer != NULL) ? ++const_reverse_iterator(end_iterator) : const_reverse_iterator(begin_iterator.group_pointer, begin_iterator.element_pointer - 1, begin_iterator.skipfield_pointer - 1); } inline const_reverse_iterator crend() const PLF_NOEXCEPT { return const_reverse_iterator(begin_iterator.group_pointer, begin_iterator.element_pointer - 1, begin_iterator.skipfield_pointer - 1); } ~colony() PLF_NOEXCEPT { destroy_all_data(); } private: group_pointer_type allocate_new_group(const skipfield_type elements_per_group, group_pointer_type const previous = NULL) { group_pointer_type const new_group = PLF_ALLOCATE(group_allocator_type, group_allocator_pair, 1, 0); try { #ifdef PLF_VARIADICS_SUPPORT PLF_CONSTRUCT(group_allocator_type, group_allocator_pair, new_group, elements_per_group, previous); #else PLF_CONSTRUCT(group_allocator_type, group_allocator_pair, new_group, group(elements_per_group, previous)); #endif } catch (...) { PLF_DEALLOCATE(group_allocator_type, group_allocator_pair, new_group, 1); throw; } return new_group; } inline void deallocate_group(group_pointer_type const the_group) PLF_NOEXCEPT { PLF_DESTROY(group_allocator_type, group_allocator_pair, the_group); PLF_DEALLOCATE(group_allocator_type, group_allocator_pair, the_group, 1); } void destroy_all_data() PLF_NOEXCEPT { if (begin_iterator.group_pointer != NULL) { end_iterator.group_pointer->next_group = unused_groups_head; #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (!std::is_trivially_destructible::value) #endif // If compiler doesn't support traits, iterate regardless - trivial destructors will not be called, hopefully compiler will optimise the 'destruct' loop out for POD types { if (total_size != 0) { while (true) // Erase elements without bothering to update skipfield - much faster: { const aligned_pointer_type end_pointer = begin_iterator.group_pointer->last_endpoint; do { PLF_DESTROY(allocator_type, *this, reinterpret_cast(begin_iterator.element_pointer)); begin_iterator.element_pointer += static_cast(*++begin_iterator.skipfield_pointer) + 1u; begin_iterator.skipfield_pointer += *begin_iterator.skipfield_pointer; } while(begin_iterator.element_pointer != end_pointer); // ie. beyond end of available data const group_pointer_type next_group = begin_iterator.group_pointer->next_group; deallocate_group(begin_iterator.group_pointer); begin_iterator.group_pointer = next_group; if (next_group == unused_groups_head) { break; } begin_iterator.element_pointer = next_group->elements + *(next_group->skipfield); begin_iterator.skipfield_pointer = next_group->skipfield + *(next_group->skipfield); } } } while (begin_iterator.group_pointer != NULL) { const group_pointer_type next_group = begin_iterator.group_pointer->next_group; deallocate_group(begin_iterator.group_pointer); begin_iterator.group_pointer = next_group; } } } void initialize(const skipfield_type first_group_size) { end_iterator.group_pointer = begin_iterator.group_pointer = allocate_new_group(first_group_size); end_iterator.element_pointer = begin_iterator.element_pointer = begin_iterator.group_pointer->elements; end_iterator.skipfield_pointer = begin_iterator.skipfield_pointer = begin_iterator.group_pointer->skipfield; total_capacity = first_group_size; } void update_skipblock(const iterator &new_location, const skipfield_type prev_free_list_index) PLF_NOEXCEPT { const skipfield_type new_value = static_cast(*(new_location.skipfield_pointer) - 1); if (new_value != 0) // ie. skipfield was not 1, ie. a single-node skipblock, with no additional nodes to update { // set (new) start and (original) end of skipblock to new value: *(new_location.skipfield_pointer + new_value) = *(new_location.skipfield_pointer + 1) = new_value; // transfer free list node to new start node: ++(groups_with_erasures_list_head->free_list_head); if (prev_free_list_index != std::numeric_limits::max()) // ie. not the tail free list node { *(reinterpret_cast(new_location.group_pointer->elements + prev_free_list_index) + 1) = groups_with_erasures_list_head->free_list_head; } *(reinterpret_cast(new_location.element_pointer + 1)) = prev_free_list_index; *(reinterpret_cast(new_location.element_pointer + 1) + 1) = std::numeric_limits::max(); } else { groups_with_erasures_list_head->free_list_head = prev_free_list_index; if (prev_free_list_index != std::numeric_limits::max()) // ie. not the last free list node { *(reinterpret_cast(new_location.group_pointer->elements + prev_free_list_index) + 1) = std::numeric_limits::max(); } else // remove this group from the list of groups with erasures { groups_with_erasures_list_head = groups_with_erasures_list_head->erasures_list_next_group; } } *(new_location.skipfield_pointer) = 0; ++(new_location.group_pointer->size); if (new_location.group_pointer == begin_iterator.group_pointer && new_location.element_pointer < begin_iterator.element_pointer) { /* ie. begin_iterator was moved forwards as the result of an erasure at some point, this erased element is before the current begin, hence, set current begin iterator to this element */ begin_iterator = new_location; } ++total_size; } public: iterator insert(const element_type &element) { if (end_iterator.element_pointer != NULL) { if (groups_with_erasures_list_head == NULL) // ie. there are no erased elements and end_iterator is not at end of current final group { if (end_iterator.element_pointer != reinterpret_cast(end_iterator.group_pointer->skipfield)) { const iterator return_iterator = end_iterator; /* Make copy for return before modifying end_iterator */ #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (std::is_nothrow_copy_constructible::value) { // For no good reason this compiles to ridiculously faster code under GCC 5-9 in raw small struct tests with large N: PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(end_iterator.element_pointer++), element); end_iterator.group_pointer->last_endpoint = end_iterator.element_pointer; } else #endif { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(end_iterator.element_pointer), element); end_iterator.group_pointer->last_endpoint = ++end_iterator.element_pointer; // Shift the addition to the second operation, avoiding a try-catch block if an exception is thrown during construction } ++(end_iterator.group_pointer->size); ++end_iterator.skipfield_pointer; ++total_size; return return_iterator; // return value before incrementation } group_pointer_type next_group; if (unused_groups_head == NULL) { const skipfield_type new_group_size = (total_size < static_cast(group_allocator_pair.max_group_capacity)) ? static_cast(total_size) : group_allocator_pair.max_group_capacity; next_group = allocate_new_group(new_group_size, end_iterator.group_pointer); #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (std::is_nothrow_copy_constructible::value) { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(next_group->elements), element); } else #endif { try { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(next_group->elements), element); } catch (...) { deallocate_group(next_group); throw; } } total_capacity += new_group_size; } else { next_group = unused_groups_head; PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(next_group->elements), element); unused_groups_head = next_group->next_group; next_group->reset(1, NULL, end_iterator.group_pointer, end_iterator.group_pointer->group_number + 1u); } end_iterator.group_pointer->next_group = next_group; end_iterator.group_pointer = next_group; end_iterator.element_pointer = next_group->last_endpoint; end_iterator.skipfield_pointer = next_group->skipfield + 1; ++total_size; return iterator(next_group, next_group->elements, next_group->skipfield); /* returns value before incrementation */ } else // ie. there are erased elements, reuse previous-erased element locations { iterator new_location(groups_with_erasures_list_head, groups_with_erasures_list_head->elements + groups_with_erasures_list_head->free_list_head, groups_with_erasures_list_head->skipfield + groups_with_erasures_list_head->free_list_head); // We always reuse the element at the start of the skipblock, this is also where the free-list information for that skipblock is stored. Get the previous free-list node's index from this memory space, before we write to our element to it. 'Next' index is always the free_list_head (as represented by the maximum value of the skipfield type) here so we don't need to get it: const skipfield_type prev_free_list_index = *(reinterpret_cast(new_location.element_pointer)); PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(new_location.element_pointer), element); update_skipblock(new_location, prev_free_list_index); return new_location; } } else // ie. newly-constructed colony, no insertions yet and no groups { initialize(tuple_allocator_pair.min_group_capacity); #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (std::is_nothrow_copy_constructible::value) { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(end_iterator.element_pointer++), element); } else #endif { try { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(end_iterator.element_pointer++), element); } catch (...) { clear(); throw; } } ++end_iterator.skipfield_pointer; total_size = 1; return begin_iterator; } } #ifdef PLF_MOVE_SEMANTICS_SUPPORT iterator insert(element_type &&element) // The move-insert function is near-identical to the regular insert function, with the exception of the element construction method and is_nothrow tests. { if (end_iterator.element_pointer != NULL) { if (groups_with_erasures_list_head == NULL) { if (end_iterator.element_pointer != reinterpret_cast(end_iterator.group_pointer->skipfield)) { const iterator return_iterator = end_iterator; #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (std::is_nothrow_move_constructible::value) { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(end_iterator.element_pointer++), std::move(element)); end_iterator.group_pointer->last_endpoint = end_iterator.element_pointer; } else #endif { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(end_iterator.element_pointer), std::move(element)); end_iterator.group_pointer->last_endpoint = ++end_iterator.element_pointer; } ++(end_iterator.group_pointer->size); ++end_iterator.skipfield_pointer; ++total_size; return return_iterator; } group_pointer_type next_group; if (unused_groups_head == NULL) { const skipfield_type new_group_size = (total_size < static_cast(group_allocator_pair.max_group_capacity)) ? static_cast(total_size) : group_allocator_pair.max_group_capacity; next_group = allocate_new_group(new_group_size, end_iterator.group_pointer); #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (std::is_nothrow_move_constructible::value) { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(next_group->elements), std::move(element)); } else #endif { try { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(next_group->elements), std::move(element)); } catch (...) { deallocate_group(next_group); throw; } } total_capacity += new_group_size; } else { next_group = unused_groups_head; PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(next_group->elements), std::move(element)); unused_groups_head = next_group->next_group; next_group->reset(1, NULL, end_iterator.group_pointer, end_iterator.group_pointer->group_number + 1u); } end_iterator.group_pointer->next_group = next_group; end_iterator.group_pointer = next_group; end_iterator.element_pointer = next_group->last_endpoint; end_iterator.skipfield_pointer = next_group->skipfield + 1; ++total_size; return iterator(next_group, next_group->elements, next_group->skipfield); /* returns value before incrementation */ } else { iterator new_location(groups_with_erasures_list_head, groups_with_erasures_list_head->elements + groups_with_erasures_list_head->free_list_head, groups_with_erasures_list_head->skipfield + groups_with_erasures_list_head->free_list_head); const skipfield_type prev_free_list_index = *(reinterpret_cast(new_location.element_pointer)); PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(new_location.element_pointer), std::move(element)); update_skipblock(new_location, prev_free_list_index); return new_location; } } else { initialize(tuple_allocator_pair.min_group_capacity); #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (std::is_nothrow_move_constructible::value) { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(end_iterator.element_pointer++), std::move(element)); } else #endif { try { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(end_iterator.element_pointer++), std::move(element)); } catch (...) { clear(); throw; } } ++end_iterator.skipfield_pointer; total_size = 1; return begin_iterator; } } #endif #ifdef PLF_VARIADICS_SUPPORT template iterator emplace(arguments &&... parameters) // The emplace function is near-identical to the regular insert function, with the exception of the element construction method, and change to is_nothrow tests. { if (end_iterator.element_pointer != NULL) { if (groups_with_erasures_list_head == NULL) { if (end_iterator.element_pointer != reinterpret_cast(end_iterator.group_pointer->skipfield)) { const iterator return_iterator = end_iterator; #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (std::is_nothrow_constructible::value) { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(end_iterator.element_pointer++), std::forward(parameters) ...); end_iterator.group_pointer->last_endpoint = end_iterator.element_pointer; } else #endif { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(end_iterator.element_pointer), std::forward(parameters) ...); end_iterator.group_pointer->last_endpoint = ++end_iterator.element_pointer; } ++(end_iterator.group_pointer->size); ++end_iterator.skipfield_pointer; ++total_size; return return_iterator; } group_pointer_type next_group; if (unused_groups_head == NULL) { const skipfield_type new_group_size = (total_size < static_cast(group_allocator_pair.max_group_capacity)) ? static_cast(total_size) : group_allocator_pair.max_group_capacity; next_group = allocate_new_group(new_group_size, end_iterator.group_pointer); #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (std::is_nothrow_constructible::value) { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(next_group->elements), std::forward(parameters) ...); } else #endif { try { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(next_group->elements), std::forward(parameters) ...); } catch (...) { deallocate_group(next_group); throw; } } total_capacity += new_group_size; } else { next_group = unused_groups_head; PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(next_group->elements), std::forward(parameters) ...); unused_groups_head = next_group->next_group; next_group->reset(1, NULL, end_iterator.group_pointer, end_iterator.group_pointer->group_number + 1u); } end_iterator.group_pointer->next_group = next_group; end_iterator.group_pointer = next_group; end_iterator.element_pointer = next_group->last_endpoint; end_iterator.skipfield_pointer = next_group->skipfield + 1; ++total_size; return iterator(next_group, next_group->elements, next_group->skipfield); /* returns value before incrementation */ } else { iterator new_location(groups_with_erasures_list_head, groups_with_erasures_list_head->elements + groups_with_erasures_list_head->free_list_head, groups_with_erasures_list_head->skipfield + groups_with_erasures_list_head->free_list_head); const skipfield_type prev_free_list_index = *(reinterpret_cast(new_location.element_pointer)); PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(new_location.element_pointer), std::forward(parameters) ...); update_skipblock(new_location, prev_free_list_index); return new_location; } } else { initialize(tuple_allocator_pair.min_group_capacity); #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (std::is_nothrow_constructible::value) { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(end_iterator.element_pointer++), std::forward(parameters) ...); } else #endif { try { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(end_iterator.element_pointer++), std::forward(parameters) ...); } catch (...) { clear(); throw; } } ++end_iterator.skipfield_pointer; total_size = 1; return begin_iterator; } } #endif private: // For catch blocks in fill() and range_fill() void recover_from_partial_fill() { #ifdef PLF_TYPE_TRAITS_SUPPORT // try to ensure this code will not be generated if this function is not called in fill() or range_fill() if PLF_CONSTEXPR (!std::is_nothrow_copy_constructible::value) #endif { end_iterator.group_pointer->last_endpoint = end_iterator.element_pointer; const skipfield_type elements_constructed_before_exception = static_cast(end_iterator.element_pointer - end_iterator.group_pointer->elements); end_iterator.group_pointer->size = elements_constructed_before_exception; end_iterator.skipfield_pointer = end_iterator.group_pointer->skipfield + elements_constructed_before_exception; total_size += elements_constructed_before_exception; unused_groups_head = end_iterator.group_pointer->next_group; end_iterator.group_pointer->next_group = NULL; } } void fill(const element_type &element, const skipfield_type size) { #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (std::is_nothrow_copy_constructible::value) { if PLF_CONSTEXPR (std::is_trivially_copyable::value && std::is_trivially_copy_constructible::value) // ie. we can get away with using the cheaper fill_n here if there is no chance of an exception being thrown: { #ifdef PLF_ALIGNMENT_SUPPORT if PLF_CONSTEXPR (sizeof(aligned_element_type) != sizeof(element_type)) { alignas (alignof(aligned_element_type)) element_type aligned_copy = element; // to avoid potentially violating memory boundaries in line below, create an initial copy object of same (but aligned) type std::fill_n(end_iterator.element_pointer, size, *(reinterpret_cast(&aligned_copy))); } else #endif { std::fill_n(reinterpret_cast(end_iterator.element_pointer), size, element); } end_iterator.element_pointer += size; } else // If at least nothrow_constructible, can remove the large block of 'catch' code below { const aligned_pointer_type fill_end = end_iterator.element_pointer + size; do { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(end_iterator.element_pointer), element); } while (++end_iterator.element_pointer != fill_end); } } else #endif { const aligned_pointer_type fill_end = end_iterator.element_pointer + size; do { try { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(end_iterator.element_pointer), element); } catch (...) { recover_from_partial_fill(); throw; } } while (++end_iterator.element_pointer != fill_end); } total_size += size; } // For catch blocks in range_fill_skipblock and fill_skipblock void recover_from_partial_skipblock_fill(aligned_pointer_type const location, const aligned_pointer_type current_location, skipfield_pointer_type const skipfield_pointer, const skipfield_type prev_free_list_node) { #ifdef PLF_TYPE_TRAITS_SUPPORT // try to ensure this code will not be generated if this function is not called in fill_skipblock or range_fill_skipblock if PLF_CONSTEXPR (!std::is_nothrow_copy_constructible::value) #endif { // Reconstruct existing skipblock and free-list indexes to reflect partially-reused skipblock: const skipfield_type elements_constructed_before_exception = static_cast((current_location - 1) - location); groups_with_erasures_list_head->size = static_cast(groups_with_erasures_list_head->size + elements_constructed_before_exception); total_size += elements_constructed_before_exception; std::memset(skipfield_pointer, 0, elements_constructed_before_exception * sizeof(skipfield_type)); *(reinterpret_cast(location + elements_constructed_before_exception)) = prev_free_list_node; *(reinterpret_cast(location + elements_constructed_before_exception) + 1) = std::numeric_limits::max(); const skipfield_type new_skipblock_head_index = static_cast((location - groups_with_erasures_list_head->elements) + elements_constructed_before_exception); groups_with_erasures_list_head->free_list_head = new_skipblock_head_index; if (prev_free_list_node != std::numeric_limits::max()) { *(reinterpret_cast(groups_with_erasures_list_head->elements + prev_free_list_node) + 1) = new_skipblock_head_index; } } } void fill_skipblock(const element_type &element, aligned_pointer_type const location, skipfield_pointer_type const skipfield_pointer, const skipfield_type size) { #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (std::is_nothrow_copy_constructible::value) { if PLF_CONSTEXPR (std::is_trivially_copyable::value && std::is_trivially_copy_constructible::value) { #ifdef PLF_ALIGNMENT_SUPPORT if PLF_CONSTEXPR (sizeof(aligned_element_type) != sizeof(element_type)) { alignas (alignof(aligned_element_type)) element_type aligned_copy = element; std::fill_n(location, size, *(reinterpret_cast(&aligned_copy))); } else #endif { std::fill_n(reinterpret_cast(location), size, element); } } else { const aligned_pointer_type fill_end = location + size; for (aligned_pointer_type current_location = location; current_location != fill_end; ++current_location) { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(current_location), element); } } } else #endif { const aligned_pointer_type fill_end = location + size; const skipfield_type prev_free_list_node = *(reinterpret_cast(location)); // in case of exception, grabbing indexes before free_list node is reused for (aligned_pointer_type current_location = location; current_location != fill_end; ++current_location) { try { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(current_location), element); } catch (...) { recover_from_partial_skipblock_fill(location, current_location, skipfield_pointer, prev_free_list_node); throw; } } } std::memset(skipfield_pointer, 0, size * sizeof(skipfield_type)); // reset skipfield nodes within skipblock to 0 groups_with_erasures_list_head->size = static_cast(groups_with_erasures_list_head->size + size); total_size += size; } void fill_unused_groups(size_type size, const element_type &element, size_type group_number, group_pointer_type previous_group, const group_pointer_type current_group) { end_iterator.group_pointer = current_group; for (; end_iterator.group_pointer->capacity < size; end_iterator.group_pointer = end_iterator.group_pointer->next_group) { const skipfield_type capacity = end_iterator.group_pointer->capacity; end_iterator.group_pointer->reset(capacity, end_iterator.group_pointer->next_group, previous_group, group_number++); previous_group = end_iterator.group_pointer; size -= static_cast(capacity); end_iterator.element_pointer = end_iterator.group_pointer->elements; fill(element, capacity); } // Deal with final group (partial fill) unused_groups_head = end_iterator.group_pointer->next_group; end_iterator.group_pointer->reset(static_cast(size), NULL, previous_group, group_number); end_iterator.element_pointer = end_iterator.group_pointer->elements; end_iterator.skipfield_pointer = end_iterator.group_pointer->skipfield + size; fill(element, static_cast(size)); } public: // Fill insert void insert(size_type size, const element_type &element) { if (size == 0) { return; } else if (size == 1) { insert(element); return; } if (total_size == 0) { assign(size, element); return; } reserve(total_size + size); // Use up erased locations if available: while(groups_with_erasures_list_head != NULL) // skipblock loop: breaks when colony is exhausted of reusable skipblocks, or returns if size == 0 { aligned_pointer_type const element_pointer = groups_with_erasures_list_head->elements + groups_with_erasures_list_head->free_list_head; skipfield_pointer_type const skipfield_pointer = groups_with_erasures_list_head->skipfield + groups_with_erasures_list_head->free_list_head; const skipfield_type skipblock_size = *skipfield_pointer; if (groups_with_erasures_list_head == begin_iterator.group_pointer && element_pointer < begin_iterator.element_pointer) { begin_iterator.element_pointer = element_pointer; begin_iterator.skipfield_pointer = skipfield_pointer; } if (skipblock_size <= size) { groups_with_erasures_list_head->free_list_head = *(reinterpret_cast(element_pointer)); // set free list head to previous free list node fill_skipblock(element, element_pointer, skipfield_pointer, skipblock_size); size -= skipblock_size; if (groups_with_erasures_list_head->free_list_head != std::numeric_limits::max()) // ie. there are more skipblocks to be filled in this group { *(reinterpret_cast(groups_with_erasures_list_head->elements + groups_with_erasures_list_head->free_list_head) + 1) = std::numeric_limits::max(); // set 'next' index of new free list head to 'end' (numeric max) } else { groups_with_erasures_list_head = groups_with_erasures_list_head->erasures_list_next_group; // change groups } if (size == 0) { return; } } else // skipblock is larger than remaining number of elements { const skipfield_type prev_index = *(reinterpret_cast(element_pointer)); // save before element location is overwritten fill_skipblock(element, element_pointer, skipfield_pointer, static_cast(size)); const skipfield_type new_skipblock_size = static_cast(skipblock_size - size); // Update skipfield (earlier nodes already memset'd in fill_skipblock function): *(skipfield_pointer + size) = new_skipblock_size; *(skipfield_pointer + skipblock_size - 1) = new_skipblock_size; groups_with_erasures_list_head->free_list_head = static_cast(groups_with_erasures_list_head->free_list_head + size); // set free list head to new start node // Update free list with new head: *(reinterpret_cast(element_pointer + size)) = prev_index; *(reinterpret_cast(element_pointer + size) + 1) = std::numeric_limits::max(); if (prev_index != std::numeric_limits::max()) { *(reinterpret_cast(groups_with_erasures_list_head->elements + prev_index) + 1) = groups_with_erasures_list_head->free_list_head; // set 'next' index of previous skipblock to new start of skipblock } return; } } // Use up remaining available element locations in end group: // This variable is either the remaining capacity of the group or the number of elements yet to be filled, whichever is smaller: const skipfield_type group_remainder = (static_cast( reinterpret_cast(end_iterator.group_pointer->skipfield) - end_iterator.element_pointer) >= size) ? static_cast(size) : static_cast(reinterpret_cast(end_iterator.group_pointer->skipfield) - end_iterator.element_pointer); if (group_remainder != 0) { fill(element, group_remainder); end_iterator.group_pointer->last_endpoint = end_iterator.element_pointer; end_iterator.group_pointer->size = static_cast(end_iterator.group_pointer->size + group_remainder); if (size == group_remainder) // Ie. remaining capacity was >= remaining elements to be filled { end_iterator.skipfield_pointer = end_iterator.group_pointer->skipfield + end_iterator.group_pointer->size; return; } size -= group_remainder; } // Use unused groups: end_iterator.group_pointer->next_group = unused_groups_head; fill_unused_groups(size, element, end_iterator.group_pointer->group_number + 1, end_iterator.group_pointer, unused_groups_head); } private: template iterator_type range_fill(iterator_type it, const skipfield_type size) { #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (std::is_nothrow_copy_constructible::value) { const aligned_pointer_type fill_end = end_iterator.element_pointer + size; do { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(end_iterator.element_pointer), *it++); } while (++end_iterator.element_pointer != fill_end); } else #endif { const aligned_pointer_type fill_end = end_iterator.element_pointer + size; do { try { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(end_iterator.element_pointer), *it++); } catch (...) { recover_from_partial_fill(); throw; } } while (++end_iterator.element_pointer != fill_end); } total_size += size; return it; } template iterator_type range_fill_skipblock(iterator_type it, aligned_pointer_type const location, skipfield_pointer_type const skipfield_pointer, const skipfield_type size) { const aligned_pointer_type fill_end = location + size; #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (std::is_nothrow_copy_constructible::value) { for (aligned_pointer_type current_location = location; current_location != fill_end; ++current_location) { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(current_location), *it++); } } else #endif { const skipfield_type prev_free_list_node = *(reinterpret_cast(location)); // in case of exception, grabbing indexes before free_list node is reused for (aligned_pointer_type current_location = location; current_location != fill_end; ++current_location) { try { PLF_CONSTRUCT(allocator_type, *this, reinterpret_cast(current_location), *it++); } catch (...) { recover_from_partial_skipblock_fill(location, current_location, skipfield_pointer, prev_free_list_node); throw; } } } std::memset(skipfield_pointer, 0, size * sizeof(skipfield_type)); // reset skipfield nodes within skipblock to 0 groups_with_erasures_list_head->size = static_cast(groups_with_erasures_list_head->size + size); total_size += size; return it; } template void range_fill_unused_groups(size_type size, iterator_type it, size_type group_number, group_pointer_type previous_group, const group_pointer_type current_group) { end_iterator.group_pointer = current_group; for (; end_iterator.group_pointer->capacity < size; end_iterator.group_pointer = end_iterator.group_pointer->next_group) { const skipfield_type capacity = end_iterator.group_pointer->capacity; end_iterator.group_pointer->reset(capacity, end_iterator.group_pointer->next_group, previous_group, group_number++); previous_group = end_iterator.group_pointer; size -= static_cast(capacity); end_iterator.element_pointer = end_iterator.group_pointer->elements; it = range_fill(it, capacity); } // Deal with final group (partial fill) unused_groups_head = end_iterator.group_pointer->next_group; end_iterator.group_pointer->reset(static_cast(size), NULL, previous_group, group_number); end_iterator.element_pointer = end_iterator.group_pointer->elements; end_iterator.skipfield_pointer = end_iterator.group_pointer->skipfield + size; range_fill(it, static_cast(size)); } template void range_insert (iterator_type it, size_type size) // this is near-identical to the fill insert, with the only alteration being incrementing an iterator for construction, rather than using a const element. And the fill etc function calls are changed to range_fill to match this pattern. comments have been removed, see fill insert for code explanations { if (size == 0) { return; } else if (size == 1) { insert(*it); return; } if (total_size == 0) { range_assign(it, size); return; } reserve(total_size + size); while(groups_with_erasures_list_head != NULL) { aligned_pointer_type const element_pointer = groups_with_erasures_list_head->elements + groups_with_erasures_list_head->free_list_head; skipfield_pointer_type const skipfield_pointer = groups_with_erasures_list_head->skipfield + groups_with_erasures_list_head->free_list_head; const skipfield_type skipblock_size = *skipfield_pointer; if (groups_with_erasures_list_head == begin_iterator.group_pointer && element_pointer < begin_iterator.element_pointer) { begin_iterator.element_pointer = element_pointer; begin_iterator.skipfield_pointer = skipfield_pointer; } if (skipblock_size <= size) { groups_with_erasures_list_head->free_list_head = *(reinterpret_cast(element_pointer)); it = range_fill_skipblock(it, element_pointer, skipfield_pointer, skipblock_size); size -= skipblock_size; if (groups_with_erasures_list_head->free_list_head != std::numeric_limits::max()) { *(reinterpret_cast(groups_with_erasures_list_head->elements + groups_with_erasures_list_head->free_list_head) + 1) = std::numeric_limits::max(); } else { groups_with_erasures_list_head = groups_with_erasures_list_head->erasures_list_next_group; } if (size == 0) { return; } } else { const skipfield_type prev_index = *(reinterpret_cast(element_pointer)); it = range_fill_skipblock(it, element_pointer, skipfield_pointer, static_cast(size)); const skipfield_type new_skipblock_size = static_cast(skipblock_size - size); *(skipfield_pointer + size) = new_skipblock_size; *(skipfield_pointer + skipblock_size - 1) = new_skipblock_size; groups_with_erasures_list_head->free_list_head = static_cast(groups_with_erasures_list_head->free_list_head + size); *(reinterpret_cast(element_pointer + size)) = prev_index; *(reinterpret_cast(element_pointer + size) + 1) = std::numeric_limits::max(); if (prev_index != std::numeric_limits::max()) { *(reinterpret_cast(groups_with_erasures_list_head->elements + prev_index) + 1) = groups_with_erasures_list_head->free_list_head; } return; } } const skipfield_type group_remainder = (static_cast( reinterpret_cast(end_iterator.group_pointer->skipfield) - end_iterator.element_pointer) >= size) ? static_cast(size) : static_cast(reinterpret_cast(end_iterator.group_pointer->skipfield) - end_iterator.element_pointer); if (group_remainder != 0) { it = range_fill(it, group_remainder); end_iterator.group_pointer->last_endpoint = end_iterator.element_pointer; end_iterator.group_pointer->size = static_cast(end_iterator.group_pointer->size + group_remainder); if (size == group_remainder) { end_iterator.skipfield_pointer = end_iterator.group_pointer->skipfield + end_iterator.group_pointer->size; return; } size -= group_remainder; } end_iterator.group_pointer->next_group = unused_groups_head; range_fill_unused_groups(size, it, end_iterator.group_pointer->group_number + 1, end_iterator.group_pointer, unused_groups_head); } public: // Range insert: template inline void insert (const typename plf_enable_if_c::is_integer, iterator_type>::type first, const iterator_type last) { using std::distance; range_insert(first, static_cast(distance(first, last))); } // Range insert for differing iterator types eg. sentinels: #ifdef PLF_CPP20_SUPPORT template requires (!std::same_as && std::equality_comparable_with && !std::integral && !std::integral) inline void insert (const iterator_type1 first, const iterator_type2 last) { size_type distance = 0; for(iterator_type1 current = first; current != last; ++current, ++distance) {}; range_insert(first, distance); } #endif // Range insert, move_iterator overload: #ifdef PLF_MOVE_SEMANTICS_SUPPORT template inline void insert (const std::move_iterator first, const std::move_iterator last) { using std::distance; range_insert(first, static_cast(distance(first.base(),last.base()))); } #endif // Initializer-list insert #ifdef PLF_INITIALIZER_LIST_SUPPORT inline void insert (const std::initializer_list &element_list) { range_insert(element_list.begin(), static_cast(element_list.size())); } #endif private: inline PLF_FORCE_INLINE void update_subsequent_group_numbers(group_pointer_type current_group) PLF_NOEXCEPT { do { --(current_group->group_number); current_group = current_group->next_group; } while (current_group != NULL); } void remove_from_groups_with_erasures_list(const group_pointer_type group_to_remove) PLF_NOEXCEPT { if (group_to_remove == groups_with_erasures_list_head) { groups_with_erasures_list_head = groups_with_erasures_list_head->erasures_list_next_group; return; } group_pointer_type previous_group = groups_with_erasures_list_head, current_group = groups_with_erasures_list_head->erasures_list_next_group; while (group_to_remove != current_group) { previous_group = current_group; current_group = current_group->erasures_list_next_group; } previous_group->erasures_list_next_group = current_group->erasures_list_next_group; } inline void reset_only_group_left(group_pointer_type const group_pointer) PLF_NOEXCEPT { groups_with_erasures_list_head = NULL; group_pointer->reset(0, NULL, NULL, 0); // Reset begin and end iterators: end_iterator.element_pointer = begin_iterator.element_pointer = group_pointer->last_endpoint; end_iterator.skipfield_pointer = begin_iterator.skipfield_pointer = group_pointer->skipfield; } inline PLF_FORCE_INLINE void add_group_to_unused_groups_list(group * const group_pointer) PLF_NOEXCEPT { group_pointer->next_group = unused_groups_head; unused_groups_head = group_pointer; } public: // must return iterator to subsequent non-erased element (or end()), in case the group containing the element which the iterator points to becomes empty after the erasure, and is thereafter removed from the colony chain, making the current iterator invalid and unusable in a ++ operation: iterator erase(const const_iterator it) // if uninitialized/invalid iterator supplied, function could generate an exception { assert(total_size != 0); assert(it.group_pointer != NULL); // ie. not uninitialized iterator assert(it.element_pointer != it.group_pointer->last_endpoint); // ie. != end() assert(*(it.skipfield_pointer) == 0); // ie. element pointed to by iterator has not been erased previously #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (!std::is_trivially_destructible::value) // This if-statement should be removed by the compiler on resolution of element_type. For some optimizing compilers this step won't be necessary (for MSVC 2013 it makes a difference) #endif { PLF_DESTROY(allocator_type, *this, reinterpret_cast(it.element_pointer)); // Destruct element } --total_size; if (it.group_pointer->size-- != 1) // ie. non-empty group at this point in time, don't consolidate - optimization note: GCC optimizes postfix - 1 comparison better than prefix - 1 comparison in many cases. { // Code logic for following section: // --------------------------------- // If current skipfield node has no skipped node on either side, continue as usual // If node only has skipped node on left, set current node and start node of the skipblock to left node value + 1. // If node only has skipped node on right, make this node the start node of the skipblock and update end node // If node has skipped nodes on left and right, set start node of left skipblock and end node of right skipblock to the values of the left + right nodes + 1 // Optimization explanation: // The contextual logic below is the same as that in the insert() functions but in this case the value of the current skipfield node will always be // zero (since it is not yet erased), meaning no additional manipulations are necessary for the previous skipfield node comparison - we only have to check against zero const char prev_skipfield = *(it.skipfield_pointer - (it.skipfield_pointer != it.group_pointer->skipfield)) != 0; const char after_skipfield = *(it.skipfield_pointer + 1) != 0; // NOTE: boundary test (checking against end-of-elements) is able to be skipped due to the extra skipfield node (compared to element field) - which is present to enable faster iterator operator ++ operations skipfield_type update_value = 1; if (!(prev_skipfield | after_skipfield)) // no consecutive erased elements { *it.skipfield_pointer = 1; // solo skipped node const skipfield_type index = static_cast(it.element_pointer - it.group_pointer->elements); if (it.group_pointer->free_list_head != std::numeric_limits::max()) // ie. if this group already has some erased elements { *(reinterpret_cast(it.group_pointer->elements + it.group_pointer->free_list_head) + 1) = index; // set prev free list head's 'next index' number to the index of the current element } else { it.group_pointer->erasures_list_next_group = groups_with_erasures_list_head; // add it to the groups-with-erasures free list groups_with_erasures_list_head = it.group_pointer; } *(reinterpret_cast(it.element_pointer)) = it.group_pointer->free_list_head; *(reinterpret_cast(it.element_pointer) + 1) = std::numeric_limits::max(); it.group_pointer->free_list_head = index; } else if (prev_skipfield & (!after_skipfield)) // previous erased consecutive elements, none following { *(it.skipfield_pointer - *(it.skipfield_pointer - 1)) = *it.skipfield_pointer = static_cast(*(it.skipfield_pointer - 1) + 1); } else if ((!prev_skipfield) & after_skipfield) // following erased consecutive elements, none preceding { const skipfield_type following_value = static_cast(*(it.skipfield_pointer + 1) + 1); *(it.skipfield_pointer + following_value - 1) = *(it.skipfield_pointer) = following_value; const skipfield_type following_previous = *(reinterpret_cast(it.element_pointer + 1)); const skipfield_type following_next = *(reinterpret_cast(it.element_pointer + 1) + 1); *(reinterpret_cast(it.element_pointer)) = following_previous; *(reinterpret_cast(it.element_pointer) + 1) = following_next; const skipfield_type index = static_cast(it.element_pointer - it.group_pointer->elements); if (following_previous != std::numeric_limits::max()) { *(reinterpret_cast(it.group_pointer->elements + following_previous) + 1) = index; // Set next index of previous free list node to this node's 'next' index } if (following_next != std::numeric_limits::max()) { *(reinterpret_cast(it.group_pointer->elements + following_next)) = index; // Set previous index of next free list node to this node's 'previous' index } else { it.group_pointer->free_list_head = index; } update_value = following_value; } else // both preceding and following consecutive erased elements - erased element is between two skipblocks { const skipfield_type preceding_value = *(it.skipfield_pointer - 1); const skipfield_type following_value = static_cast(*(it.skipfield_pointer + 1) + 1); // Join the skipblocks *(it.skipfield_pointer - preceding_value) = *(it.skipfield_pointer + following_value - 1) = static_cast(preceding_value + following_value); // Remove the following skipblock's entry from the free list const skipfield_type following_previous = *(reinterpret_cast(it.element_pointer + 1)); const skipfield_type following_next = *(reinterpret_cast(it.element_pointer + 1) + 1); if (following_previous != std::numeric_limits::max()) { *(reinterpret_cast(it.group_pointer->elements + following_previous) + 1) = following_next; // Set next index of previous free list node to this node's 'next' index } if (following_next != std::numeric_limits::max()) { *(reinterpret_cast(it.group_pointer->elements + following_next)) = following_previous; // Set previous index of next free list node to this node's 'previous' index } else { it.group_pointer->free_list_head = following_previous; } update_value = following_value; } iterator return_iterator(it.group_pointer, it.element_pointer + update_value, it.skipfield_pointer + update_value); if (return_iterator.element_pointer == it.group_pointer->last_endpoint && it.group_pointer->next_group != NULL) { return_iterator.group_pointer = it.group_pointer->next_group; const aligned_pointer_type elements = return_iterator.group_pointer->elements; const skipfield_pointer_type skipfield = return_iterator.group_pointer->skipfield; const skipfield_type skip = *skipfield; return_iterator.element_pointer = elements + skip; return_iterator.skipfield_pointer = skipfield + skip; } if (it.element_pointer == begin_iterator.element_pointer) // If original iterator was first element in colony, update it's value with the next non-erased element: { begin_iterator = return_iterator; } return return_iterator; } // else: group is empty, consolidate groups const bool in_back_block = (it.group_pointer->next_group == NULL), in_front_block = (it.group_pointer == begin_iterator.group_pointer); if (in_back_block & in_front_block) // ie. only group in colony { // Reset skipfield and free list rather than clearing - leads to fewer allocations/deallocations: reset_only_group_left(it.group_pointer); return end_iterator; } else if ((!in_back_block) & in_front_block) // ie. Remove first group, change first group to next group { it.group_pointer->next_group->previous_group = NULL; // Cut off this group from the chain begin_iterator.group_pointer = it.group_pointer->next_group; // Make the next group the first group update_subsequent_group_numbers(begin_iterator.group_pointer); if (it.group_pointer->free_list_head != std::numeric_limits::max()) // Erasures present within the group, ie. was part of the linked list of groups with erasures. { remove_from_groups_with_erasures_list(it.group_pointer); } total_capacity -= it.group_pointer->capacity; deallocate_group(it.group_pointer); // note: end iterator only needs to be changed if the deleted group was the final group in the chain ie. not in this case begin_iterator.element_pointer = begin_iterator.group_pointer->elements + *(begin_iterator.group_pointer->skipfield); // If the beginning index has been erased (ie. skipfield != 0), skip to next non-erased element begin_iterator.skipfield_pointer = begin_iterator.group_pointer->skipfield + *(begin_iterator.group_pointer->skipfield); return begin_iterator; } else if (!(in_back_block | in_front_block)) // this is a non-first group but not final group in chain: delete the group, then link previous group to the next group in the chain: { it.group_pointer->next_group->previous_group = it.group_pointer->previous_group; const group_pointer_type return_group = it.group_pointer->previous_group->next_group = it.group_pointer->next_group; // close the chain, removing this group from it update_subsequent_group_numbers(return_group); if (it.group_pointer->free_list_head != std::numeric_limits::max()) { remove_from_groups_with_erasures_list(it.group_pointer); } if (it.group_pointer->next_group != end_iterator.group_pointer) { total_capacity -= it.group_pointer->capacity; deallocate_group(it.group_pointer); } else { add_group_to_unused_groups_list(it.group_pointer); } // Return next group's first non-erased element: return iterator(return_group, return_group->elements + *(return_group->skipfield), return_group->skipfield + *(return_group->skipfield)); } else // this is a non-first group and the final group in the chain { if (it.group_pointer->free_list_head != std::numeric_limits::max()) { remove_from_groups_with_erasures_list(it.group_pointer); } it.group_pointer->previous_group->next_group = NULL; end_iterator.group_pointer = it.group_pointer->previous_group; // end iterator needs to be changed as element supplied was the back element of the colony end_iterator.element_pointer = reinterpret_cast(end_iterator.group_pointer->skipfield); end_iterator.skipfield_pointer = end_iterator.group_pointer->skipfield + end_iterator.group_pointer->capacity; add_group_to_unused_groups_list(it.group_pointer); return end_iterator; } } // Range erase: iterator erase(const const_iterator iterator1, const const_iterator iterator2) // if uninitialized/invalid iterators supplied, function could generate an exception. If iterator1 > iterator2, behaviour is undefined. { assert(iterator1 <= iterator2); const_iterator current = iterator1; if (current.group_pointer != iterator2.group_pointer) // ie. if start and end iterators are in separate groups { if (current.element_pointer != current.group_pointer->elements + *(current.group_pointer->skipfield)) // if iterator1 is not the first non-erased element in it's group - most common case { size_type number_of_group_erasures = 0; // Now update skipfield: const aligned_pointer_type end = iterator1.group_pointer->last_endpoint; // Schema: first erase all non-erased elements until end of group & remove all skipblocks post-iterator1 from the free_list. Then, either update preceding skipblock or create new one: #ifdef PLF_TYPE_TRAITS_SUPPORT // if trivially-destructible, and C++11 or higher, and no erasures in group, skip while loop below and just jump straight to the location if (std::is_trivially_destructible::value && current.group_pointer->free_list_head == std::numeric_limits::max()) { number_of_group_erasures += static_cast(end - current.element_pointer); } else #endif { while (current.element_pointer != end) { if (*current.skipfield_pointer == 0) { #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (!std::is_trivially_destructible::value) #endif { PLF_DESTROY(allocator_type, *this, reinterpret_cast(current.element_pointer)); // Destruct element } ++number_of_group_erasures; ++current.element_pointer; ++current.skipfield_pointer; } else // remove skipblock from group: { const skipfield_type prev_free_list_index = *(reinterpret_cast(current.element_pointer)); const skipfield_type next_free_list_index = *(reinterpret_cast(current.element_pointer) + 1); current.element_pointer += *(current.skipfield_pointer); current.skipfield_pointer += *(current.skipfield_pointer); if (next_free_list_index == std::numeric_limits::max() && prev_free_list_index == std::numeric_limits::max()) // if this is the last skipblock in the free list { remove_from_groups_with_erasures_list(iterator1.group_pointer); // remove group from list of free-list groups - will be added back in down below, but not worth optimizing for iterator1.group_pointer->free_list_head = std::numeric_limits::max(); number_of_group_erasures += static_cast(end - current.element_pointer); #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (!std::is_trivially_destructible::value) #endif { while (current.element_pointer != end) // miniloop - avoid checking skipfield for rest of elements in group, as there are no more skipped elements now { PLF_DESTROY(allocator_type, *this, reinterpret_cast(current.element_pointer++)); // Destruct element } } break; // end overall while loop } else if (next_free_list_index == std::numeric_limits::max()) // if this is the head of the free list { current.group_pointer->free_list_head = prev_free_list_index; // make free list head equal to next free list node *(reinterpret_cast(current.group_pointer->elements + prev_free_list_index) + 1) = std::numeric_limits::max(); } else // either a tail or middle free list node { *(reinterpret_cast(current.group_pointer->elements + next_free_list_index)) = prev_free_list_index; if (prev_free_list_index != std::numeric_limits::max()) // ie. not the tail free list node { *(reinterpret_cast(current.group_pointer->elements + prev_free_list_index) + 1) = next_free_list_index; } } } } } const skipfield_type previous_node_value = *(iterator1.skipfield_pointer - 1); const skipfield_type distance_to_end = static_cast(end - iterator1.element_pointer); if (previous_node_value == 0) // no previous skipblock { *iterator1.skipfield_pointer = distance_to_end; // set start node value *(iterator1.skipfield_pointer + distance_to_end - 1) = distance_to_end; // set end node value const skipfield_type index = static_cast(iterator1.element_pointer - iterator1.group_pointer->elements); if (iterator1.group_pointer->free_list_head != std::numeric_limits::max()) // ie. if this group already has some erased elements { *(reinterpret_cast(iterator1.group_pointer->elements + iterator1.group_pointer->free_list_head) + 1) = index; // set prev free list head's 'next index' number to the index of the iterator1 element } else { iterator1.group_pointer->erasures_list_next_group = groups_with_erasures_list_head; // add it to the groups-with-erasures free list groups_with_erasures_list_head = iterator1.group_pointer; } *(reinterpret_cast(iterator1.element_pointer)) = iterator1.group_pointer->free_list_head; *(reinterpret_cast(iterator1.element_pointer) + 1) = std::numeric_limits::max(); iterator1.group_pointer->free_list_head = index; } else { // update previous skipblock, no need to update free list: *(iterator1.skipfield_pointer - previous_node_value) = *(iterator1.skipfield_pointer + distance_to_end - 1) = static_cast(previous_node_value + distance_to_end); } iterator1.group_pointer->size = static_cast(iterator1.group_pointer->size - number_of_group_erasures); total_size -= number_of_group_erasures; current.group_pointer = current.group_pointer->next_group; } // Intermediate groups: const group_pointer_type previous_group = current.group_pointer->previous_group; while (current.group_pointer != iterator2.group_pointer) { #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (!std::is_trivially_destructible::value) #endif { current.element_pointer = current.group_pointer->elements + *(current.group_pointer->skipfield); current.skipfield_pointer = current.group_pointer->skipfield + *(current.group_pointer->skipfield); const aligned_pointer_type end = current.group_pointer->last_endpoint; do { PLF_DESTROY(allocator_type, *this, reinterpret_cast(current.element_pointer)); // Destruct element const skipfield_type skip = *(++current.skipfield_pointer); current.element_pointer += static_cast(skip) + 1u; current.skipfield_pointer += skip; } while (current.element_pointer != end); } if (current.group_pointer->free_list_head != std::numeric_limits::max()) { remove_from_groups_with_erasures_list(current.group_pointer); } total_size -= current.group_pointer->size; const group_pointer_type current_group = current.group_pointer; current.group_pointer = current.group_pointer->next_group; if (current_group != end_iterator.group_pointer && current_group->next_group != end_iterator.group_pointer) { total_capacity -= current_group->capacity; deallocate_group(current_group); } else { add_group_to_unused_groups_list(current_group); } } current.element_pointer = current.group_pointer->elements + *(current.group_pointer->skipfield); current.skipfield_pointer = current.group_pointer->skipfield + *(current.group_pointer->skipfield); current.group_pointer->previous_group = previous_group; if (previous_group != NULL) { previous_group->next_group = current.group_pointer; } else { begin_iterator = iterator2; // This line is included here primarily to avoid a secondary if statement within the if block below - it will not be needed in any other situation } } if (current.element_pointer == iterator2.element_pointer) // in case iterator2 was at beginning of it's group - also covers empty range case (first == last) { return iterator2; } // Final group: // Code explanation: // If not erasing entire final group, 1. Destruct elements (if non-trivial destructor) and add locations to group free list. 2. process skipfield. // If erasing entire group, 1. Destruct elements (if non-trivial destructor), 2. if no elements left in colony, clear() 3. otherwise reset end_iterator and remove group from groups-with-erasures list (if free list of erasures present) if (iterator2.element_pointer != end_iterator.element_pointer || current.element_pointer != current.group_pointer->elements + *(current.group_pointer->skipfield)) // ie. not erasing entire group { size_type number_of_group_erasures = 0; // Schema: first erased all non-erased elements until end of group & remove all skipblocks post-iterator2 from the free_list. Then, either update preceding skipblock or create new one: const const_iterator current_saved = current; #ifdef PLF_TYPE_TRAITS_SUPPORT // if trivially-destructible, and C++11 or higher, and no erasures in group, skip while loop below and just jump straight to the location if (std::is_trivially_destructible::value && current.group_pointer->free_list_head == std::numeric_limits::max()) { number_of_group_erasures += static_cast(iterator2.element_pointer - current.element_pointer); } else #endif { while (current.element_pointer != iterator2.element_pointer) { if (*current.skipfield_pointer == 0) { #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (!std::is_trivially_destructible::value) #endif { PLF_DESTROY(allocator_type, *this, reinterpret_cast(current.element_pointer)); // Destruct element } ++number_of_group_erasures; ++current.element_pointer; ++current.skipfield_pointer; } else // remove skipblock from group: { const skipfield_type prev_free_list_index = *(reinterpret_cast(current.element_pointer)); const skipfield_type next_free_list_index = *(reinterpret_cast(current.element_pointer) + 1); current.element_pointer += *(current.skipfield_pointer); current.skipfield_pointer += *(current.skipfield_pointer); if (next_free_list_index == std::numeric_limits::max() && prev_free_list_index == std::numeric_limits::max()) // if this is the last skipblock in the free list { remove_from_groups_with_erasures_list(iterator2.group_pointer); // remove group from list of free-list groups - will be added back in down below, but not worth optimizing for iterator2.group_pointer->free_list_head = std::numeric_limits::max(); number_of_group_erasures += static_cast(iterator2.element_pointer - current.element_pointer); #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (!std::is_trivially_destructible::value) #endif { while (current.element_pointer != iterator2.element_pointer) { PLF_DESTROY(allocator_type, *this, reinterpret_cast(current.element_pointer++)); // Destruct element } } break; // end overall while loop } else if (next_free_list_index == std::numeric_limits::max()) // if this is the head of the free list { current.group_pointer->free_list_head = prev_free_list_index; *(reinterpret_cast(current.group_pointer->elements + prev_free_list_index) + 1) = std::numeric_limits::max(); } else { *(reinterpret_cast(current.group_pointer->elements + next_free_list_index)) = prev_free_list_index; if (prev_free_list_index != std::numeric_limits::max()) // ie. not the tail free list node { *(reinterpret_cast(current.group_pointer->elements + prev_free_list_index) + 1) = next_free_list_index; } } } } } const skipfield_type distance_to_iterator2 = static_cast(iterator2.element_pointer - current_saved.element_pointer); const skipfield_type index = static_cast(current_saved.element_pointer - iterator2.group_pointer->elements); if (index == 0 || *(current_saved.skipfield_pointer - 1) == 0) // element is either at start of group or previous skipfield node is 0 { *(current_saved.skipfield_pointer) = distance_to_iterator2; *(iterator2.skipfield_pointer - 1) = distance_to_iterator2; if (iterator2.group_pointer->free_list_head != std::numeric_limits::max()) // ie. if this group already has some erased elements { *(reinterpret_cast(iterator2.group_pointer->elements + iterator2.group_pointer->free_list_head) + 1) = index; } else { iterator2.group_pointer->erasures_list_next_group = groups_with_erasures_list_head; // add it to the groups-with-erasures free list groups_with_erasures_list_head = iterator2.group_pointer; } *(reinterpret_cast(current_saved.element_pointer)) = iterator2.group_pointer->free_list_head; *(reinterpret_cast(current_saved.element_pointer) + 1) = std::numeric_limits::max(); iterator2.group_pointer->free_list_head = index; } else // If iterator 1 & 2 are in same group, but iterator 1 was not at start of group, and previous skipfield node is an end node in a skipblock: { // Just update existing skipblock, no need to create new free list node: const skipfield_type prev_node_value = *(current_saved.skipfield_pointer - 1); *(current_saved.skipfield_pointer - prev_node_value) = static_cast(prev_node_value + distance_to_iterator2); *(iterator2.skipfield_pointer - 1) = static_cast(prev_node_value + distance_to_iterator2); } if (iterator1.element_pointer == begin_iterator.element_pointer) { begin_iterator = iterator2; } iterator2.group_pointer->size = static_cast(iterator2.group_pointer->size - number_of_group_erasures); total_size -= number_of_group_erasures; } else // ie. full group erasure { #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (!std::is_trivially_destructible::value) #endif { while(current.element_pointer != iterator2.element_pointer) { PLF_DESTROY(allocator_type, *this, reinterpret_cast(current.element_pointer)); ++current.skipfield_pointer; current.element_pointer += static_cast(*current.skipfield_pointer) + 1u; current.skipfield_pointer += *current.skipfield_pointer; } } if ((total_size -= current.group_pointer->size) != 0) // ie. either previous_group != NULL or next_group != NULL { if (current.group_pointer->free_list_head != std::numeric_limits::max()) { remove_from_groups_with_erasures_list(current.group_pointer); } current.group_pointer->previous_group->next_group = current.group_pointer->next_group; if (current.group_pointer == end_iterator.group_pointer) { end_iterator.group_pointer = current.group_pointer->previous_group; end_iterator.element_pointer = end_iterator.group_pointer->last_endpoint; end_iterator.skipfield_pointer = end_iterator.group_pointer->skipfield + end_iterator.group_pointer->capacity; add_group_to_unused_groups_list(current.group_pointer); return end_iterator; } else if (current.group_pointer == begin_iterator.group_pointer) { begin_iterator.group_pointer = current.group_pointer->next_group; const skipfield_type skip = *(begin_iterator.group_pointer->skipfield); begin_iterator.element_pointer = begin_iterator.group_pointer->elements + skip; begin_iterator.skipfield_pointer = begin_iterator.group_pointer->skipfield + skip; } if (current.group_pointer->next_group != end_iterator.group_pointer) { total_capacity -= current.group_pointer->capacity; } else { add_group_to_unused_groups_list(current.group_pointer); return iterator2; } } else // ie. colony is now empty { // Reset skipfield and free list rather than clearing - leads to fewer allocations/deallocations: reset_only_group_left(current.group_pointer); return end_iterator; } deallocate_group(current.group_pointer); } return iterator2; } private: void prepare_groups_for_assign(const size_type size) { // Destroy all elements if non-trivial: #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (!std::is_trivially_destructible::value) #endif { for (iterator current = begin_iterator; current != end_iterator; ++current) { PLF_DESTROY(allocator_type, *this, reinterpret_cast(current.element_pointer)); } } if (size < total_capacity && (total_capacity - size) >= tuple_allocator_pair.min_group_capacity) { size_type difference = total_capacity - size; end_iterator.group_pointer->next_group = unused_groups_head; // Remove surplus groups which're under the difference limit: group_pointer_type current_group = begin_iterator.group_pointer, previous_group = NULL; do { const group_pointer_type next_group = current_group->next_group; if (current_group->capacity <= difference) { // Remove group: difference -= current_group->capacity; total_capacity -= current_group->capacity; deallocate_group(current_group); if (current_group == begin_iterator.group_pointer) { begin_iterator.group_pointer = next_group; } } else { if (previous_group != NULL) { previous_group->next_group = current_group; } previous_group = current_group; } current_group = next_group; } while (current_group != NULL); previous_group->next_group = NULL; } else { if (size > total_capacity) { reserve(size); } // Join all unused_groups to main chain: end_iterator.group_pointer->next_group = unused_groups_head; } begin_iterator.element_pointer = begin_iterator.group_pointer->elements; begin_iterator.skipfield_pointer = begin_iterator.group_pointer->skipfield; groups_with_erasures_list_head = NULL; total_size = 0; } public: // Fill assign: inline void assign(const size_type size, const element_type &element) { if (size == 0) { clear(); return; } prepare_groups_for_assign(size); fill_unused_groups(size, element, 0, NULL, begin_iterator.group_pointer); } private: // Range assign core: template inline void range_assign(const iterator_type it, const size_type size) { if (size == 0) { clear(); return; } prepare_groups_for_assign(size); range_fill_unused_groups(size, it, 0, NULL, begin_iterator.group_pointer); } public: // Range assign: template inline void assign(const typename plf_enable_if_c::is_integer, iterator_type>::type first, const iterator_type last) { using std::distance; range_assign(first, static_cast(distance(first,last))); } // Range insert for differing iterator types eg. sentinels: #ifdef PLF_CPP20_SUPPORT template requires (!std::same_as && std::equality_comparable_with && !std::integral && !std::integral) inline void assign (const iterator_type1 first, const iterator_type2 last) { size_type distance = 0; for(iterator_type1 current = first; current != last; ++current, ++distance) {}; range_assign(first, distance); } #endif // Range assign, move_iterator overload: #ifdef PLF_MOVE_SEMANTICS_SUPPORT template inline void assign (const std::move_iterator first, const std::move_iterator last) { using std::distance; range_assign(first, static_cast(distance(first.base(),last.base()))); } #endif // Initializer-list assign: #ifdef PLF_INITIALIZER_LIST_SUPPORT inline void assign(const std::initializer_list &element_list) { range_assign(element_list.begin(), static_cast(element_list.size())); } #endif #ifdef PLF_CPP20_SUPPORT [[nodiscard]] #endif inline PLF_FORCE_INLINE bool empty() const PLF_NOEXCEPT { return total_size == 0; } inline size_type size() const PLF_NOEXCEPT { return total_size; } #ifdef PLF_COLONY_TEST_DEBUG // used for debugging during internal testing only: size_type group_size_sum() const PLF_NOEXCEPT { size_type temp = 0; for (group_pointer_type current = begin_iterator.group_pointer; current != NULL; current = current->next_group) { temp += current->size; } return temp; } #endif inline size_type max_size() const PLF_NOEXCEPT { #ifdef PLF_ALLOCATOR_TRAITS_SUPPORT return std::allocator_traits::max_size(*this); #else return allocator_type::max_size(); #endif } inline size_type capacity() const PLF_NOEXCEPT { return total_capacity; } inline size_type memory() const PLF_NOEXCEPT { size_type memory_use = sizeof(*this); // sizeof colony basic structure end_iterator.group_pointer->next_group = unused_groups_head; // temporarily link the main groups and unused groups (reserved groups) in order to only have one loop below instead of several for(group_pointer_type current = begin_iterator.group_pointer; current != NULL; current = current->next_group) { memory_use += sizeof(group) + (PLF_GROUP_ALIGNED_BLOCK_SIZE(current->capacity) * sizeof(aligned_allocation_struct)); // add memory block sizes and the size of the group structs themselves. The original calculation, including divisor, is necessary in order to correctly round up the number of allocations } end_iterator.group_pointer->next_group = NULL; // unlink main groups and unused groups return memory_use; } private: // get all elements contiguous in memory and shrink to fit, remove erasures and erasure free lists. Invalidates all iterators and pointers to elements. void consolidate() { #if defined(PLF_MOVE_SEMANTICS_SUPPORT) && defined(PLF_TYPE_TRAITS_SUPPORT) if PLF_CONSTEXPR (std::is_move_constructible::value && std::is_move_assignable::value) { colony temp(plf::colony_limits(tuple_allocator_pair.min_group_capacity, group_allocator_pair.max_group_capacity)); temp.range_assign(std::make_move_iterator(begin_iterator), total_size); *this = std::move(temp); // Avoid generating 2nd temporary } else #endif { colony temp(*this); swap(temp); } } public: void reshape(const plf::colony_limits capacities) { check_capacities_conformance(capacities); tuple_allocator_pair.min_group_capacity = static_cast(capacities.min); group_allocator_pair.max_group_capacity = static_cast(capacities.max); // Need to check all group sizes here, because splice might append smaller blocks to the end of a larger block: for (group_pointer_type current = begin_iterator.group_pointer; current != end_iterator.group_pointer; current = current->next_group) { if (current->capacity < tuple_allocator_pair.min_group_capacity || current->capacity > group_allocator_pair.max_group_capacity) { #ifdef PLF_TYPE_TRAITS_SUPPORT // If type is non-copyable/movable, cannot be consolidated, throw exception: if PLF_CONSTEXPR (!(std::is_copy_constructible::value || std::is_move_constructible::value)) { throw; } else #endif { consolidate(); } return; } } } inline plf::colony_limits block_limits() const PLF_NOEXCEPT { return plf::colony_limits(static_cast(tuple_allocator_pair.min_group_capacity), static_cast(group_allocator_pair.max_group_capacity)); } inline PLF_FORCE_INLINE void clear() PLF_NOEXCEPT { destroy_all_data(); blank(); } inline colony & operator = (const colony &source) { assert(&source != this); range_assign(source.begin_iterator, source.total_size); return *this; } #ifdef PLF_MOVE_SEMANTICS_SUPPORT // Move assignment colony & operator = (colony &&source) PLF_NOEXCEPT_MOVE_ASSIGN(allocator_type) { assert(&source != this); destroy_all_data(); #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (std::is_trivial::value && std::is_trivial::value && std::is_trivial::value) { std::memcpy(static_cast(this), &source, sizeof(colony)); } else #endif { end_iterator = std::move(source.end_iterator); begin_iterator = std::move(source.begin_iterator); groups_with_erasures_list_head = std::move(source.groups_with_erasures_list_head); unused_groups_head = std::move(source.unused_groups_head); total_size = source.total_size; total_capacity = source.total_capacity; tuple_allocator_pair.min_group_capacity = source.tuple_allocator_pair.min_group_capacity; group_allocator_pair.max_group_capacity = source.group_allocator_pair.max_group_capacity; } source.blank(); return *this; } #endif #ifdef PLF_INITIALIZER_LIST_SUPPORT inline colony & operator = (const std::initializer_list &element_list) { range_assign(element_list.begin(), static_cast(element_list.size())); return *this; } #endif friend bool operator == (const colony &lh, const colony &rh) { if (lh.total_size != rh.total_size) { return false; } for (const_iterator lh_iterator = lh.begin_iterator, rh_iterator = rh.begin_iterator; lh_iterator != lh.end_iterator; ++lh_iterator, ++rh_iterator) { if (*lh_iterator != *rh_iterator) { return false; } } return true; } friend bool operator != (const colony &lh, const colony &rh) { return !(lh == rh); } void shrink_to_fit() { if (total_size == total_capacity) { return; } else if (total_size == 0) { clear(); return; } consolidate(); } void trim() PLF_NOEXCEPT { while(unused_groups_head != NULL) { total_capacity -= unused_groups_head->capacity; const group_pointer_type next_group = unused_groups_head->next_group; deallocate_group(unused_groups_head); unused_groups_head = next_group; } } void reserve(size_type new_capacity) { if (new_capacity == 0 || new_capacity <= total_capacity) // We already have enough space allocated { return; } if (new_capacity > max_size()) { throw std::length_error("Capacity requested via reserve() greater than max_size()"); } new_capacity -= total_capacity; size_type number_of_max_groups = new_capacity / group_allocator_pair.max_group_capacity; skipfield_type remainder = static_cast(new_capacity - (number_of_max_groups * group_allocator_pair.max_group_capacity)); if (remainder == 0) { remainder = group_allocator_pair.max_group_capacity; --number_of_max_groups; } else if (remainder < tuple_allocator_pair.min_group_capacity) { remainder = tuple_allocator_pair.min_group_capacity; } group_pointer_type current_group, first_unused_group; if (begin_iterator.group_pointer == NULL) // Most common scenario - empty colony { initialize(remainder); begin_iterator.group_pointer->last_endpoint = begin_iterator.group_pointer->elements; // last_endpoint initially == elements + 1 via default constructor begin_iterator.group_pointer->size = 0; // 1 by default if (number_of_max_groups == 0) { return; } else { first_unused_group = current_group = allocate_new_group(group_allocator_pair.max_group_capacity, begin_iterator.group_pointer); total_capacity += group_allocator_pair.max_group_capacity; --number_of_max_groups; } } else // Non-empty colony, add first group: { first_unused_group = current_group = allocate_new_group(remainder, end_iterator.group_pointer); total_capacity += remainder; } while (number_of_max_groups != 0) { try { current_group->next_group = allocate_new_group(group_allocator_pair.max_group_capacity, current_group); } catch (...) { deallocate_group(current_group->next_group); current_group->next_group = unused_groups_head; unused_groups_head = first_unused_group; throw; } current_group = current_group->next_group; total_capacity += group_allocator_pair.max_group_capacity; --number_of_max_groups; } current_group->next_group = unused_groups_head; unused_groups_head = first_unused_group; } private: template colony_iterator get_it(const pointer element_pointer) const PLF_NOEXCEPT { typedef colony_iterator iterator_type; if (total_size != 0) // Necessary here to prevent a pointer matching to an empty colony with one memory block retained with the skipfield wiped (see erase()) { // Start with last group first, as will be the largest group in most cases: for (group_pointer_type current_group = end_iterator.group_pointer; current_group != NULL; current_group = current_group->previous_group) { if (reinterpret_cast(element_pointer) >= current_group->elements && reinterpret_cast(element_pointer) < reinterpret_cast(current_group->skipfield)) { const skipfield_pointer_type skipfield_pointer = current_group->skipfield + (reinterpret_cast(element_pointer) - current_group->elements); return (*skipfield_pointer == 0) ? iterator_type(current_group, reinterpret_cast(element_pointer), skipfield_pointer) : static_cast(end_iterator); // If element has been erased, return end() } } } return end_iterator; } public: inline iterator get_iterator(const pointer element_pointer) PLF_NOEXCEPT { return get_it(element_pointer); } inline const_iterator get_iterator(const const_pointer element_pointer) const PLF_NOEXCEPT { return get_it(const_cast(element_pointer)); } inline allocator_type get_allocator() const PLF_NOEXCEPT { return *this; } void splice(colony &source) { // Process: if there are unused memory spaces at the end of the current back group of the chain, convert them // to skipped elements and add the locations to the group's free list. // Then link the destination's groups to the source's groups and nullify the source. // If the source has more unused memory spaces in the back group than the destination, swap them before processing to reduce the number of locations added to a free list and also subsequent jumps during iteration. assert(&source != this); if (source.total_size == 0) { return; } else if (total_size == 0) { #ifdef PLF_MOVE_SEMANTICS_SUPPORT *this = std::move(source); #else clear(); swap(source); #endif return; } // If there's more unused element locations in back memory block of destination than in back memory block of source, swap with source to reduce number of skipped elements during iteration, and reduce size of free-list: if ((reinterpret_cast(end_iterator.group_pointer->skipfield) - end_iterator.element_pointer) > (reinterpret_cast(source.end_iterator.group_pointer->skipfield) - source.end_iterator.element_pointer)) { swap(source); } // Throw if incompatible group capacity found: if (source.tuple_allocator_pair.min_group_capacity < tuple_allocator_pair.min_group_capacity || source.group_allocator_pair.max_group_capacity > group_allocator_pair.max_group_capacity) { for (group_pointer_type current_group = source.begin_iterator.group_pointer; current_group != NULL; current_group = current_group->next_group) { if (current_group->capacity < tuple_allocator_pair.min_group_capacity || current_group->capacity > group_allocator_pair.max_group_capacity) { throw std::length_error("A source memory block capacity is outside of the destination's minimum or maximum memory block capacity limits - please change either the source or the destination's min/max block capacity limits using reshape() before calling splice() in this case"); } } } // Add source list of groups-with-erasures to destination list of groups-with-erasures: if (source.groups_with_erasures_list_head != NULL) { if (groups_with_erasures_list_head != NULL) { group_pointer_type tail_group = groups_with_erasures_list_head; while (tail_group->erasures_list_next_group != NULL) { tail_group = tail_group->erasures_list_next_group; } tail_group->erasures_list_next_group = source.groups_with_erasures_list_head; } else { groups_with_erasures_list_head = source.groups_with_erasures_list_head; } } const skipfield_type distance_to_end = static_cast(reinterpret_cast(end_iterator.group_pointer->skipfield) - end_iterator.element_pointer); if (distance_to_end != 0) // 0 == edge case { // Mark unused element memory locations from back group as skipped/erased: // Update skipfield: const skipfield_type previous_node_value = *(end_iterator.skipfield_pointer - 1); end_iterator.group_pointer->last_endpoint = reinterpret_cast(end_iterator.group_pointer->skipfield); if (previous_node_value == 0) // no previous skipblock { *end_iterator.skipfield_pointer = distance_to_end; *(end_iterator.skipfield_pointer + distance_to_end - 1) = distance_to_end; const skipfield_type index = static_cast(end_iterator.element_pointer - end_iterator.group_pointer->elements); if (end_iterator.group_pointer->free_list_head != std::numeric_limits::max()) // ie. if this group already has some erased elements { *(reinterpret_cast(end_iterator.group_pointer->elements + end_iterator.group_pointer->free_list_head) + 1) = index; // set prev free list head's 'next index' number to the index of the current element } else { end_iterator.group_pointer->erasures_list_next_group = groups_with_erasures_list_head; // add it to the groups-with-erasures free list groups_with_erasures_list_head = end_iterator.group_pointer; } *(reinterpret_cast(end_iterator.element_pointer)) = end_iterator.group_pointer->free_list_head; *(reinterpret_cast(end_iterator.element_pointer) + 1) = std::numeric_limits::max(); end_iterator.group_pointer->free_list_head = index; } else { // update previous skipblock, no need to update free list: *(end_iterator.skipfield_pointer - previous_node_value) = *(end_iterator.skipfield_pointer + distance_to_end - 1) = static_cast(previous_node_value + distance_to_end); } } // Update subsequent group numbers: group_pointer_type current_group = source.begin_iterator.group_pointer; size_type current_group_number = end_iterator.group_pointer->group_number; do { current_group->group_number = ++current_group_number; current_group = current_group->next_group; } while (current_group != NULL); // Join the destination and source group chains: end_iterator.group_pointer->next_group = source.begin_iterator.group_pointer; source.begin_iterator.group_pointer->previous_group = end_iterator.group_pointer; end_iterator = source.end_iterator; total_size += source.total_size; total_capacity += source.total_capacity; // Remove source unused groups: source.trim(); source.blank(); } private: struct less { bool operator() (const element_type &a, const element_type &b) const PLF_NOEXCEPT { return a < b; } }; struct item_index_tuple { pointer original_location; size_type original_index; item_index_tuple(const pointer _item, const size_type _index) PLF_NOEXCEPT: original_location(_item), original_index(_index) {} }; template struct sort_dereferencer { comparison_function stored_instance; explicit sort_dereferencer(const comparison_function &function_instance): stored_instance(function_instance) {} bool operator() (const item_index_tuple first, const item_index_tuple second) { return stored_instance(*(first.original_location), *(second.original_location)); } }; public: template void sort(comparison_function compare) { if (total_size < 2) { return; } tuple_pointer_type const sort_array = PLF_ALLOCATE(tuple_allocator_type, tuple_allocator_pair, total_size, NULL); tuple_pointer_type tuple_pointer = sort_array; // Construct pointers to all elements in the sequence: size_type index = 0; for (iterator current_element = begin_iterator; current_element != end_iterator; ++current_element, ++tuple_pointer, ++index) { #ifdef PLF_VARIADICS_SUPPORT PLF_CONSTRUCT(tuple_allocator_type, tuple_allocator_pair, tuple_pointer, &*current_element, index); #else PLF_CONSTRUCT(tuple_allocator_type, tuple_allocator_pair, tuple_pointer, item_index_tuple(&*current_element, index)); #endif } // Now, sort the pointers by the values they point to (std::sort is default sort function if the macro below is not defined): #ifndef PLF_SORT_FUNCTION std::sort(sort_array, sort_array + total_size, sort_dereferencer(compare)); #else PLF_SORT_FUNCTION(sort_array, sort_array + total_size, sort_dereferencer(compare)); #endif // Sort the actual elements via the tuple array: index = 0; for (tuple_pointer_type current_tuple = sort_array; current_tuple != tuple_pointer; ++current_tuple, ++index) { if (current_tuple->original_index != index) { #ifdef PLF_MOVE_SEMANTICS_SUPPORT element_type end_value = std::move(*(current_tuple->original_location)); #else element_type end_value = *(current_tuple->original_location); #endif size_type destination_index = index; size_type source_index = current_tuple->original_index; do { #ifdef PLF_MOVE_SEMANTICS_SUPPORT *(sort_array[destination_index].original_location) = std::move(*(sort_array[source_index].original_location)); #else *(sort_array[destination_index].original_location) = *(sort_array[source_index].original_location); #endif destination_index = source_index; source_index = sort_array[destination_index].original_index; sort_array[destination_index].original_index = destination_index; } while (source_index != index); #ifdef PLF_MOVE_SEMANTICS_SUPPORT *(sort_array[destination_index].original_location) = std::move(end_value); #else *(sort_array[destination_index].original_location) = end_value; #endif } } PLF_DEALLOCATE(tuple_allocator_type, tuple_allocator_pair, sort_array, total_size); } inline void sort() { sort(less()); } struct colony_data : public uchar_allocator_type { aligned_pointer_type * const block_pointers; // array of pointers to element memory blocks unsigned char * * const bitfield_pointers; // array of pointers to bitfields in the form of unsigned char arrays representing whether an element is erased or not (0 for erased). size_t * const block_capacities; // array of the number of elements in each memory block const size_t number_of_blocks; // size of each of the arrays above colony_data(const size_type size) : block_pointers(reinterpret_cast(PLF_ALLOCATE(uchar_allocator_type, *this, size * sizeof(aligned_pointer_type), NULL))), bitfield_pointers(reinterpret_cast(PLF_ALLOCATE(uchar_allocator_type, *this, size * sizeof(unsigned char *), NULL))), block_capacities(reinterpret_cast(PLF_ALLOCATE(uchar_allocator_type, *this, size * sizeof(size_t), NULL))), number_of_blocks(size) {} ~colony_data() { for (size_t index = 0; index != number_of_blocks; ++index) { PLF_DEALLOCATE(uchar_allocator_type, *this, bitfield_pointers[index], (block_capacities[index] + 7) / 8); } PLF_DEALLOCATE(uchar_allocator_type, *this, reinterpret_cast(block_pointers), number_of_blocks * sizeof(aligned_pointer_type)); PLF_DEALLOCATE(uchar_allocator_type, *this, reinterpret_cast(bitfield_pointers), number_of_blocks * sizeof(unsigned char *)); PLF_DEALLOCATE(uchar_allocator_type, *this, reinterpret_cast(block_capacities), number_of_blocks * sizeof(size_t)); } }; typedef colony_data hive_data; private: void setup_data_cell(colony_data *data, const group_pointer_type current_group, const size_t capacity, const size_t group_number) { const size_t bitfield_capacity = (capacity + 7) / 8; // round up data->block_pointers[group_number] = current_group->elements; unsigned char *bitfield_location = data->bitfield_pointers[group_number] = PLF_ALLOCATE(uchar_allocator_type, (*data), bitfield_capacity, NULL); data->block_capacities[group_number] = capacity; std::memset(bitfield_location, 0, bitfield_capacity); skipfield_pointer_type skipfield_pointer = current_group->skipfield; const unsigned char *end = bitfield_location + bitfield_capacity; for (size_t index = 0; bitfield_location != end; ++bitfield_location) { for (unsigned char offset = 0; offset != 8 && index != capacity; ++index, ++offset, ++skipfield_pointer) { *bitfield_location |= static_cast(static_cast(!*skipfield_pointer) << offset); } } } public: colony_data * data() { colony_data *data = new colony_data(end_iterator.group_pointer->group_number + 1); size_t group_number = 0; for (group_pointer_type current_group = begin_iterator.group_pointer; current_group != end_iterator.group_pointer; current_group = current_group->next_group, ++group_number) { setup_data_cell(data, current_group, current_group->capacity, group_number); } // Special case for end group: setup_data_cell(data, end_iterator.group_pointer, static_cast(end_iterator.group_pointer->last_endpoint - end_iterator.group_pointer->elements), group_number); return data; } void swap(colony &source) PLF_NOEXCEPT_SWAP(allocator_type) { assert(&source != this); #ifdef PLF_TYPE_TRAITS_SUPPORT if PLF_CONSTEXPR (std::is_trivial::value && std::is_trivial::value && std::is_trivial::value) // if all pointer types are trivial we can just copy using memcpy - avoids constructors/destructors etc and is faster { char temp[sizeof(colony)]; std::memcpy(&temp, static_cast(this), sizeof(colony)); std::memcpy(static_cast(this), static_cast(&source), sizeof(colony)); std::memcpy(static_cast(&source), &temp, sizeof(colony)); } #ifdef PLF_MOVE_SEMANTICS_SUPPORT // Moving is probably going to be more efficient than copying, particularly if pointer types are non-trivial: else if PLF_CONSTEXPR (std::is_move_assignable::value && std::is_move_assignable::value && std::is_move_assignable::value && std::is_move_constructible::value && std::is_move_constructible::value && std::is_move_constructible::value) { colony temp(std::move(source)); source = std::move(*this); *this = std::move(temp); } #endif else #endif { const iterator swap_end_iterator = end_iterator, swap_begin_iterator = begin_iterator; const group_pointer_type swap_groups_with_erasures_list_head = groups_with_erasures_list_head, swap_unused_groups_head = unused_groups_head; const size_type swap_total_size = total_size, swap_total_capacity = total_capacity; const skipfield_type swap_min_group_capacity = tuple_allocator_pair.min_group_capacity, swap_max_group_capacity = group_allocator_pair.max_group_capacity; end_iterator = source.end_iterator; begin_iterator = source.begin_iterator; groups_with_erasures_list_head = source.groups_with_erasures_list_head; unused_groups_head = source.unused_groups_head; total_size = source.total_size; total_capacity = source.total_capacity; tuple_allocator_pair.min_group_capacity = source.tuple_allocator_pair.min_group_capacity; group_allocator_pair.max_group_capacity = source.group_allocator_pair.max_group_capacity; source.end_iterator = swap_end_iterator; source.begin_iterator = swap_begin_iterator; source.groups_with_erasures_list_head = swap_groups_with_erasures_list_head; source.unused_groups_head = swap_unused_groups_head; source.total_size = swap_total_size; source.total_capacity = swap_total_capacity; source.tuple_allocator_pair.min_group_capacity = swap_min_group_capacity; source.group_allocator_pair.max_group_capacity = swap_max_group_capacity; } } }; // colony // Set up hive as alias of colony: #if defined(__cplusplus) && __cplusplus >= 201103L typedef colony_limits hive_limits; typedef colony_priority hive_priority; template , plf::hive_priority priority = plf::performance> using hive = plf::colony; #endif // Used by std::erase_if() overload below: template struct colony_eq_to { const element_type value; explicit colony_eq_to(const element_type store_value): /* may not be noexcept as the element may allocate and therefore potentially throw when copied */ value(store_value) {} inline bool operator() (const element_type compare_value) const PLF_NOEXCEPT { return value == compare_value; } }; } // plf namespace namespace std { template inline void swap (plf::colony &a, plf::colony &b) PLF_NOEXCEPT_SWAP(allocator_type) { a.swap(b); } template typename plf::colony::size_type erase_if(plf::colony &container, predicate_function predicate) { typedef typename plf::colony colony; typedef typename colony::const_iterator const_iterator; typedef typename colony::size_type size_type; size_type count = 0; for(const_iterator current = container.begin(), end = container.end(); current != end;) { if (predicate(*current)) { const size_type original_count = ++count; const_iterator last(++const_iterator(current)); while(last != end && predicate(*last)) { ++last; ++count; } if (count != original_count) { current = container.erase(current, last); } else { current = container.erase(current); } end = container.end(); } else { ++current; } } return count; } template inline typename plf::colony::size_type erase(plf::colony &container, const element_type &value) { return erase_if(container, plf::colony_eq_to(value)); } } #undef PLF_MIN_BLOCK_CAPACITY #undef PLF_GROUP_ALIGNED_BLOCK_SIZE #undef PLF_FORCE_INLINE #undef PLF_ALIGNMENT_SUPPORT #undef PLF_INITIALIZER_LIST_SUPPORT #undef PLF_TYPE_TRAITS_SUPPORT #undef PLF_ALLOCATOR_TRAITS_SUPPORT #undef PLF_VARIADICS_SUPPORT #undef PLF_MOVE_SEMANTICS_SUPPORT #undef PLF_NOEXCEPT #undef PLF_NOEXCEPT_SWAP #undef PLF_NOEXCEPT_MOVE_ASSIGN #undef PLF_CONSTEXPR #undef PLF_CPP20_SUPPORT #undef PLF_STATIC_ASSERT #undef PLF_CONSTRUCT #undef PLF_DESTROY #undef PLF_ALLOCATE #undef PLF_DEALLOCATE #endif // PLF_COLONY_H ================================================ FILE: SG14/ring.h ================================================ #pragma once #include #include #include #include namespace sg14 { template struct null_popper { void operator()(T&) const noexcept; }; template struct default_popper { T operator()(T& t) const; }; template struct copy_popper { copy_popper(T t); T operator()(T& t) const; private: T m_copy; }; template class ring_iterator; template> class ring_span { public: using type = ring_span; using size_type = std::size_t; using value_type = T; using pointer = T*; using reference = T&; using const_reference = const T&; using iterator = ring_iterator; using const_iterator = ring_iterator; using reverse_iterator = std::reverse_iterator; using const_reverse_iterator = std::reverse_iterator; friend class ring_iterator; friend class ring_iterator; template ring_span(ContiguousIterator begin, ContiguousIterator end, Popper p = Popper()) noexcept; template ring_span(ContiguousIterator begin, ContiguousIterator end, ContiguousIterator first, size_type size, Popper p = Popper()) noexcept; ring_span(ring_span&&) = default; ring_span& operator=(ring_span&&) = default; bool empty() const noexcept; bool full() const noexcept; size_type size() const noexcept; size_type capacity() const noexcept; reference front() noexcept; const_reference front() const noexcept; reference back() noexcept; const_reference back() const noexcept; iterator begin() noexcept; const_iterator begin() const noexcept; iterator end() noexcept; const_iterator end() const noexcept; const_iterator cbegin() const noexcept; const_reverse_iterator crbegin() const noexcept; reverse_iterator rbegin() noexcept; const_reverse_iterator rbegin() const noexcept; const_iterator cend() const noexcept; const_reverse_iterator crend() const noexcept; reverse_iterator rend() noexcept; const_reverse_iterator rend() const noexcept; template::value>> void push_back(const value_type& from_value) noexcept(std::is_nothrow_copy_assignable::value); template::value>> void push_back(value_type&& from_value) noexcept(std::is_nothrow_move_assignable::value); template void emplace_back(FromType&&... from_value) noexcept(std::is_nothrow_constructible::value && std::is_nothrow_move_assignable::value); auto pop_front(); void swap(type& rhs) noexcept;// (std::is_nothrow_swappable::value); // Example implementation private: reference at(size_type idx) noexcept; const_reference at(size_type idx) const noexcept; size_type back_idx() const noexcept; void increase_size() noexcept; T* m_data; size_type m_size; size_type m_capacity; size_type m_front_idx; Popper m_popper; }; template void swap(ring_span&, ring_span&) noexcept; template class ring_iterator { public: using type = ring_iterator; using value_type = typename Ring::value_type; using difference_type = std::ptrdiff_t; using pointer = typename std::conditional_t*; using reference = typename std::conditional_t&; using iterator_category = std::random_access_iterator_tag; operator ring_iterator() const noexcept; template bool operator==(const ring_iterator& rhs) const noexcept; template bool operator!=(const ring_iterator& rhs) const noexcept; template bool operator<(const ring_iterator& rhs) const noexcept; template bool operator<=(const ring_iterator& rhs) const noexcept; template bool operator>(const ring_iterator& rhs) const noexcept; template bool operator>=(const ring_iterator& rhs) const noexcept; reference operator*() const noexcept; type& operator++() noexcept; type operator++(int) noexcept; type& operator--() noexcept; type operator--(int) noexcept; type& operator+=(std::ptrdiff_t i) noexcept; type& operator-=(std::ptrdiff_t i) noexcept; template std::ptrdiff_t operator-(const ring_iterator& rhs) const noexcept; // Example implementation private: friend Ring; friend class ring_iterator; using size_type = typename Ring::size_type; ring_iterator(size_type idx, std::conditional_t* rv) noexcept; size_type m_idx; std::conditional_t* m_rv; }; template ring_iterator operator+(ring_iterator it, std::ptrdiff_t) noexcept; template ring_iterator operator-(ring_iterator it, std::ptrdiff_t) noexcept; } // namespace sg14 // Sample implementation template void sg14::null_popper::operator()(T&) const noexcept {} template T sg14::default_popper::operator()(T& t) const { return std::move(t); } template sg14::copy_popper::copy_popper(T t) : m_copy(std::move(t)) {} template T sg14::copy_popper::operator()(T& t) const { T old = m_copy; using std::swap; swap(old, t); return old; } template template sg14::ring_span::ring_span(ContiguousIterator begin, ContiguousIterator end, Popper p) noexcept : m_data(&*begin) , m_size(0) , m_capacity(end - begin) , m_front_idx(0) , m_popper(std::move(p)) {} template template sg14::ring_span::ring_span(ContiguousIterator begin, ContiguousIterator end, ContiguousIterator first, size_type size, Popper p) noexcept : m_data(&*begin) , m_size(size) , m_capacity(end - begin) , m_front_idx(first - begin) , m_popper(std::move(p)) {} template bool sg14::ring_span::empty() const noexcept { return m_size == 0; } template bool sg14::ring_span::full() const noexcept { return m_size == m_capacity; } template typename sg14::ring_span::size_type sg14::ring_span::size() const noexcept { return m_size; } template typename sg14::ring_span::size_type sg14::ring_span::capacity() const noexcept { return m_capacity; } template typename sg14::ring_span::reference sg14::ring_span::front() noexcept { return *begin(); } template typename sg14::ring_span::reference sg14::ring_span::back() noexcept { return *(--end()); } template typename sg14::ring_span::const_reference sg14::ring_span::front() const noexcept { return *begin(); } template typename sg14::ring_span::const_reference sg14::ring_span::back() const noexcept { return *(--end()); } template typename sg14::ring_span::iterator sg14::ring_span::begin() noexcept { return iterator(m_front_idx, this); } template typename sg14::ring_span::const_iterator sg14::ring_span::begin() const noexcept { return const_iterator(m_front_idx, this); } template typename sg14::ring_span::iterator sg14::ring_span::end() noexcept { return iterator(size() + m_front_idx, this); } template typename sg14::ring_span::const_iterator sg14::ring_span::end() const noexcept { return const_iterator(size() + m_front_idx, this); } template typename sg14::ring_span::const_iterator sg14::ring_span::cbegin() const noexcept { return begin(); } template typename sg14::ring_span::reverse_iterator sg14::ring_span::rbegin() noexcept { return reverse_iterator(end()); } template typename sg14::ring_span::const_reverse_iterator sg14::ring_span::rbegin() const noexcept { return const_reverse_iterator(end()); } template typename sg14::ring_span::const_reverse_iterator sg14::ring_span::crbegin() const noexcept { return const_reverse_iterator(end()); } template typename sg14::ring_span::const_iterator sg14::ring_span::cend() const noexcept { return end(); } template typename sg14::ring_span::reverse_iterator sg14::ring_span::rend() noexcept { return reverse_iterator(begin()); } template typename sg14::ring_span::const_reverse_iterator sg14::ring_span::rend() const noexcept { return const_reverse_iterator(begin()); } template typename sg14::ring_span::const_reverse_iterator sg14::ring_span::crend() const noexcept { return const_reverse_iterator(begin()); } template template void sg14::ring_span::push_back(const T& value) noexcept(std::is_nothrow_copy_assignable::value) { m_data[back_idx()] = value; increase_size(); } template template void sg14::ring_span::push_back(T&& value) noexcept(std::is_nothrow_move_assignable::value) { m_data[back_idx()] = std::move(value); increase_size(); } template template void sg14::ring_span::emplace_back(FromType&&... from_value) noexcept(std::is_nothrow_constructible::value && std::is_nothrow_move_assignable::value) { m_data[back_idx()] = T(std::forward(from_value)...); increase_size(); } template auto sg14::ring_span::pop_front() { assert(m_size != 0); auto old_front_idx = m_front_idx; m_front_idx = (m_front_idx + 1) % m_capacity; --m_size; return m_popper(m_data[old_front_idx]); } template void sg14::ring_span::swap(sg14::ring_span& rhs) noexcept//(std::is_nothrow_swappable::value) { using std::swap; swap(m_data, rhs.m_data); swap(m_size, rhs.m_size); swap(m_capacity, rhs.m_capacity); swap(m_front_idx, rhs.m_front_idx); swap(m_popper, rhs.m_popper); } template typename sg14::ring_span::reference sg14::ring_span::at(size_type i) noexcept { return m_data[i % m_capacity]; } template typename sg14::ring_span::const_reference sg14::ring_span::at(size_type i) const noexcept { return m_data[i % m_capacity]; } template typename sg14::ring_span::size_type sg14::ring_span::back_idx() const noexcept { return (m_front_idx + m_size) % m_capacity; } template void sg14::ring_span::increase_size() noexcept { if (++m_size > m_capacity) { m_size = m_capacity; m_front_idx = (m_front_idx + 1) % m_capacity; } } template sg14::ring_iterator::operator sg14::ring_iterator() const noexcept { return sg14::ring_iterator(m_idx, m_rv); } template template bool sg14::ring_iterator::operator==(const sg14::ring_iterator& rhs) const noexcept { return (m_idx == rhs.m_idx) && (m_rv == rhs.m_rv); } template template bool sg14::ring_iterator::operator!=(const sg14::ring_iterator& rhs) const noexcept { return !(*this == rhs); } template template bool sg14::ring_iterator::operator<(const sg14::ring_iterator& rhs) const noexcept { return (m_idx < rhs.m_idx) && (m_rv == rhs.m_rv); } template template bool sg14::ring_iterator::operator<=(const sg14::ring_iterator& rhs) const noexcept { return !(rhs < *this); } template template bool sg14::ring_iterator::operator>(const sg14::ring_iterator& rhs) const noexcept { return (rhs < *this); } template template bool sg14::ring_iterator::operator>=(const sg14::ring_iterator& rhs) const noexcept { return !(*this < rhs); } template typename sg14::ring_iterator::reference sg14::ring_iterator::operator*() const noexcept { return m_rv->at(m_idx); } template sg14::ring_iterator& sg14::ring_iterator::operator++() noexcept { ++m_idx; return *this; } template sg14::ring_iterator sg14::ring_iterator::operator++(int) noexcept { auto r(*this); ++*this; return r; } template sg14::ring_iterator& sg14::ring_iterator::operator--() noexcept { --m_idx; return *this; } template sg14::ring_iterator sg14::ring_iterator::operator--(int) noexcept { auto r(*this); --*this; return r; } template sg14::ring_iterator& sg14::ring_iterator::operator+=(std::ptrdiff_t i) noexcept { this->m_idx += i; return *this; } template sg14::ring_iterator& sg14::ring_iterator::operator-=(std::ptrdiff_t i) noexcept { this->m_idx -= i; return *this; } template template std::ptrdiff_t sg14::ring_iterator::operator-(const sg14::ring_iterator& rhs) const noexcept { return static_cast(this->m_idx) - static_cast(rhs.m_idx); } template sg14::ring_iterator::ring_iterator(typename sg14::ring_iterator::size_type idx, std::conditional_t* rv) noexcept : m_idx(idx) , m_rv(rv) {} namespace sg14 { template void swap(ring_span& a, ring_span& b) noexcept { a.swap(b); } template ring_iterator operator+(ring_iterator it, std::ptrdiff_t i) noexcept { it += i; return it; } template ring_iterator operator-(ring_iterator it, std::ptrdiff_t i) noexcept { it -= i; return it; } } // namespace sg14 ================================================ FILE: SG14/slot_map.h ================================================ /* * Boost Software License - Version 1.0 - August 17th, 2003 * * Permission is hereby granted, free of charge, to any person or organization * obtaining a copy of the software and accompanying documentation covered by * this license (the "Software") to use, reproduce, display, distribute, * execute, and transmit the Software, and to prepare derivative works of the * Software, and to permit third-parties to whom the Software is furnished to * do so, all subject to the following: * * The copyright notices in the Software and this entire statement, including * the above license grant, this restriction and the following disclaimer, * must be included in all copies of the Software, in whole or in part, and * all derivative works of the Software, unless such copies or derivative * works are solely in the form of machine-executable object code generated by * a source language processor. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #pragma once #include #include #include #ifndef SLOT_MAP_THROW_EXCEPTION #include #define SLOT_MAP_THROW_EXCEPTION(type, ...) throw type(__VA_ARGS__) #endif namespace stdext { namespace slot_map_detail { template struct priority_tag : public priority_tag {}; template<> struct priority_tag<0> {}; template inline auto reserve_if_possible(Ctr&, SizeType, priority_tag<0>) -> void {} template inline auto reserve_if_possible(Ctr& ctr, SizeType n, priority_tag<1>) -> decltype(void(ctr.reserve(n))) { ctr.reserve(n); } template inline void reserve_if_possible(Ctr& ctr, const SizeType& n) { slot_map_detail::reserve_if_possible(ctr, n, priority_tag<1>{}); } } // namespace slot_map_detail template< class T, class Key = std::pair, template class Container = std::vector > class slot_map { #if __cplusplus >= 201703L static constexpr auto get_index(const Key& k) { const auto& [idx, gen] = k; return idx; } static constexpr auto get_generation(const Key& k) { const auto& [idx, gen] = k; return gen; } template static constexpr void set_index(Key& k, Integral value) { auto& [idx, gen] = k; idx = static_cast(value); } static constexpr void increment_generation(Key& k) { auto& [idx, gen] = k; ++gen; } #else static constexpr auto get_index(const Key& k) { using std::get; return get<0>(k); } static constexpr auto get_generation(const Key& k) { using std::get; return get<1>(k); } template static constexpr void set_index(Key& k, Integral value) { using std::get; get<0>(k) = static_cast(value); } static constexpr void increment_generation(Key& k) { using std::get; ++get<1>(k); } #endif using slot_iterator = typename Container::iterator; public: using key_type = Key; using mapped_type = T; using key_index_type = decltype(slot_map::get_index(std::declval())); using key_generation_type = decltype(slot_map::get_generation(std::declval())); using container_type = Container; using reference = typename container_type::reference; using const_reference = typename container_type::const_reference; using pointer = typename container_type::pointer; using const_pointer = typename container_type::const_pointer; using iterator = typename container_type::iterator; using const_iterator = typename container_type::const_iterator; using reverse_iterator = typename container_type::reverse_iterator; using const_reverse_iterator = typename container_type::const_reverse_iterator; using size_type = typename container_type::size_type; using value_type = typename container_type::value_type; static_assert(std::is_same::value, "Container::value_type must be identical to T"); constexpr slot_map() = default; constexpr slot_map(const slot_map&) = default; constexpr slot_map(slot_map&&) = default; constexpr slot_map& operator=(const slot_map&) = default; constexpr slot_map& operator=(slot_map&&) = default; ~slot_map() = default; // The at() functions have both generation counter checking // and bounds checking, and throw if either check fails. // O(1) time and space complexity. // constexpr reference at(const key_type& key) { auto value_iter = this->find(key); if (value_iter == this->end()) { SLOT_MAP_THROW_EXCEPTION(std::out_of_range, "at"); } return *value_iter; } constexpr const_reference at(const key_type& key) const { auto value_iter = this->find(key); if (value_iter == this->end()) { SLOT_MAP_THROW_EXCEPTION(std::out_of_range, "at"); } return *value_iter; } // The bracket operator[] has a generation counter check. // If the check fails it is undefined behavior. // O(1) time and space complexity. // constexpr reference operator[](const key_type& key) { return *find_unchecked(key); } constexpr const_reference operator[](const key_type& key) const { return *find_unchecked(key); } // The find() functions have generation counter checking. // If the check fails, the result of end() is returned. // O(1) time and space complexity. // constexpr iterator find(const key_type& key) { auto slot_index = get_index(key); if (slot_index >= slots_.size()) { return end(); } auto slot_iter = std::next(slots_.begin(), slot_index); if (get_generation(*slot_iter) != get_generation(key)) { return end(); } auto value_iter = std::next(values_.begin(), get_index(*slot_iter)); return value_iter; } constexpr const_iterator find(const key_type& key) const { auto slot_index = get_index(key); if (slot_index >= slots_.size()) { return end(); } auto slot_iter = std::next(slots_.begin(), slot_index); if (get_generation(*slot_iter) != get_generation(key)) { return end(); } auto value_iter = std::next(values_.begin(), get_index(*slot_iter)); return value_iter; } // The find_unchecked() functions perform no checks of any kind. // O(1) time and space complexity. // constexpr iterator find_unchecked(const key_type& key) { auto slot_iter = std::next(slots_.begin(), get_index(key)); auto value_iter = std::next(values_.begin(), get_index(*slot_iter)); return value_iter; } constexpr const_iterator find_unchecked(const key_type& key) const { auto slot_iter = std::next(slots_.begin(), get_index(key)); auto value_iter = std::next(values_.begin(), get_index(*slot_iter)); return value_iter; } // All begin() and end() variations have O(1) time and space complexity. // constexpr iterator begin() { return values_.begin(); } constexpr iterator end() { return values_.end(); } constexpr const_iterator begin() const { return values_.begin(); } constexpr const_iterator end() const { return values_.end(); } constexpr const_iterator cbegin() const { return values_.begin(); } constexpr const_iterator cend() const { return values_.end(); } constexpr reverse_iterator rbegin() { return values_.rbegin(); } constexpr reverse_iterator rend() { return values_.rend(); } constexpr const_reverse_iterator rbegin() const { return values_.rbegin(); } constexpr const_reverse_iterator rend() const { return values_.rend(); } constexpr const_reverse_iterator crbegin() const { return values_.rbegin(); } constexpr const_reverse_iterator crend() const { return values_.rend(); } // Functions for checking the size and capacity of the adapted container // have the same complexity as the adapted container. // reserve(n) has the complexity of the adapted container, and uses // additional time which is linear on the increase in size. // This is caused by adding the new slots to the free list. // constexpr bool empty() const { return values_.size() == 0; } constexpr size_type size() const { return values_.size(); } // constexpr size_type max_size() const; TODO, NO SEMANTICS constexpr void reserve(size_type n) { slot_map_detail::reserve_if_possible(values_, n); slot_map_detail::reserve_if_possible(reverse_map_, n); reserve_slots(n); } template, class = decltype(std::declval().capacity())> constexpr size_type capacity() const { return values_.capacity(); } // Functions for accessing and modifying the size of the slots container. // These are beneficial as allocating more slots than values will cause the // generation counter increases to be more evenly distributed across the slots. // constexpr void reserve_slots(size_type n) { slot_map_detail::reserve_if_possible(slots_, n); key_index_type original_num_slots = static_cast(slots_.size()); if (original_num_slots < n) { slots_.emplace_back(key_type{next_available_slot_index_, key_generation_type{}}); key_index_type last_new_slot = original_num_slots; --n; while (last_new_slot != n) { slots_.emplace_back(key_type{last_new_slot, key_generation_type{}}); ++last_new_slot; } next_available_slot_index_ = last_new_slot; } } constexpr size_type slot_count() const { return slots_.size(); } // These operations have O(1) time and space complexity. // When size() == capacity() an allocation is required // which has O(n) time and space complexity. // constexpr key_type insert(const mapped_type& value) { return this->emplace(value); } constexpr key_type insert(mapped_type&& value) { return this->emplace(std::move(value)); } template constexpr key_type emplace(Args&&... args) { auto value_pos = values_.size(); values_.emplace_back(std::forward(args)...); reverse_map_.emplace_back(next_available_slot_index_); if (next_available_slot_index_ == slots_.size()) { auto idx = next_available_slot_index_; ++idx; slots_.emplace_back(key_type{idx, key_generation_type{}}); // make a new slot last_available_slot_index_ = idx; } auto slot_iter = std::next(slots_.begin(), next_available_slot_index_); if (next_available_slot_index_ == last_available_slot_index_) { next_available_slot_index_ = static_cast(slots_.size()); last_available_slot_index_ = next_available_slot_index_; } else { next_available_slot_index_ = this->get_index(*slot_iter); } this->set_index(*slot_iter, value_pos); key_type result = *slot_iter; this->set_index(result, std::distance(slots_.begin(), slot_iter)); return result; } // Each erase() version has an O(1) time complexity per value // and O(1) space complexity. // constexpr iterator erase(iterator pos) { return this->erase(const_iterator(pos)); } constexpr iterator erase(iterator first, iterator last) { return this->erase(const_iterator(first), const_iterator(last)); } constexpr iterator erase(const_iterator pos) { auto slot_iter = this->slot_iter_from_value_iter(pos); return erase_slot_iter(slot_iter); } constexpr iterator erase(const_iterator first, const_iterator last) { // Must use indexes, not iterators, because Container iterators might be invalidated by pop_back auto first_index = std::distance(this->cbegin(), first); auto last_index = std::distance(this->cbegin(), last); while (last_index != first_index) { --last_index; auto iter = std::next(this->cbegin(), last_index); this->erase(iter); } return std::next(this->begin(), first_index); } constexpr size_type erase(const key_type& key) { auto iter = this->find(key); if (iter == this->end()) { return 0; } this->erase(iter); return 1; } // clear() has O(n) time complexity and O(1) space complexity. // It also has semantics differing from erase(begin(), end()) // in that it also resets the generation counter of every slot // and rebuilds the free list. // constexpr void clear() { // This resets the generation counters, which "undefined-behavior-izes" at() and find() for the old keys. slots_.clear(); values_.clear(); reverse_map_.clear(); next_available_slot_index_ = key_index_type{}; last_available_slot_index_ = key_index_type{}; } // swap is not mentioned in P0661r1 but it should be. constexpr void swap(slot_map& rhs) { using std::swap; swap(slots_, rhs.slots_); swap(values_, rhs.values_); swap(reverse_map_, rhs.reverse_map_); swap(next_available_slot_index_, rhs.next_available_slot_index_); swap(last_available_slot_index_, rhs.last_available_slot_index_); } protected: // These accessors are not part of P0661R2 but are "modernized" versions // of the protected interface of std::priority_queue, std::stack, etc. constexpr Container& c() & noexcept { return values_; } constexpr const Container& c() const& noexcept { return values_; } constexpr Container&& c() && noexcept { return std::move(values_); } constexpr const Container&& c() const&& noexcept { return std::move(values_); } private: constexpr slot_iterator slot_iter_from_value_iter(const_iterator value_iter) { auto value_index = std::distance(const_iterator(values_.begin()), value_iter); auto slot_index = *std::next(reverse_map_.begin(), value_index); return std::next(slots_.begin(), slot_index); } constexpr iterator erase_slot_iter(slot_iterator slot_iter) { auto slot_index = std::distance(slots_.begin(), slot_iter); auto value_index = get_index(*slot_iter); auto value_iter = std::next(values_.begin(), value_index); auto value_back_iter = std::prev(values_.end()); if (value_iter != value_back_iter) { auto slot_back_iter = slot_iter_from_value_iter(value_back_iter); *value_iter = std::move(*value_back_iter); this->set_index(*slot_back_iter, value_index); auto reverse_map_iter = std::next(reverse_map_.begin(), value_index); *reverse_map_iter = static_cast(std::distance(slots_.begin(), slot_back_iter)); } values_.pop_back(); reverse_map_.pop_back(); // Expire this key. if (next_available_slot_index_ == slots_.size()) { next_available_slot_index_ = static_cast(slot_index); last_available_slot_index_ = static_cast(slot_index); } else { auto last_slot_iter = std::next(slots_.begin(), last_available_slot_index_); this->set_index(*last_slot_iter, slot_index); last_available_slot_index_ = static_cast(slot_index); } this->increment_generation(*slot_iter); return std::next(values_.begin(), value_index); } Container slots_; // high_water_mark() entries Container reverse_map_; // exactly size() entries Container values_; // exactly size() entries key_index_type next_available_slot_index_{}; key_index_type last_available_slot_index_{}; // Class invariant: // Either next_available_slot_index_ == last_available_slot_index_ == slots_.size(), // or else 0 <= next_available_slot_index_ < slots_.size() and the "key" of that slot // entry points to the subsequent available slot, and so on, until reaching // last_available_slot_index_ (which might equal next_available_slot_index_ if there // is only one available slot at the moment). }; template class Container> constexpr void swap(slot_map& lhs, slot_map& rhs) { lhs.swap(rhs); } } // namespace stdext ================================================ FILE: SG14_test/SG14_test.h ================================================ #if !defined SG14_TEST_2015_06_11_18_24 #define SG14_TEST_2015_06_11_18_24 #undef NDEBUG namespace sg14_test { void flat_map_test(); void flat_set_test(); void inplace_function_test(); void plf_colony_test(); void ring_test(); void slot_map_test(); void uninitialized_test(); void unstable_remove_test(); } #endif ================================================ FILE: SG14_test/flat_map_test.cpp ================================================ #include "SG14_test.h" #include "flat_map.h" #include #include #include #include #if __has_include() #include #endif #include #include namespace { struct AmbiguousEraseWidget { explicit AmbiguousEraseWidget(const char *s) : s_(s) {} template AmbiguousEraseWidget(T) : s_("notfound") {} friend bool operator<(const AmbiguousEraseWidget& a, const AmbiguousEraseWidget& b) { return a.s_ < b.s_; } private: std::string s_; }; struct InstrumentedWidget { static int move_ctors, copy_ctors; InstrumentedWidget() = delete; InstrumentedWidget(const char *s) : s_(s) {} InstrumentedWidget(InstrumentedWidget&& o) noexcept : s_(std::move(o.s_)) { o.is_moved_from = true; move_ctors += 1; } InstrumentedWidget(const InstrumentedWidget& o) : s_(o.s_) { copy_ctors += 1; } InstrumentedWidget& operator=(InstrumentedWidget&& o) noexcept { s_ = std::move(o.s_); o.is_moved_from = true; return *this; } InstrumentedWidget& operator=(const InstrumentedWidget&) = default; friend bool operator<(const InstrumentedWidget& a, const InstrumentedWidget& b) { return a.s_ < b.s_; } std::string str() const { return s_; } bool is_moved_from = false; private: std::string s_; }; int InstrumentedWidget::move_ctors = 0; int InstrumentedWidget::copy_ctors = 0; static void AmbiguousEraseTest() { stdext::flat_map fs; fs.emplace("a", 1); fs.emplace("b", 2); fs.emplace("c", 3); assert(fs.size() == 3); fs.erase(AmbiguousEraseWidget("a")); // calls erase(const Key&) assert(fs.size() == 2); fs.erase(fs.begin()); // calls erase(iterator) assert(fs.size() == 1); fs.erase(fs.cbegin()); // calls erase(const_iterator) assert(fs.size() == 0); } static void ExtractDoesntSwapTest() { #if defined(__cpp_lib_memory_resource) // This test fails if extract() is implemented in terms of swap(). { std::pmr::monotonic_buffer_resource mr; std::pmr::polymorphic_allocator a(&mr); stdext::flat_map, std::pmr::vector, std::pmr::vector> fs({{1, 10}, {2, 20}}, a); auto ctrs = std::move(fs).extract(); assert(ctrs.keys.get_allocator() == a); assert(ctrs.values.get_allocator() == a); } #endif // Sanity-check with std::allocator, even though this can't fail. { std::allocator a; stdext::flat_map, std::vector, std::vector> fs({{1, 10}, {2, 20}}, a); auto ctrs = std::move(fs).extract(); assert(ctrs.keys.get_allocator() == a); assert(ctrs.values.get_allocator() == a); } } static void MoveOperationsPilferOwnership() { using FS = stdext::flat_map; InstrumentedWidget::move_ctors = 0; InstrumentedWidget::copy_ctors = 0; FS fs; fs.insert(std::make_pair(InstrumentedWidget("abc"), 1)); assert(InstrumentedWidget::move_ctors == 3); assert(InstrumentedWidget::copy_ctors == 0); fs.emplace(InstrumentedWidget("def"), 1); fs.erase("def"); // poor man's reserve() InstrumentedWidget::copy_ctors = 0; InstrumentedWidget::move_ctors = 0; fs.emplace("def", 1); // is still not directly emplaced; a temporary is created to find() assert(InstrumentedWidget::move_ctors == 1); assert(InstrumentedWidget::copy_ctors == 0); InstrumentedWidget::move_ctors = 0; FS fs2 = std::move(fs); // should just transfer buffer ownership assert(InstrumentedWidget::move_ctors == 0); assert(InstrumentedWidget::copy_ctors == 0); fs = std::move(fs2); // should just transfer buffer ownership assert(InstrumentedWidget::move_ctors == 0); assert(InstrumentedWidget::copy_ctors == 0); FS fs3(fs, std::allocator()); assert(InstrumentedWidget::move_ctors == 0); assert(InstrumentedWidget::copy_ctors == 2); InstrumentedWidget::copy_ctors = 0; FS fs4(std::move(fs), std::allocator()); // should just transfer buffer ownership assert(InstrumentedWidget::move_ctors == 0); assert(InstrumentedWidget::copy_ctors == 0); } static void SortedUniqueConstructionTest() { auto a = stdext::sorted_unique; stdext::sorted_unique_t b; stdext::sorted_unique_t c{}; (void)a; (void)b; (void)c; #if 0 // TODO: GCC cannot compile this struct explicitness_tester { bool test(std::vector) { return true; } bool test(stdext::sorted_unique_t) { return false; } }; explicitness_tester tester; assert(tester.test({}) == true); #endif } static void TryEmplaceTest() { stdext::flat_map fm; std::pair::iterator, bool> pair; if (true) { // try_emplace for a non-existent key does move-from. InstrumentedWidget w("abc"); pair = fm.try_emplace(1, std::move(w)); assert(w.is_moved_from); assert(pair.second); } if (true) { // try_emplace over an existing key is a no-op. InstrumentedWidget w("def"); pair = fm.try_emplace(1, std::move(w)); assert(!w.is_moved_from); assert(!pair.second); assert(pair.first->first == 1); assert(pair.first->second.str() == "abc"); } if (true) { // emplace for a non-existent key does move-from. InstrumentedWidget w("abc"); pair = fm.emplace(2, std::move(w)); assert(w.is_moved_from); assert(pair.second); assert(pair.first->first == 2); assert(pair.first->second.str() == "abc"); } if (true) { // emplace over an existing key is a no-op, but does move-from in order to construct the pair. InstrumentedWidget w("def"); pair = fm.emplace(2, std::move(w)); assert(w.is_moved_from); assert(!pair.second); assert(pair.first->first == 2); assert(pair.first->second.str() == "abc"); } if (true) { // insert-or-assign for a non-existent key does move-construct. InstrumentedWidget w("abc"); pair = fm.insert_or_assign(3, std::move(w)); assert(w.is_moved_from); assert(pair.second); assert(pair.first->first == 3); assert(pair.first->second.str() == "abc"); } if (true) { // insert-or-assign over an existing key does a move-assign. InstrumentedWidget w("def"); pair = fm.insert_or_assign(3, std::move(w)); assert(w.is_moved_from); assert(!pair.second); assert(pair.first->first == 3); assert(pair.first->second.str() == "def"); } } static void VectorBoolSanityTest() { using FM = stdext::flat_map; FM fm; auto it_inserted = fm.emplace(true, false); assert(it_inserted.second); auto it = it_inserted.first; assert(it == fm.begin()); assert(it->first == true); assert(it->first); assert(it->second == false); assert(!it->second); it->second = false; assert(fm.size() == 1); it = fm.emplace_hint(it, false, true); assert(it == fm.begin()); assert(it->first == false); assert(!it->first); assert(it->second == true); assert(it->second); it->second = true; assert(fm.size() == 2); auto count = fm.erase(false); assert(count == 1); assert(fm.size() == 1); it = fm.erase(fm.begin()); assert(fm.empty()); assert(it == fm.begin()); assert(it == fm.end()); assert(fm.find(true) == fm.end()); fm.try_emplace(true, true); assert(fm.find(true) != fm.end()); assert(fm[true] == true); fm[true] = false; assert(fm.find(true) != fm.end()); assert(fm[true] == false); fm.clear(); } #if defined(__cpp_deduction_guides) static bool free_function_less(const int& a, const int& b) { return (a < b); } template static auto flatmap_is_ctadable_from(int, Args&&... args) -> decltype(flat_map(std::forward(args)...), std::true_type{}) { return {}; } template static auto flatmap_is_ctadable_from(long, Args&&...) -> std::false_type { return {}; } #endif // defined(__cpp_deduction_guides) static void DeductionGuideTests() { using stdext::flat_map; #if defined(__cpp_deduction_guides) if (true) { // flat_map(Container) std::vector> v; flat_map fm1(v); static_assert(std::is_same_v>); flat_map fm2 = flat_map(std::deque>()); static_assert(std::is_same_v>); std::list> lst; flat_map fm3(lst); static_assert(std::is_same_v>); #if __cpp_lib_memory_resource std::pmr::vector> pv; flat_map fm4(pv); static_assert(std::is_same_v>); #endif std::initializer_list> il = {{1,"c"}, {5,"b"}, {3,"a"}}; flat_map fm5(il); static_assert(std::is_same_v>); assert(fm5.size() == 3); assert(( fm5 == decltype(fm5)(stdext::sorted_unique, {{1,"c"}, {3,"a"}, {5,"b"}}) )); } if (true) { // flat_map(KeyContainer, MappedContainer) std::vector vi {2,1}; std::vector vs {"a","b"}; flat_map fm1(vi, vs); static_assert(std::is_same_v>); assert(( fm1 == flat_map(stdext::sorted_unique, {{1,"b"}, {2,"a"}}) )); flat_map fm2(std::move(vs), std::move(vi)); static_assert(std::is_same_v>); assert(( fm2 == flat_map(stdext::sorted_unique, {{"a",2}, {"b",1}}) )); } if (true) { // flat_map(Container, Allocator) std::vector> v; flat_map fm1(v, std::allocator()); static_assert(std::is_same_v>); #if __cpp_lib_memory_resource std::pmr::vector> pv; // TODO: neither of these lines compiles, and it's unclear what is INTENDED to happen // flat_map fm2(pv, std::allocator()); // flat_map fm2(pv, std::pmr::polymorphic_allocator()); #endif } if (true) { // flat_map(KeyContainer, MappedContainer, Allocator) std::vector vi {2,1}; std::vector vs {"a","b"}; flat_map fm1(vi, vs, std::allocator()); static_assert(std::is_same_v>); assert(( fm1 == decltype(fm1)(stdext::sorted_unique, {{1,"b"}, {2,"a"}}) )); #if __cpp_lib_memory_resource std::pmr::vector pvi {2,1}; std::pmr::vector pvs {"a","b"}; flat_map fm2(pvi, pvs, std::pmr::polymorphic_allocator()); static_assert(std::is_same_v, std::pmr::vector, std::pmr::vector>>); assert(( fm2 == decltype(fm2)(stdext::sorted_unique, {{1,"b"}, {2,"a"}}) )); #endif } if (true) { // flat_map(sorted_unique_t, Container) std::vector> v; flat_map fm1(stdext::sorted_unique, v); static_assert(std::is_same_v>); flat_map fm2 = flat_map(stdext::sorted_unique, std::deque>()); static_assert(std::is_same_v>); std::list> lst; flat_map fm3(stdext::sorted_unique, lst); static_assert(std::is_same_v>); #if __cpp_lib_memory_resource std::pmr::vector> pv; flat_map fm4(stdext::sorted_unique, pv); static_assert(std::is_same_v>); #endif std::initializer_list> il = {{1,"c"}, {3,"b"}, {5,"a"}}; flat_map fm5(stdext::sorted_unique, il); static_assert(std::is_same_v>); assert(( fm5 == decltype(fm5)(stdext::sorted_unique, {{1,"c"}, {3,"b"}, {5,"a"}}) )); } if (true) { // flat_map(sorted_unique_t, KeyContainer, MappedContainer) std::vector vi {1,2}; std::vector vs {"a","b"}; flat_map fm1(stdext::sorted_unique, vi, vs); static_assert(std::is_same_v>); assert(( fm1 == decltype(fm1)(stdext::sorted_unique, {{1,"a"}, {2,"b"}}) )); flat_map fm2(stdext::sorted_unique, std::move(vs), std::move(vi)); static_assert(std::is_same_v>); assert(( fm2 == decltype(fm2)(stdext::sorted_unique, {{"a",1}, {"b",2}}) )); } if (true) { // flat_map(sorted_unique_t, Container, Allocator) std::vector> v; flat_map fm1(stdext::sorted_unique, v, std::allocator()); static_assert(std::is_same_v>); #if __cpp_lib_memory_resource std::pmr::vector> pv; // TODO: neither of these lines compiles, and it's unclear what is INTENDED to happen // flat_map fm2(stdext::sorted_unique, pv, std::allocator()); // flat_map fm2(stdext::sorted_unique, pv, std::pmr::polymorphic_allocator()); #endif } if (true) { // flat_map(sorted_unique_t, KeyContainer, MappedContainer, Allocator) std::vector vi {2,1}; std::vector vs {"a","b"}; flat_map fm1(stdext::sorted_unique, vs, vi, std::allocator()); static_assert(std::is_same_v>); assert(( fm1 == decltype(fm1)(stdext::sorted_unique, {{"a",2}, {"b",1}}) )); #if __cpp_lib_memory_resource std::pmr::vector pvi {1, 2}; std::pmr::vector pvs {"b","a"}; flat_map fm2(stdext::sorted_unique, pvi, pvs, std::pmr::polymorphic_allocator()); static_assert(std::is_same_v, std::pmr::vector, std::pmr::vector>>); assert(( fm2 == decltype(fm2)(stdext::sorted_unique, {{1,"b"}, {2,"a"}}) )); #endif } if (true) { // flat_map(InputIterator, InputIterator, Compare = Compare()) std::vector> v; flat_map fm1(v.begin(), v.end()); static_assert(std::is_same_v>); std::list> lst; flat_map fm3(lst.begin(), lst.end()); static_assert(std::is_same_v>); #if __cpp_lib_memory_resource std::pmr::vector> pv; flat_map fm4(pv.begin(), pv.end()); static_assert(std::is_same_v>); #endif std::initializer_list> il = {{1,"c"}, {5,"b"}, {3,"a"}}; flat_map fm5(il.begin(), il.end()); static_assert(std::is_same_v>); assert(( fm5 == decltype(fm5)(stdext::sorted_unique, {{1,"c"}, {3,"a"}, {5,"b"}}) )); } if (true) { // flat_map(InputIterator, InputIterator, Compare = Compare()) std::vector> v; flat_map fm1(v.begin(), v.end(), std::less()); static_assert(std::is_same_v>); int x = 3; std::pair arr[] = {{1,2}, {2,3}, {3,4}, {4,5}}; flat_map fm2(arr, arr + 4, [&x](int a, int b){ return (a % x) < (b % x); }); assert(fm2.key_comp()(2, 4) == false); x = 10; assert(fm2.key_comp()(2, 4) == true); x = 3; assert(fm2.begin()[0].first == 3); std::list> lst; flat_map fm3(lst.begin(), lst.end(), std::greater<>()); static_assert(std::is_same_v>>); #if __cpp_lib_memory_resource std::pmr::vector> pv; flat_map fm4(pv.begin(), pv.end(), std::greater()); static_assert(std::is_same_v>>); #endif std::initializer_list> il = {{1,"c"}, {5,"b"}, {3,"a"}}; flat_map fm5(il.begin(), il.end(), std::less()); static_assert(std::is_same_v>); assert(( fm5 == decltype(fm5)(stdext::sorted_unique, {{1,"c"}, {3,"a"}, {5,"b"}}) )); flat_map fm6(arr, arr + 4, free_function_less); static_assert(std::is_same_v>); assert(fm6.key_comp() == free_function_less); assert(( fm6 == decltype(fm6)(stdext::sorted_unique, {{1,2}, {2,3}, {3,4}, {4,5}}, free_function_less) )); } if (true) { // flat_map(InputIterator, InputIterator, Compare, Allocator) std::vector> v; flat_map fm1(v.begin(), v.end(), std::less(), std::allocator()); static_assert(std::is_same_v>); int x = 3; std::pair arr[] = {{1,2}, {2,3}, {3,4}, {4,5}}; flat_map fm2(arr, arr + 4, [&x](int a, int b){ return (a % x) < (b % x); }, std::allocator()); assert(fm2.key_comp()(2, 4) == false); x = 10; assert(fm2.key_comp()(2, 4) == true); x = 3; assert(fm2.begin()[0].first == 3); std::list> lst; flat_map fm3(lst.begin(), lst.end(), std::greater<>(), std::allocator()); static_assert(std::is_same_v>>); #if __cpp_lib_memory_resource std::pmr::vector> pv; flat_map fm4(pv.begin(), pv.end(), std::greater<>(), std::allocator()); static_assert(std::is_same_v>>); assert(!flatmap_is_ctadable_from(0, pv.begin(), pv.end(), std::greater(), std::pmr::polymorphic_allocator())); #endif std::initializer_list> il = {{1,"c"}, {5,"b"}, {3,"a"}}; flat_map fm5(il.begin(), il.end(), std::less(), std::allocator()); static_assert(std::is_same_v>); assert(( fm5 == decltype(fm5)(stdext::sorted_unique, {{1,"c"}, {3,"a"}, {5,"b"}}) )); flat_map fm6(arr, arr + 4, free_function_less, std::allocator()); static_assert(std::is_same_v>); assert(fm6.key_comp() == free_function_less); assert(( fm6 == decltype(fm6)(stdext::sorted_unique, {{1,2}, {2,3}, {3,4}, {4,5}}, free_function_less) )); } if (true) { // flat_map(InputIterator, InputIterator, Allocator) } if (true) { // flat_map(sorted_unique_t, InputIterator, InputIterator, Compare = Compare()) } if (true) { // flat_map(sorted_unique_t, InputIterator, InputIterator, Compare, Allocator) } if (true) { // flat_map(sorted_unique_t, InputIterator, InputIterator, Allocator) } if (true) { // flat_map(std::initializer_list>, Compare = Compare()) } if (true) { // flat_map(std::initializer_list>, Compare, Allocator) } if (true) { // flat_map(std::initializer_list>, Allocator) } if (true) { // flat_map(sorted_unique_t, std::initializer_list>, Compare = Compare()) } if (true) { // flat_map(sorted_unique_t, std::initializer_list>, Compare, Allocator) } if (true) { // flat_map(sorted_unique_t, std::initializer_list>, Allocator) } #endif // defined(__cpp_deduction_guides) } template static void ConstructionTest() { static_assert(std::is_same::value, ""); static_assert(std::is_convertible::value, ""); using Mapped = typename FS::mapped_type; using Str = std::conditional_t::value, std::string, Mapped>; using Compare = typename FS::key_compare; std::vector keys = {1, 3, 5}; std::vector values = {"a", "c", "b"}; std::vector> pairs = { {1, "a"}, {3, "c"}, {5, "b"}, }; if (true) { FS fs; // default constructor fs = { {1, "a"}, {3, "c"}, {5, "b"}, }; assert(std::is_sorted(fs.keys().begin(), fs.keys().end(), fs.key_comp())); assert(std::is_sorted(fs.begin(), fs.end(), fs.value_comp())); assert(fs[1] == Str("a")); assert(fs[3] == Str("c")); assert(fs[5] == Str("b")); } for (auto&& fs : { FS({{1, "a"}, {3, "c"}, {5, "b"}}), FS(pairs.begin(), pairs.end()), FS(pairs.rbegin(), pairs.rend()), FS(pairs, Compare()), FS({{1, "a"}, {3, "c"}, {5, "b"}}, Compare()), FS(pairs.begin(), pairs.end(), Compare()), FS(pairs.rbegin(), pairs.rend(), Compare()), }) { assert(std::is_sorted(fs.keys().begin(), fs.keys().end(), fs.key_comp())); assert(std::is_sorted(fs.begin(), fs.end(), fs.value_comp())); assert(fs.find(0) == fs.end()); assert(fs.find(1) != fs.end()); assert(fs.find(2) == fs.end()); assert(fs.find(3) != fs.end()); assert(fs.find(4) == fs.end()); assert(fs.find(5) != fs.end()); assert(fs.find(6) == fs.end()); assert(fs.at(1) == Str("a")); assert(fs.at(3) == Str("c")); assert(fs.find(5)->second == Str("b")); } if (std::is_sorted(keys.begin(), keys.end(), Compare())) { for (auto&& fs : { FS(stdext::sorted_unique, pairs), FS(stdext::sorted_unique, pairs.begin(), pairs.end()), FS(stdext::sorted_unique, {{1, "a"}, {3, "c"}, {5, "b"}}), FS(stdext::sorted_unique, pairs, Compare()), FS(stdext::sorted_unique, pairs.begin(), pairs.end(), Compare()), FS(stdext::sorted_unique, {{1, "a"}, {3, "c"}, {5, "b"}}, Compare()), }) { assert(std::is_sorted(fs.keys().begin(), fs.keys().end(), fs.key_comp())); assert(std::is_sorted(fs.begin(), fs.end(), fs.value_comp())); assert(fs.at(1) == Str("a")); assert(fs.at(3) == Str("c")); assert(fs.find(5)->second == Str("b")); } } } template static void InsertOrAssignTest() { FM fm; const char *str = "a"; using Mapped = typename FM::mapped_type; using Str = std::conditional_t::value, std::string, Mapped>; fm.insert_or_assign(1, str); assert(fm.at(1) == Str("a")); assert(fm[1] == Str("a")); fm.insert_or_assign(2, std::move(str)); assert(fm.at(2) == Str("a")); assert(fm[2] == Str("a")); fm.insert_or_assign(2, "b"); assert(fm.at(2) == Str("b")); assert(fm[2] == Str("b")); fm.insert_or_assign(3, "c"); assert(fm.at(3) == Str("c")); assert(fm[3] == Str("c")); // With hints. fm.insert_or_assign(fm.begin(), 1, str); assert(fm.at(1) == Str("a")); assert(fm[1] == Str("a")); fm.insert_or_assign(fm.begin()+2, 2, std::move(str)); assert(fm.at(2) == Str("a")); assert(fm[2] == Str("a")); fm.insert_or_assign(fm.end(), 2, "c"); assert(fm.at(2) == Str("c")); assert(fm[2] == Str("c")); fm.insert_or_assign(fm.end() - 1, 3, "b"); assert(fm.at(3) == Str("b")); assert(fm[3] == Str("b")); } template static void SpecialMemberTest() { static_assert(std::is_default_constructible::value, ""); static_assert(std::is_nothrow_move_constructible::value == std::is_nothrow_move_constructible::value && std::is_nothrow_move_constructible::value && std::is_nothrow_move_constructible::value, ""); static_assert(std::is_copy_constructible::value, ""); static_assert(std::is_copy_assignable::value, ""); static_assert(std::is_move_assignable::value, ""); static_assert(std::is_nothrow_destructible::value, ""); static_assert(std::is_default_constructible::value, ""); static_assert(std::is_nothrow_move_constructible::value == std::is_nothrow_move_constructible::value && std::is_nothrow_move_constructible::value, ""); static_assert(std::is_copy_constructible::value, ""); static_assert(std::is_copy_assignable::value, ""); static_assert(std::is_move_assignable::value, ""); static_assert(std::is_nothrow_destructible::value, ""); } template static void ComparisonOperatorsTest() { const char *abc[] = {"", "a", "b", "c"}; FM fm1 = { {1, abc[2]}, {2, abc[3]}, }; FM fm2 = { {1, abc[2]}, {2, abc[3]}, }; // {1b, 2c} is equal to {1b, 2c}. assert(fm1 == fm2); assert(!(fm1 != fm2)); assert(!(fm1 < fm2)); assert(!(fm1 > fm2)); assert(fm1 <= fm2); assert(fm1 >= fm2); fm2[2] = abc[1]; // {1b, 2c} is greater than {1b, 2a}. assert(!(fm1 == fm2)); assert(fm1 != fm2); assert(!(fm1 < fm2)); assert(fm1 > fm2); assert(!(fm1 <= fm2)); assert(fm1 >= fm2); fm2.erase(2); fm2.insert({0, abc[3]}); // {1b, 2c} is greater than {0c, 1b}. assert(!(fm1 == fm2)); assert(fm1 != fm2); assert(!(fm1 < fm2)); assert(fm1 > fm2); assert(!(fm1 <= fm2)); assert(fm1 >= fm2); } template static void SearchTest() { FM fm{{1, "a"}, {2, "b"}, {3, "c"}}; auto it = fm.lower_bound(2); auto cit = const_cast(fm).lower_bound(2); assert(it == fm.begin() + 1); assert(cit == it); it = fm.upper_bound(2); cit = const_cast(fm).upper_bound(2); assert(it == fm.begin() + 2); assert(cit == it); auto itpair = fm.equal_range(2); auto citpair = const_cast(fm).equal_range(2); assert(itpair.first == fm.begin() + 1); assert(itpair.second == fm.begin() + 2); assert(citpair == decltype(citpair)(itpair)); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); } } // anonymous namespace void sg14_test::flat_map_test() { AmbiguousEraseTest(); ExtractDoesntSwapTest(); MoveOperationsPilferOwnership(); SortedUniqueConstructionTest(); TryEmplaceTest(); VectorBoolSanityTest(); DeductionGuideTests(); // Test the most basic flat_set. { using FS = stdext::flat_map; ConstructionTest(); SpecialMemberTest(); InsertOrAssignTest(); ComparisonOperatorsTest(); SearchTest(); } // Test a custom comparator. { using FS = stdext::flat_map>; ConstructionTest(); SpecialMemberTest(); InsertOrAssignTest(); ComparisonOperatorsTest(); SearchTest(); } // Test a transparent comparator. { using FS = stdext::flat_map>; ConstructionTest(); SpecialMemberTest(); InsertOrAssignTest(); ComparisonOperatorsTest(); SearchTest(); } // Test a custom container. { using FS = stdext::flat_map, std::deque>; ConstructionTest(); SpecialMemberTest(); InsertOrAssignTest(); ComparisonOperatorsTest(); SearchTest(); } #if defined(__cpp_lib_memory_resource) // Test a pmr container. { using FS = stdext::flat_map, std::pmr::vector>; ConstructionTest(); SpecialMemberTest(); InsertOrAssignTest(); ComparisonOperatorsTest(); SearchTest(); } // Test a pmr container with uses-allocator construction! { using FS = stdext::flat_map, std::pmr::vector>; ConstructionTest(); SpecialMemberTest(); InsertOrAssignTest(); ComparisonOperatorsTest(); SearchTest(); } #endif } #ifdef TEST_MAIN int main() { sg14_test::flat_map_test(); } #endif ================================================ FILE: SG14_test/flat_set_test.cpp ================================================ #include "SG14_test.h" #include "flat_set.h" #include #include #include #if __has_include() #include #endif #include #include namespace { struct AmbiguousEraseWidget { friend bool operator<(const AmbiguousEraseWidget& a, const AmbiguousEraseWidget& b) { return a.s_ < b.s_; } using iterator = stdext::flat_set::iterator; using const_iterator = stdext::flat_set::const_iterator; explicit AmbiguousEraseWidget(const char *s) : s_(s) {} AmbiguousEraseWidget(iterator) : s_("notfound") {} AmbiguousEraseWidget(const_iterator) : s_("notfound") {} private: std::string s_; }; struct InstrumentedWidget { static int move_ctors, copy_ctors; InstrumentedWidget(const char *s) : s_(s) {} InstrumentedWidget(InstrumentedWidget&& o) : s_(std::move(o.s_)) { move_ctors += 1; } InstrumentedWidget(const InstrumentedWidget& o) : s_(o.s_) { copy_ctors += 1; } InstrumentedWidget& operator=(InstrumentedWidget&&) = default; InstrumentedWidget& operator=(const InstrumentedWidget&) = default; friend bool operator<(const InstrumentedWidget& a, const InstrumentedWidget& b) { return a.s_ < b.s_; } private: std::string s_; }; int InstrumentedWidget::move_ctors = 0; int InstrumentedWidget::copy_ctors = 0; static void AmbiguousEraseTest() { stdext::flat_set fs; fs.emplace("a"); fs.emplace("b"); fs.emplace("c"); assert(fs.size() == 3); fs.erase(AmbiguousEraseWidget("a")); // calls erase(const Key&) assert(fs.size() == 2); fs.erase(fs.cbegin()); // calls erase(const_iterator) assert(fs.size() == 1); #if __cplusplus >= 201703L fs.erase(fs.begin()); // calls erase(iterator) assert(fs.size() == 0); #endif } static void ExtractDoesntSwapTest() { #if defined(__cpp_lib_memory_resource) // This test fails if extract() is implemented in terms of swap(). { std::pmr::monotonic_buffer_resource mr; std::pmr::polymorphic_allocator a(&mr); stdext::flat_set, std::pmr::vector> fs({1, 2}, a); std::pmr::vector v = std::move(fs).extract(); assert(v.get_allocator() == a); assert(fs.empty()); } #endif // Sanity-check with std::allocator, even though this can't fail. { std::allocator a; stdext::flat_set, std::vector> fs({1, 2}, a); std::vector v = std::move(fs).extract(); assert(v.get_allocator() == a); assert(fs.empty()); } } struct ThrowingSwapException {}; struct ComparatorWithThrowingSwap { std::function cmp_; static bool please_throw; ComparatorWithThrowingSwap(std::function f) : cmp_(f) {} friend void swap(ComparatorWithThrowingSwap& a, ComparatorWithThrowingSwap& b) { if (please_throw) throw ThrowingSwapException(); a.cmp_.swap(b.cmp_); } bool operator()(int a, int b) const { return cmp_(a, b); } }; bool ComparatorWithThrowingSwap::please_throw = false; static void ThrowingSwapDoesntBreakInvariants() { using std::swap; stdext::flat_set fsx({1,2,3}, ComparatorWithThrowingSwap(std::less())); stdext::flat_set fsy({4,5,6}, ComparatorWithThrowingSwap(std::greater())); if (true) { ComparatorWithThrowingSwap::please_throw = false; swap(fsx, fsy); // should swap both the comparators and the containers fsx.insert(7); fsy.insert(8); std::vector expected_fsx = {7, 6, 5, 4}; std::vector expected_fsy = {1, 2, 3, 8}; assert(expected_fsx.size() == fsx.size()); assert(std::equal(expected_fsx.begin(), expected_fsx.end(), fsx.begin())); assert(expected_fsy.size() == fsy.size()); assert(std::equal(expected_fsy.begin(), expected_fsy.end(), fsy.begin())); } // However, if ComparatorWithThrowingSwap::please_throw were // set to `true`, then flat_set's behavior would be undefined. } static void VectorBoolSanityTest() { #if __cplusplus >= 201402L // C++11 doesn't support vector::emplace using FS = stdext::flat_set; FS fs; auto it_inserted = fs.emplace(true); assert(it_inserted.second); auto it = it_inserted.first; assert(it == fs.begin()); assert(fs.size() == 1); it = fs.emplace_hint(it, false); assert(it == fs.begin()); assert(fs.size() == 2); auto count = fs.erase(false); assert(count == 1); assert(fs.size() == 1); it = fs.erase(fs.begin()); assert(fs.empty()); assert(it == fs.begin()); assert(it == fs.end()); #endif } struct VectorBoolEvilComparator { bool operator()(bool a, bool b) const { return a < b; } template bool operator()(T a, U b) const { static_assert(sizeof(T) < 0, "should never instantiate this call operator"); return a < b; } }; static void VectorBoolEvilComparatorTest() { using FS = stdext::flat_set; FS fs; (void)fs.lower_bound(true); (void)fs.upper_bound(true); (void)fs.equal_range(true); const FS& cfs = fs; (void)cfs.lower_bound(true); (void)cfs.upper_bound(true); (void)cfs.equal_range(true); } static void MoveOperationsPilferOwnership() { using FS = stdext::flat_set; InstrumentedWidget::move_ctors = 0; InstrumentedWidget::copy_ctors = 0; FS fs; fs.insert(InstrumentedWidget("abc")); assert(InstrumentedWidget::move_ctors == 1); assert(InstrumentedWidget::copy_ctors == 0); fs.emplace(InstrumentedWidget("def")); fs.erase("def"); // poor man's reserve() InstrumentedWidget::copy_ctors = 0; InstrumentedWidget::move_ctors = 0; fs.emplace("def"); // is still not directly emplaced; a temporary is created to find() assert(InstrumentedWidget::move_ctors == 1); assert(InstrumentedWidget::copy_ctors == 0); InstrumentedWidget::move_ctors = 0; FS fs2 = std::move(fs); // should just transfer buffer ownership assert(InstrumentedWidget::move_ctors == 0); assert(InstrumentedWidget::copy_ctors == 0); fs = std::move(fs2); // should just transfer buffer ownership assert(InstrumentedWidget::move_ctors == 0); assert(InstrumentedWidget::copy_ctors == 0); FS fs3(fs, std::allocator()); assert(InstrumentedWidget::move_ctors == 0); assert(InstrumentedWidget::copy_ctors == 2); InstrumentedWidget::copy_ctors = 0; FS fs4(std::move(fs), std::allocator()); // should just transfer buffer ownership assert(InstrumentedWidget::move_ctors == 0); assert(InstrumentedWidget::copy_ctors == 0); } template static void ConstructionTest() { static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); using Compare = typename FS::key_compare; std::vector vec = {1, 3, 5}; if (true) { FS fs; // default constructor fs = {1, 3, 5}; // assignment operator assert(std::is_sorted(fs.begin(), fs.end(), fs.key_comp())); } if (true) { FS fs {1, 3, 1, 5, 3}; assert(fs.size() == 3); // assert that uniqueing takes place std::vector vec2 = {1, 3, 1, 5, 3}; FS fs2(vec2); assert(fs2.size() == 3); // assert that uniqueing takes place } for (auto&& fs : { FS(vec), FS({1, 3, 5}), FS(vec.begin(), vec.end()), FS(vec.rbegin(), vec.rend()), FS(vec, Compare()), FS({1, 3, 5}, Compare()), FS(vec.begin(), vec.end(), Compare()), FS(vec.rbegin(), vec.rend(), Compare()), }) { auto cmp = fs.key_comp(); assert(std::is_sorted(fs.begin(), fs.end(), cmp)); assert(fs.find(0) == fs.end()); assert(fs.find(1) != fs.end()); assert(fs.find(2) == fs.end()); assert(fs.find(3) != fs.end()); assert(fs.find(4) == fs.end()); assert(fs.find(5) != fs.end()); assert(fs.find(6) == fs.end()); } if (std::is_sorted(vec.begin(), vec.end(), Compare())) { for (auto&& fs : { FS(stdext::sorted_unique, vec), FS(stdext::sorted_unique, vec.begin(), vec.end()), FS(stdext::sorted_unique, {1, 3, 5}), FS(stdext::sorted_unique, vec, Compare()), FS(stdext::sorted_unique, vec.begin(), vec.end(), Compare()), FS(stdext::sorted_unique, {1, 3, 5}, Compare()), }) { auto cmp = fs.key_comp(); assert(std::is_sorted(fs.begin(), fs.end(), cmp)); } } } template static void SpecialMemberTest() { static_assert(std::is_default_constructible::value, ""); static_assert(std::is_nothrow_move_constructible::value == std::is_nothrow_move_constructible::value, ""); static_assert(std::is_copy_constructible::value, ""); static_assert(std::is_copy_assignable::value, ""); static_assert(std::is_move_assignable::value, ""); static_assert(std::is_nothrow_destructible::value, ""); } } // anonymous namespace void sg14_test::flat_set_test() { AmbiguousEraseTest(); ExtractDoesntSwapTest(); MoveOperationsPilferOwnership(); ThrowingSwapDoesntBreakInvariants(); VectorBoolSanityTest(); VectorBoolEvilComparatorTest(); // Test the most basic flat_set. { using FS = stdext::flat_set; ConstructionTest(); SpecialMemberTest(); } // Test a custom comparator. { using FS = stdext::flat_set>; ConstructionTest(); SpecialMemberTest(); } #if __cplusplus >= 201402L // Test a transparent comparator. { using FS = stdext::flat_set>; ConstructionTest(); SpecialMemberTest(); } #endif // Test a custom container. { using FS = stdext::flat_set, std::deque>; ConstructionTest(); SpecialMemberTest(); } #if defined(__cpp_lib_memory_resource) // Test a pmr container. { using FS = stdext::flat_set, std::pmr::vector>; ConstructionTest(); SpecialMemberTest(); } #endif } #ifdef TEST_MAIN int main() { sg14_test::flat_set_test(); } #endif ================================================ FILE: SG14_test/inplace_function_test.cpp ================================================ #include "SG14_test.h" #include "inplace_function.h" #include #include #include #include #include #define EXPECT_EQ(val1, val2) assert(val1 == val2) #define EXPECT_TRUE(val) assert(val) #define EXPECT_FALSE(val) assert(!val) namespace { static int copied, moved, called_with; static int expected; struct Functor { Functor() {} Functor(const Functor&) { copied += 1; } Functor(Functor&&) noexcept { moved += 1; } void operator()(int i) { assert(i == expected); called_with = i; } }; struct ConstFunctor { ConstFunctor() {} ConstFunctor(const ConstFunctor&) { copied += 1; } ConstFunctor(ConstFunctor&&) noexcept { moved += 1; } void operator()(int i) const { assert(i == expected); called_with = i; } }; void Foo(int i) { assert(i == expected); called_with = i; } } // anonymous namespace static std::string gLastS; static int gLastI = 0; static double gNextReturn = 0.0; static double GlobalFunction(const std::string& s, int i) { gLastS = s; gLastI = i; return gNextReturn; } static void FunctionPointer() { // Even compatible function pointers require an appropriate amount of "storage". using CompatibleFunctionType = std::remove_reference_t; stdext::inplace_function fun(&GlobalFunction); EXPECT_TRUE(bool(fun)); gNextReturn = 7.77; double r = fun("hello", 42); EXPECT_EQ(gNextReturn, r); EXPECT_EQ("hello", gLastS); EXPECT_EQ(42, gLastI); } static void Lambda() { stdext::inplace_function fun; std::string closure("some closure"); fun = [&closure](int x) { return GlobalFunction(closure, x); }; gNextReturn = 7.77; double r = fun(42); EXPECT_EQ(gNextReturn, r); EXPECT_EQ(closure, gLastS); EXPECT_EQ(42, gLastI); } static void Bind() { stdext::inplace_function fun; std::string closure("some closure"); fun = std::bind(GlobalFunction, closure, std::placeholders::_1); gNextReturn = 7.77; double r = fun(42); EXPECT_EQ(gNextReturn, r); EXPECT_EQ(closure, gLastS); EXPECT_EQ(42, gLastI); } struct AnotherFunctor { int mTotal = 0; static int mDestructorCalls; static int mConstructorCalls; int operator()(int x) { mTotal += x; return mTotal; } AnotherFunctor() { mConstructorCalls++; } AnotherFunctor(AnotherFunctor&&) noexcept { mConstructorCalls++; } AnotherFunctor(const AnotherFunctor&) { mConstructorCalls++; } ~AnotherFunctor() { mDestructorCalls++; } }; int AnotherFunctor::mDestructorCalls = 0; int AnotherFunctor::mConstructorCalls = 0; static void FunctorDestruction() { AnotherFunctor::mDestructorCalls = 0; AnotherFunctor::mConstructorCalls = 0; { AnotherFunctor ftor; stdext::inplace_function fun(ftor); int r1 = fun(1); int r2 = fun(7); EXPECT_EQ(1, r1); EXPECT_EQ(8, r2); } EXPECT_EQ(AnotherFunctor::mDestructorCalls, AnotherFunctor::mConstructorCalls); AnotherFunctor::mDestructorCalls = 0; AnotherFunctor::mConstructorCalls = 0; { AnotherFunctor ftor; stdext::inplace_function fun(ftor); stdext::inplace_function fun2(fun); // copy-ctor stdext::inplace_function fun3(std::move(fun)); // move-ctor fun3 = fun2; // copy-asgn fun3 = std::move(fun2); // move-asgn } EXPECT_EQ(AnotherFunctor::mDestructorCalls, AnotherFunctor::mConstructorCalls); } static void Swapping() { AnotherFunctor::mDestructorCalls = 0; AnotherFunctor::mConstructorCalls = 0; { AnotherFunctor ftor; auto lambda = [](int x){ return x + 10; }; stdext::inplace_function fun(ftor); stdext::inplace_function fun2(lambda); fun.swap(fun2); // swap... fun2.swap(fun); // ...and swap back int r1 = fun(1); int r2 = fun(7); EXPECT_EQ(1, r1); EXPECT_EQ(8, r2); int r3 = fun2(1); int r4 = fun2(7); EXPECT_EQ(11, r3); EXPECT_EQ(17, r4); } EXPECT_EQ(AnotherFunctor::mDestructorCalls, AnotherFunctor::mConstructorCalls); } static void Copying() { auto sptr = std::make_shared(42); EXPECT_EQ(1, sptr.use_count()); stdext::inplace_function fun1 = [sptr]() { return *sptr; }; stdext::inplace_function fun2; EXPECT_EQ(2, sptr.use_count()); EXPECT_TRUE(bool(fun1)); EXPECT_FALSE(bool(fun2)); fun2 = fun1; EXPECT_EQ(3, sptr.use_count()); EXPECT_TRUE(bool(fun1)); EXPECT_TRUE(bool(fun2)); // this should call destructor on existing functor fun1 = nullptr; EXPECT_EQ(2, sptr.use_count()); EXPECT_FALSE(bool(fun1)); EXPECT_TRUE(bool(fun2)); } static void ContainingStdFunction() { // build a big closure, bigger than 32 bytes uint64_t offset1 = 1234; uint64_t offset2 = 77; uint64_t offset3 = 666; std::string str1 = "12345"; std::function stdfun = [offset1, offset2, offset3, str1](const std::string& str) { return int(offset1 + offset2 + offset3 + str1.length() + str.length()); }; stdext::inplace_function fun = stdfun; int r = fun("123"); EXPECT_EQ(r, int(offset1+offset2+offset3+str1.length()+3)); } static void SimilarTypeCopy() { auto sptr = std::make_shared(42); EXPECT_EQ(1, sptr.use_count()); stdext::inplace_function fun1 = [sptr]() { return *sptr; }; stdext::inplace_function fun2(fun1); // fun1 is bigger than 17, but we should be smart about it stdext::inplace_function fun3; EXPECT_EQ(3, sptr.use_count()); EXPECT_FALSE(fun3); fun3 = fun2; EXPECT_EQ(4, sptr.use_count()); EXPECT_TRUE(bool(fun2)); EXPECT_TRUE(bool(fun3)); fun1 = nullptr; fun2 = nullptr; EXPECT_EQ(2, sptr.use_count()); fun3 = nullptr; EXPECT_EQ(1, sptr.use_count()); stdext::inplace_function fun4; fun4 = fun1; // fun1 is bigger than 17, but we should be smart about it } static void AssignmentDifferentFunctor() { int calls = 0; stdext::inplace_function add = [&calls] (int a, int b) { ++calls; return a+b; }; stdext::inplace_function mul = [&calls] (int a, int b) { ++calls; return a*b; }; int r1 = add(3, 5); EXPECT_EQ(8, r1); int r2 = mul(2, 5); EXPECT_EQ(10, r2); EXPECT_EQ(2, calls); add = mul; int r3 = add(3, 5); EXPECT_EQ(15, r3); int r4 = mul(2, 5); EXPECT_EQ(10, r4); EXPECT_EQ(4, calls); } struct ThrowingFunctor { static int countdown; static int constructed; static int destructed; static int called; static void reset(int k) { countdown = k; constructed = 0; destructed = 0; called = 0; } static void check_countdown() { if (countdown > 0 && --countdown == 0) { throw 42; } } ThrowingFunctor() { check_countdown(); ++constructed; } ThrowingFunctor(const ThrowingFunctor&) { check_countdown(); ++constructed; } ThrowingFunctor(ThrowingFunctor&&) noexcept { ++constructed; } ThrowingFunctor& operator=(const ThrowingFunctor&) = delete; ThrowingFunctor& operator=(ThrowingFunctor&&) = delete; ~ThrowingFunctor() noexcept { ++destructed; } void operator()() const { ++called; } }; int ThrowingFunctor::countdown = 0; int ThrowingFunctor::constructed = 0; int ThrowingFunctor::destructed = 0; int ThrowingFunctor::called = 0; static void test_exception_safety() { using IPF = stdext::inplace_function; ThrowingFunctor tf; EXPECT_EQ(ThrowingFunctor::constructed, 1); EXPECT_EQ(ThrowingFunctor::destructed, 0); EXPECT_EQ(ThrowingFunctor::called, 0); bool caught; // Copy-construction from a ThrowingFunctor might throw. try { tf.reset(1); caught = false; IPF a(tf); } catch (int) { caught = true; } EXPECT_TRUE((caught) && (tf.countdown == 0) && (tf.constructed == 0) && (tf.destructed == 0)); try { tf.reset(2); caught = false; IPF a(tf); } catch (int) { caught = true; } EXPECT_TRUE((!caught) && (tf.countdown == 1) && (tf.constructed == 1) && (tf.destructed == 1)); // Copy-construction from an IPF (containing a ThrowingFunctor) might throw. try { tf.reset(2); caught = false; IPF a(tf); IPF b(a); } catch (int) { caught = true; } EXPECT_TRUE((caught) && (tf.countdown == 0) && (tf.constructed == 1) && (tf.destructed == 1)); try { tf.reset(3); caught = false; IPF a(tf); IPF b(a); } catch (int) { caught = true; } EXPECT_TRUE((!caught) && (tf.countdown == 1) && (tf.constructed == 2) && (tf.destructed == 2)); // Move-construction and destruction should be assumed not to throw. try { tf.reset(1); caught = false; IPF a(std::move(tf)); IPF b(std::move(a)); } catch (int) { caught = true; } EXPECT_TRUE((!caught) && (tf.countdown == 1) && (tf.constructed == 2) && (tf.destructed == 2)); // The assignment operators are implemented as "construct an IPF, then move from it"; so, one copy and one move of the ThrowingFunctor. try { tf.reset(1); caught = false; IPF a; a = tf; } catch (int) { caught = true; } EXPECT_TRUE((caught) && (tf.countdown == 0) && (tf.constructed == 0) && (tf.destructed == 0)); try { tf.reset(2); caught = false; IPF a; a = tf; } catch (int) { caught = true; } EXPECT_TRUE((!caught) && (tf.countdown == 1) && (tf.constructed == 2) && (tf.destructed == 2)); // The assignment operators are implemented as "construct an IPF, then move from it"; so, two moves of the ThrowingFunctor. try { tf.reset(1); caught = false; IPF a; a = std::move(tf); } catch (int) { caught = true; } EXPECT_TRUE((!caught) && (tf.countdown == 1) && (tf.constructed == 2) && (tf.destructed == 2)); try { tf.reset(1); caught = false; IPF a; IPF b(tf); a = b; } catch (int) { caught = true; } EXPECT_TRUE((caught) && (tf.countdown == 0) && (tf.constructed == 0) && (tf.destructed == 0)); try { tf.reset(2); caught = false; IPF a; IPF b(tf); a = b; } catch (int) { caught = true; } EXPECT_TRUE((caught) && (tf.countdown == 0) && (tf.constructed == 1) && (tf.destructed == 1)); } template constexpr size_t expected_alignment_for_capacity() { constexpr size_t alignof_ptr = std::alignment_of::value; constexpr size_t alignof_cap = std::alignment_of>::value; #define MIN(a,b) (a < b ? a : b) #define MAX(a,b) (a > b ? a : b) return MAX(MIN(Cap, alignof_cap), alignof_ptr); #undef MAX #undef MIN } static void test_struct_layout() { static_assert(std::alignment_of< stdext::inplace_function >::value == expected_alignment_for_capacity<1>(), ""); static_assert(std::alignment_of< stdext::inplace_function >::value == expected_alignment_for_capacity<2>(), ""); static_assert(std::alignment_of< stdext::inplace_function >::value == expected_alignment_for_capacity<4>(), ""); static_assert(std::alignment_of< stdext::inplace_function >::value == expected_alignment_for_capacity<8>(), ""); static_assert(std::alignment_of< stdext::inplace_function >::value == expected_alignment_for_capacity<16>(), ""); static_assert(std::alignment_of< stdext::inplace_function >::value == expected_alignment_for_capacity<32>(), ""); static_assert(sizeof( stdext::inplace_function ) == 2 * sizeof(void*), ""); } static void test_nullptr() { using IPF = stdext::inplace_function; auto nil = nullptr; const auto cnil = nullptr; IPF f; assert(! bool(f)); f = nullptr; assert(! bool(f)); f = IPF(nullptr); assert(! bool(f)); f = IPF(); assert(! bool(f)); f = IPF{}; assert(! bool(f)); f = {}; assert(! bool(f)); f = nil; assert(! bool(f)); f = IPF(nil); assert(! bool(f)); f = IPF(std::move(nil)); assert(! bool(f)); f = cnil; assert(! bool(f)); f = IPF(cnil); assert(! bool(f)); f = IPF(std::move(cnil)); assert(! bool(f)); } struct oon_functor { int dummy; oon_functor(int i) : dummy(i) {} int operator()(int i) { return i + dummy; } void *operator new (size_t, void *p) { EXPECT_TRUE(false); // class-specific "new" should not be called return p; } }; static void test_overloaded_operator_new() { using IPF = stdext::inplace_function; oon_functor oon(42); IPF fun = oon; IPF fun2; fun2 = oon; fun = fun2; EXPECT_EQ(43, fun(1)); } static void test_move_construction_is_noexcept() { using IPF = stdext::inplace_function; std::vector vec; vec.push_back(Functor()); copied = 0; moved = 0; vec.reserve(vec.capacity() + 1); EXPECT_EQ(0, copied); EXPECT_EQ(1, moved); } static void test_move_construction_from_smaller_buffer_is_noexcept() { using IPF32 = stdext::inplace_function; using IPF40 = stdext::inplace_function; static_assert(std::is_nothrow_constructible::value, ""); static_assert(std::is_nothrow_assignable::value, ""); static_assert(std::is_nothrow_constructible::value, ""); static_assert(std::is_nothrow_assignable::value, ""); } // https://bugs.llvm.org/show_bug.cgi?id=32072 struct test_bug_32072_C; struct test_bug_32072 { stdext::inplace_function m; }; static_assert(std::is_copy_constructible::value, ""); static_assert(std::is_nothrow_move_constructible::value, ""); static void RvalueRefParameter() { stdext::inplace_function&&)> f; f = [](std::unique_ptr) {}; f = [](std::unique_ptr&&) {}; f = [](const std::unique_ptr&) {}; f(std::make_unique(42)); stdext::inplace_function)> g; g = [](std::unique_ptr) {}; g = [](std::unique_ptr&&) {}; g = [](const std::unique_ptr&) {}; g(std::make_unique(42)); } static void test_is_convertible() { static_assert(std::is_convertible>::value, ""); static_assert(std::is_convertible>::value, ""); static_assert(std::is_convertible>::value, ""); static_assert(std::is_convertible>::value, ""); } static void test_convertibility_with_qualified_call_operators() { struct Callable { void operator()() {} }; struct LvalueOnlyCallable { void operator()() & {} }; struct RvalueOnlyCallable { void operator()() && {} }; struct ConstCallable { void operator()() const {} }; struct ConstOnlyCallable { void operator()() const {} void operator()() = delete; }; struct NonconstOnlyCallable { void operator()() {} void operator()() const = delete; }; struct LvalueConstCallable { void operator()() const & {} }; struct NoexceptCallable { void operator()() noexcept {} }; static_assert(std::is_convertible>::value, ""); static_assert(std::is_convertible>::value, ""); static_assert(!std::is_convertible>::value, ""); static_assert(std::is_convertible>::value, ""); static_assert(!std::is_convertible>::value, ""); static_assert(std::is_convertible>::value, ""); static_assert(std::is_convertible>::value, ""); static_assert(std::is_convertible>::value, ""); } static void test_convertibility_with_lambdas() { struct NoDefaultCtor { int val; explicit NoDefaultCtor(int v) : val{v} {} }; const auto a = []() -> int { return 3; }; static_assert(std::is_convertible>::value, ""); static_assert(!std::is_convertible>::value, ""); static_assert(!std::is_convertible>::value, ""); const auto b = [](int&) -> void {}; static_assert(std::is_convertible>::value, ""); static_assert(!std::is_convertible>::value, ""); static_assert(!std::is_convertible>::value, ""); const auto c = [](int, NoDefaultCtor) -> int { return 3; }; static_assert(std::is_convertible>::value, ""); static_assert(!std::is_convertible>::value, ""); static_assert(!std::is_convertible>::value, ""); const auto d = []() -> void {}; static_assert(std::is_convertible>::value, ""); static_assert(!std::is_convertible>::value, ""); static_assert(!std::is_convertible>::value, ""); static_assert(std::is_convertible>::value, ""); static_assert(std::is_convertible>::value, ""); // Same as a, but not const. auto e = []() -> int { return 3; }; static_assert(std::is_convertible>::value, ""); static_assert(!std::is_convertible>::value, ""); static_assert(!std::is_convertible>::value, ""); // Same as a, but not const and mutable. auto f = []() mutable -> int { return 3; }; static_assert(std::is_convertible>::value, ""); static_assert(!std::is_convertible>::value, ""); static_assert(!std::is_convertible>::value, ""); } namespace { struct InstrumentedCopyConstructor { static int copies; static int moves; InstrumentedCopyConstructor() = default; InstrumentedCopyConstructor(const InstrumentedCopyConstructor&) { copies += 1; } InstrumentedCopyConstructor(InstrumentedCopyConstructor&&) noexcept { moves += 1; } }; int InstrumentedCopyConstructor::copies = 0; int InstrumentedCopyConstructor::moves = 0; } // anonymous namespace static void test_return_by_move() { using IPF20 = stdext::inplace_function; using IPF40 = stdext::inplace_function; static_assert(std::is_convertible::value, ""); static_assert(std::is_convertible::value, ""); static_assert(std::is_convertible::value, ""); static_assert(std::is_convertible::value, ""); static_assert(std::is_convertible::value, ""); auto foo = []() -> IPF40 { InstrumentedCopyConstructor cc; InstrumentedCopyConstructor::copies = 0; InstrumentedCopyConstructor::moves = 0; IPF20 f = [cc]() { }; assert(InstrumentedCopyConstructor::copies == 1); assert(InstrumentedCopyConstructor::moves == 1); InstrumentedCopyConstructor::copies = 0; InstrumentedCopyConstructor::moves = 0; return f; }; IPF40 f = foo(); assert(InstrumentedCopyConstructor::copies == 0); assert(InstrumentedCopyConstructor::moves == 1); } static void test_is_invocable() { using C_Int1 = int(); using C_Int2 = int(int); using C_Void = void(int&); using stdext::inplace_function_detail::is_invocable_r; static_assert(is_invocable_r::value, ""); static_assert(! is_invocable_r::value, ""); static_assert(! is_invocable_r::value, ""); static_assert(is_invocable_r::value, ""); static_assert(! is_invocable_r::value, ""); static_assert(! is_invocable_r::value, ""); static_assert(is_invocable_r::value, ""); static_assert(! is_invocable_r::value, ""); // Testing widening and narrowing conversions, and the "conversion" to void. static_assert(is_invocable_r::value, ""); static_assert(is_invocable_r::value, ""); static_assert(is_invocable_r::value, ""); // Testing the conversion from void to int, which should definitely not be allowed. static_assert(! is_invocable_r::value, ""); // cppreference: // > Determines whether Fn can be invoked with the arguments ArgTypes... // > to yield a result that is convertible to R. // // void is treated specially because a functions return value can be ignored. static_assert(is_invocable_r::value, ""); static_assert(is_invocable_r::value, ""); // Regression tests for both is_invocable and is_convertible. static_assert(is_invocable_r::value, ""); static_assert(is_invocable_r::value, ""); } static int overloaded_function(stdext::inplace_function) { return 1; } static int overloaded_function(stdext::inplace_function) { return 2; } static void test_overloading_on_arity() { EXPECT_EQ(overloaded_function([]() { return 0; }), 1); EXPECT_EQ(overloaded_function([](int) { return 0; }), 2); } static int overloaded_function2(stdext::inplace_function) { return 1; } static int overloaded_function2(stdext::inplace_function) { return 2; } static void test_overloading_on_parameter_type() { EXPECT_EQ(overloaded_function2([](int) { return 0; }), 1); EXPECT_EQ(overloaded_function2([](int*) { return 0; }), 2); } static int overloaded_function3(stdext::inplace_function) { return 1; } static int overloaded_function3(stdext::inplace_function) { return 2; } static void test_overloading_on_return_type() { EXPECT_EQ(overloaded_function3([](int) { return 0; }), 1); EXPECT_EQ(overloaded_function3([](int) { return nullptr; }), 2); } void sg14_test::inplace_function_test() { // first set of tests (from Optiver) AssignmentDifferentFunctor(); FunctionPointer(); Lambda(); Bind(); Swapping(); Copying(); ContainingStdFunction(); SimilarTypeCopy(); FunctorDestruction(); RvalueRefParameter(); // second set of tests using IPF = stdext::inplace_function; static_assert(std::is_nothrow_default_constructible::value, ""); static_assert(std::is_copy_constructible::value, ""); static_assert(std::is_move_constructible::value, ""); static_assert(std::is_copy_assignable::value, ""); static_assert(std::is_move_assignable::value, ""); #if __cplusplus >= 201703L static_assert(std::is_swappable::value, ""); static_assert(std::is_invocable::value, ""); static_assert(std::is_invocable_r::value, ""); #endif static_assert(std::is_nothrow_destructible::value, ""); test_struct_layout(); IPF func; assert(!func); assert(!bool(func)); assert(func == nullptr); assert(!(func != nullptr)); expected = 0; try { func(42); } catch (std::bad_function_call&) { expected = 1; } assert(expected == 1); func = Foo; assert(!!func); assert(func); assert(!(func == nullptr)); assert(func != nullptr); called_with = 0; expected = 42; func(42); assert(called_with == 42); func = nullptr; assert(!func); assert(!bool(func)); assert(func == nullptr); assert(!(func != nullptr)); expected = 0; try { func(42); } catch (std::bad_function_call&) { expected = 1; } assert(expected == 1); using IPF40 = stdext::inplace_function; // the default is 32 static_assert(std::is_constructible::value, ""); static_assert(std::is_constructible::value, ""); // TODO: nothrow static_assert(std::is_assignable::value, ""); static_assert(std::is_assignable::value, ""); // TODO: nothrow //static_assert(!std::is_assignable::value, ""); //static_assert(!std::is_assignable::value, ""); #if __cplusplus >= 201703L static_assert(!std::is_swappable_with::value, ""); static_assert(!std::is_swappable_with::value, ""); #endif static_assert(std::is_nothrow_destructible::value, ""); IPF40 func40; assert(!func40); assert(!bool(func40)); assert(func40 == nullptr); assert(!(func40 != nullptr)); expected = 0; try { func40(42); } catch (std::bad_function_call&) { expected = 1; } assert(expected == 1); func = nullptr; func40 = func; assert(!func40); assert(!bool(func40)); assert(func40 == nullptr); assert(!(func40 != nullptr)); expected = 0; try { func40(42); } catch (std::bad_function_call&) { expected = 1; } assert(expected == 1); test_exception_safety(); test_nullptr(); test_overloaded_operator_new(); test_move_construction_is_noexcept(); test_move_construction_from_smaller_buffer_is_noexcept(); test_is_convertible(); test_convertibility_with_qualified_call_operators(); test_convertibility_with_lambdas(); test_return_by_move(); test_is_invocable(); test_overloading_on_arity(); test_overloading_on_parameter_type(); test_overloading_on_return_type(); } #ifdef TEST_MAIN int main() { sg14_test::inplace_function_test(); } #endif ================================================ FILE: SG14_test/main.cpp ================================================ #if defined(_MSC_VER) #include #endif #include #include "SG14_test.h" int main(int, char *[]) { sg14_test::flat_map_test(); sg14_test::flat_set_test(); sg14_test::inplace_function_test(); sg14_test::plf_colony_test(); sg14_test::ring_test(); sg14_test::slot_map_test(); sg14_test::uninitialized_test(); sg14_test::unstable_remove_test(); puts("tests completed"); return 0; } ================================================ FILE: SG14_test/plf_colony_test.cpp ================================================ #define PLF_COLONY_TEST_DEBUG #if defined(_MSC_VER) #if _MSC_VER >= 1600 #define PLF_TEST_MOVE_SEMANTICS_SUPPORT #endif #if _MSC_VER >= 1700 #define PLF_TEST_TYPE_TRAITS_SUPPORT #endif #if _MSC_VER >= 1800 #define PLF_TEST_VARIADICS_SUPPORT // Variadics, in this context, means both variadic templates and variadic macros are supported #define PLF_TEST_INITIALIZER_LIST_SUPPORT #endif #if defined(_MSVC_LANG) && (_MSVC_LANG > 201703L) && _MSC_VER >= 1923 #define PLF_TEST_CPP20_SUPPORT #endif #elif defined(__cplusplus) && __cplusplus >= 201103L // C++11 support, at least #define PLF_TEST_MOVE_SEMANTICS_SUPPORT #if defined(__GNUC__) && defined(__GNUC_MINOR__) && !defined(__clang__) // If compiler is GCC/G++ #if (__GNUC__ == 4 && __GNUC_MINOR__ >= 3) || __GNUC__ > 4 // 4.2 and below do not support variadic templates #define PLF_TEST_MOVE_SEMANTICS_SUPPORT #define PLF_TEST_VARIADICS_SUPPORT #endif #if (__GNUC__ == 4 && __GNUC_MINOR__ >= 4) || __GNUC__ > 4 // 4.3 and below do not support initializer lists #define PLF_TEST_INITIALIZER_LIST_SUPPORT #endif #if __GNUC__ >= 5 // GCC v4.9 and below do not support std::is_trivially_copyable #define PLF_TEST_TYPE_TRAITS_SUPPORT #endif #elif defined(__clang__) && !defined(__GLIBCXX__) && !defined(_LIBCPP_CXX03_LANG) #if __clang_major__ >= 3 // clang versions < 3 don't support __has_feature() or traits #define PLF_TEST_TYPE_TRAITS_SUPPORT #if __has_feature(cxx_rvalue_references) && !defined(_LIBCPP_HAS_NO_RVALUE_REFERENCES) #define PLF_TEST_MOVE_SEMANTICS_SUPPORT #endif #if __has_feature(cxx_variadic_templates) && !defined(_LIBCPP_HAS_NO_VARIADICS) #define PLF_TEST_VARIADICS_SUPPORT #endif #if (__clang_major__ == 3 && __clang_minor__ >= 1) || __clang_major__ > 3 #define PLF_TEST_INITIALIZER_LIST_SUPPORT #endif #endif #elif defined(__GLIBCXX__) #if __GLIBCXX__ >= 20080606 #define PLF_TEST_MOVE_SEMANTICS_SUPPORT #define PLF_TEST_VARIADICS_SUPPORT #endif #if __GLIBCXX__ >= 20090421 #define PLF_TEST_INITIALIZER_LIST_SUPPORT #endif #if __GLIBCXX__ >= 20150422 #define PLF_TEST_TYPE_TRAITS_SUPPORT #endif #elif !(defined(_LIBCPP_CXX03_LANG) || defined(_LIBCPP_HAS_NO_RVALUE_REFERENCES) || defined(_LIBCPP_HAS_NO_VARIADICS)) // Assume full support for other compilers and standard libraries #define PLF_TEST_VARIADICS_SUPPORT #define PLF_TEST_TYPE_TRAITS_SUPPORT #define PLF_TEST_MOVE_SEMANTICS_SUPPORT #define PLF_TEST_INITIALIZER_LIST_SUPPORT #endif #if __cplusplus > 201703L && ((defined(__clang__) && (__clang_major__ >= 10)) || (defined(__GNUC__) && __GNUC__ >= 10) || (!defined(__clang__) && !defined(__GNUC__))) // assume correct C++20 implementation for other compilers #define PLF_TEST_CPP20_SUPPORT #endif #endif #include // std::accumulate #include // std::find #include // log redirection, printf #include // abort #include // std::greater #include // range-insert testing #ifdef PLF_TEST_TEST_MOVE_SEMANTICS_SUPPORT #include // std::move #endif #include "plf_colony.h" namespace { inline void failpass(const char* test_type, bool condition) { if (!condition) { printf("%s: Fail\n", test_type); getchar(); abort(); } } void message(const char *) { } void title1(const char*) { } void title2(const char*) { } #ifdef PLF_TEST_VARIADICS_SUPPORT struct perfect_forwarding_test { const bool success; perfect_forwarding_test(int&& /*perfect1*/, int& perfect2) : success(true) { perfect2 = 1; } template perfect_forwarding_test(T&& /*imperfect1*/, U&& /*imperfect2*/) : success(false) {} }; struct small_struct { double *empty_field_1; double unused_number; unsigned int empty_field2; double *empty_field_3; int number; unsigned int empty_field4; small_struct(const int num) : number(num) {}; }; class non_copyable_type { private: int i; non_copyable_type(const non_copyable_type &); // non construction-copyable non_copyable_type& operator=(const non_copyable_type &); // non copyable public: non_copyable_type(int a) : i(a) {} }; #endif int global_counter = 0; struct small_struct_non_trivial { double* empty_field_1; double unused_number; unsigned int empty_field2; double* empty_field_3; int number; unsigned int empty_field4; small_struct_non_trivial(const int num) : number(num) {}; ~small_struct_non_trivial() { ++global_counter; }; }; } // xor_shift128++ generator substituted for pre-C++11 compilers (since C++03 doesn't have guaranteed cross-compiler 64-bit unsigned ints). // Based on https://codingforspeed.com/using-faster-psudo-random-generator-xorshift/ namespace plf { // unsigned long is at least 32 bits in C++ - unsigned int is only guaranteed to be at least 16 bits: unsigned long xorand_nums[4] = {123456789, 362436069, 521288629, 88675123}; unsigned int rand() { const unsigned long temp = xorand_nums[0] ^ (xorand_nums[0] << 11); // Rotate the static values ([3] rotation in return statement): xorand_nums[0] = xorand_nums[1]; xorand_nums[1] = xorand_nums[2]; xorand_nums[2] = xorand_nums[3]; return static_cast(xorand_nums[3] = xorand_nums[3] ^ (xorand_nums[3] >> 19) ^ (temp ^ (temp >> 8))); } } namespace sg14_test { void plf_colony_test() { using namespace std; using namespace plf; unsigned int looper = 0; while (++looper != 25) { { title1("Colony"); title2("Test Basics"); colony p_colony; failpass("Colony empty", p_colony.empty()); int ten = 10; p_colony.insert(&ten); failpass("Colony not-empty", !p_colony.empty()); title2("Iterator tests"); failpass("Begin() working", **p_colony.begin() == 10); failpass("End() working", p_colony.begin() != p_colony.end()); p_colony.clear(); failpass("Begin = End after clear", p_colony.begin() == p_colony.end()); int twenty = 20; for (unsigned int temp = 0; temp != 200; ++temp) { p_colony.insert(&ten); p_colony.insert(&twenty); } int total = 0, numtotal = 0; for(colony::iterator the_iterator = p_colony.begin(); the_iterator != p_colony.end(); ++the_iterator) { ++total; numtotal += **the_iterator; } failpass("Iteration count test", total == 400); failpass("Iterator access test", numtotal == 6000); colony::iterator plus_twenty = p_colony.begin(); advance(plus_twenty, 20); colony::iterator plus_two_hundred = p_colony.begin(); advance(plus_two_hundred, 200); failpass("Iterator + distance test", distance(p_colony.begin(), plus_twenty) == 20); failpass("Iterator - distance test", distance(plus_two_hundred, p_colony.begin()) == -200); colony::iterator next_iterator = next(p_colony.begin(), 5); colony::const_iterator prev_iterator = prev(p_colony.cend(), 300); failpass("Iterator next test", distance(p_colony.begin(), next_iterator) == 5); failpass("Const iterator prev test", distance(p_colony.cend(), prev_iterator) == -300); #if defined(__cplusplus) && __cplusplus >= 201402L colony::iterator prev_iterator2 = prev(p_colony.end(), 300); failpass("Iterator/Const iterator equality operator test", prev_iterator == prev_iterator2); #endif prev_iterator = p_colony.begin(); advance(prev_iterator, 5); failpass("Iterator/Const iterator equality operator test 2", prev_iterator == next_iterator); colony p_colony2; p_colony2 = p_colony; colony p_colony3(p_colony); colony p_colony4(p_colony2, p_colony2.get_allocator()); colony::iterator it1 = p_colony.begin(); colony::const_iterator cit(it1); failpass("Copy test", p_colony2.size() == 400); failpass("Copy construct test", p_colony3.size() == 400); failpass("Allocator-extended copy construct test", p_colony4.size() == 400); failpass("Equality operator test", p_colony == p_colony2); failpass("Equality operator test 2", p_colony2 == p_colony3); p_colony2.insert(&ten); failpass("Inequality operator test", p_colony2 != p_colony3); numtotal = 0; total = 0; for (colony::reverse_iterator the_iterator = p_colony.rbegin(); the_iterator != p_colony.rend(); ++the_iterator) { ++total; numtotal += **the_iterator; } failpass("Reverse iteration count test", total == 400); failpass("Reverse iterator access test", numtotal == 6000); colony::reverse_iterator r_iterator = p_colony.rbegin(); advance(r_iterator, 50); failpass("Reverse iterator advance and distance test", distance(p_colony.rbegin(), r_iterator) == 50); colony::reverse_iterator r_iterator2 = next(r_iterator, 2); failpass("Reverse iterator next and distance test", distance(p_colony.rbegin(), r_iterator2) == 52); numtotal = 0; total = 0; for(colony::iterator the_iterator = p_colony.begin(); the_iterator < p_colony.end(); advance(the_iterator, 2)) { ++total; numtotal += **the_iterator; } failpass("Multiple iteration test", total == 200); failpass("Multiple iteration access test", numtotal == 2000); numtotal = 0; total = 0; for(colony::const_iterator the_iterator = p_colony.cbegin(); the_iterator != p_colony.cend(); ++the_iterator) { ++total; numtotal += **the_iterator; } failpass("Const_iterator test", total == 400); failpass("Const_iterator access test", numtotal == 6000); numtotal = 0; total = 0; for(colony::const_reverse_iterator the_iterator = --colony::const_reverse_iterator(p_colony.crend()); the_iterator != colony::const_reverse_iterator(p_colony.crbegin()); --the_iterator) { ++total; numtotal += **the_iterator; } failpass("Const_reverse_iterator -- test", total == 399); failpass("Const_reverse_iterator -- access test", numtotal == 5980); total = 0; for(colony::iterator the_iterator = ++colony::iterator(p_colony.begin()); the_iterator < p_colony.end(); ++the_iterator) { ++total; the_iterator = p_colony.erase(the_iterator); } failpass("Partial erase iteration test", total == 200); failpass("Post-erase size test", p_colony.size() == 200); const unsigned int temp_capacity = static_cast(p_colony.capacity()); p_colony.shrink_to_fit(); failpass("Shrink_to_fit test", p_colony.capacity() < temp_capacity); failpass("Shrink_to_fit test 2", p_colony.capacity() == 200); total = 0; for(colony::reverse_iterator the_iterator = p_colony.rbegin(); the_iterator != p_colony.rend(); ++the_iterator) { colony::iterator it = the_iterator.base(); the_iterator = p_colony.erase(--it); ++total; } failpass("Full erase reverse iteration test", total == 200); failpass("Post-erase size test", p_colony.size() == 0); for (unsigned int temp = 0; temp != 200; ++temp) { p_colony.insert(&ten); p_colony.insert(&twenty); } total = 0; for(colony::iterator the_iterator = --colony::iterator(p_colony.end()); the_iterator != p_colony.begin(); --the_iterator) { ++total; } failpass("Negative iteration test", total == 399); total = 0; for(colony::iterator the_iterator = --(colony::iterator(p_colony.end())); the_iterator != p_colony.begin(); advance(the_iterator, -2)) { ++total; } failpass("Negative multiple iteration test", total == 200); #ifdef PLF_TEST_MOVE_SEMANTICS_SUPPORT p_colony2 = std::move(p_colony); failpass("Move test", p_colony2.size() == 400); p_colony.insert(&ten); failpass("Insert to post-moved-colony test", p_colony.size() == 1); colony p_colony5(p_colony2); colony p_colony6(std::move(p_colony5), p_colony2.get_allocator()); failpass("Allocator-extended move construct test", p_colony6.size() == 400); #else p_colony2 = p_colony; #endif p_colony3 = p_colony2; failpass("Copy test 2", p_colony3.size() == 400); p_colony2.insert(&ten); p_colony2.swap(p_colony3); failpass("Swap test", p_colony2.size() == p_colony3.size() - 1); std::swap(p_colony2, p_colony3); failpass("Swap test 2", p_colony3.size() == p_colony2.size() - 1); failpass("max_size() test", p_colony2.max_size() > p_colony2.size()); } { title2("Iterator comparison tests"); colony i_colony; for (int temp = 0; temp != 10; ++temp) { i_colony.insert(temp); } colony::iterator it1 = i_colony.begin(), it2 = i_colony.begin(); ++it2; ++it2; ++it2; failpass("Iterator ++ test", *it2 == 3); failpass("Iterator > test", it2 > it1); failpass("Iterator >= test", it2 >= it1); failpass("Iterator < test", it1 < it2); failpass("Iterator <= test", it1 <= it2); failpass("Iterator != test", it2 != it1); #ifdef PLF_TEST_CPP20_SUPPORT failpass("Iterator <=> test 1", (it2 <=> it1) == 1); failpass("Iterator <=> test 2", (it1 <=> it2) == -1); it1 = it2; failpass("Iterator <=> test 3", (it1 <=> it2) == 0); #endif } { title2("Insert and Erase tests"); colony i_colony; for (int temp = 0; temp != 500000; ++temp) { i_colony.insert(temp); } failpass("Size after insert test", i_colony.size() == 500000); colony::iterator found_item = std::find(i_colony.begin(), i_colony.end(), 5000);; failpass("std::find iterator test", *found_item == 5000); colony::reverse_iterator found_item2 = std::find(i_colony.rbegin(), i_colony.rend(), 5000);; failpass("std::find reverse_iterator test", *found_item2 == 5000); for (colony::iterator the_iterator = i_colony.begin(); the_iterator != i_colony.end(); ++the_iterator) { the_iterator = i_colony.erase(the_iterator); } failpass("Erase alternating test", i_colony.size() == 250000); do { for (colony::iterator the_iterator = i_colony.begin(); the_iterator != i_colony.end();) { if ((plf::rand() & 7) == 0) { the_iterator = i_colony.erase(the_iterator); } else { ++the_iterator; } } } while (!i_colony.empty()); failpass("Erase randomly till-empty test", i_colony.size() == 0); i_colony.clear(); i_colony.reshape(plf::colony_limits(10000, i_colony.block_limits().max)); i_colony.insert(30000, 1); // fill-insert 30000 elements failpass("Size after reinitialize + fill-insert test", i_colony.size() == 30000); unsigned short count2 = 0; do { for (colony::iterator the_iterator = i_colony.begin(); the_iterator != i_colony.end();) { if ((plf::rand() & 7) == 0) { the_iterator = i_colony.erase(the_iterator); ++count2; } else { ++the_iterator; } } } while (count2 < 15000); failpass("Erase randomly till half-empty test", i_colony.size() == 30000u - count2); i_colony.insert(count2, 1); failpass("Size after reinsert test", i_colony.size() == 30000); unsigned int sum = 0; for (colony::iterator the_iterator = i_colony.begin(); the_iterator != i_colony.end();) { if (++sum == 3) { sum = 0; the_iterator = i_colony.erase(the_iterator); } else { i_colony.insert(1); ++the_iterator; } } failpass("Alternating insert/erase test", i_colony.size() == 45001); do { for (colony::iterator the_iterator = i_colony.begin(); the_iterator != i_colony.end();) { if ((plf::rand() & 3) == 0) { ++the_iterator; i_colony.insert(1); } else { the_iterator = i_colony.erase(the_iterator); } } } while (!i_colony.empty());; failpass("Random insert/erase till empty test", i_colony.size() == 0); i_colony.insert(500000, 10); failpass("Insert post-erase test", i_colony.size() == 500000); colony::iterator it2 = i_colony.begin(); advance(it2, 250000); for (; it2 != i_colony.end();) { it2 = i_colony.erase(it2); } failpass("Large multi-increment iterator test", i_colony.size() == 250000); i_colony.insert(250000, 10); colony::iterator end_iterator = i_colony.end(); colony::iterator end_iterator2 = i_colony.end(); advance(end_iterator, -250000); for (unsigned int count = 0; count != 250000; ++count, --end_iterator2){} failpass("Large multi-decrement iterator test 1", end_iterator == end_iterator2); for (colony::iterator the_iterator = i_colony.begin(); the_iterator != end_iterator;) { the_iterator = i_colony.erase(the_iterator); } failpass("Large multi-decrement iterator test", i_colony.size() == 250000); i_colony.insert(250000, 10); int total = 0; for (colony::iterator the_iterator = i_colony.begin(); the_iterator != i_colony.end(); ++the_iterator) { total += *the_iterator; } failpass("Re-insert post-heavy-erasure test", total == 5000000); end_iterator = i_colony.end(); advance(end_iterator, -50001); colony::iterator begin_iterator = i_colony.begin(); advance(begin_iterator, 300000); for (colony::iterator the_iterator = begin_iterator; the_iterator != end_iterator;) { the_iterator = i_colony.erase(the_iterator); } failpass("Non-end decrement + erase test", i_colony.size() == 350001); i_colony.insert(100000, 10); begin_iterator = i_colony.begin(); advance(begin_iterator, 300001); for (colony::iterator the_iterator = begin_iterator; the_iterator != i_colony.end();) { the_iterator = i_colony.erase(the_iterator); } failpass("Non-beginning increment + erase test", i_colony.size() == 300001); colony::iterator temp_iterator = i_colony.begin(); advance(temp_iterator, 20); // Advance test 1 unsigned int index = static_cast(distance(i_colony.begin(), temp_iterator)); failpass("Advance + iterator-to-index test", index == 20); i_colony.erase(temp_iterator); temp_iterator = i_colony.begin(); // Check edge-case with advance when erasures present in initial group advance(temp_iterator, 500); index = static_cast(distance(i_colony.begin(), temp_iterator)); failpass("Advance + iterator-to-index test", index == 500); colony::iterator temp2 = i_colony.get_iterator(&(*temp_iterator)); failpass("Pointer-to-iterator test", temp2 != i_colony.end()); temp2 = i_colony.begin(); advance(temp2, 500); failpass("Index-to-iterator test", temp2 == temp_iterator); for (colony::iterator the_iterator = i_colony.begin(); the_iterator != i_colony.end();) { the_iterator = i_colony.erase(the_iterator); } failpass("Total erase test", i_colony.empty()); i_colony.clear(); i_colony.reshape(plf::colony_limits(3, i_colony.block_limits().max)); const unsigned int temp_capacity2 = static_cast(i_colony.capacity()); i_colony.reserve(100000); failpass("Colony reserve test", temp_capacity2 != i_colony.capacity()); i_colony.insert(110000, 1); failpass("Post-reserve insert test", i_colony.size() == 110000); unsigned int count = 110000; for (unsigned int loop1 = 0; loop1 != 50000; ++loop1) { for (unsigned int loop = 0; loop != 10; ++loop) { if ((plf::rand() & 7) == 0) { i_colony.insert(1); ++count; } } unsigned int internal_loop_counter = 0; for (colony::iterator the_iterator = i_colony.begin(); the_iterator != i_colony.end();) { if ((plf::rand() & 7) == 0) { the_iterator = i_colony.erase(the_iterator); --count; } else { ++the_iterator; } ++internal_loop_counter; } } failpass("Multiple sequential small insert/erase commands test", count == i_colony.size()); } { title2("Range-erase tests"); colony i_colony; int counter = 0; for (; counter != 1000; ++counter) { i_colony.insert(counter); } colony::iterator it1 = i_colony.begin(), it2 = i_colony.begin(); advance(it1, 500); advance(it2, 800); i_colony.erase(it1, it2); counter = 0; for (colony::iterator it = i_colony.begin(); it != i_colony.end(); ++it) { ++counter; } failpass("Simple range-erase test 1", counter == 700 && i_colony.size() == 700); it1 = it2 = i_colony.begin(); advance(it1, 400); advance(it2, 500); // This should put it2 past the point of previous erasures i_colony.erase(it1, it2); counter = 0; for (colony::iterator it = i_colony.begin(); it != i_colony.end(); ++it) { ++counter; } failpass("Simple range-erase test 2", counter == 600 && i_colony.size() == 600); it2 = it1 = i_colony.begin(); advance(it1, 4); advance(it2, 9); // This should put it2 past the point of previous erasures i_colony.erase(it1, it2); counter = 0; for (colony::iterator it = i_colony.begin(); it != i_colony.end(); ++it) { ++counter; } failpass("Simple range-erase test 3", counter == 595 && i_colony.size() == 595); it2 = it1 = i_colony.begin(); advance(it2, 50); i_colony.erase(it1, it2); counter = 0; for (colony::iterator it = i_colony.begin(); it != i_colony.end(); ++it) { ++counter; } failpass("Range-erase from begin() test 1", counter == 545 && i_colony.size() == 545); it1 = i_colony.begin(); it2 = i_colony.end(); advance(it1, 345); // Test erasing and validity when it removes the final group in colony i_colony.erase(it1, it2); counter = 0; for (colony::iterator it = i_colony.begin(); it != i_colony.end(); ++it) { ++counter; } failpass("Range-erase to end() test 1", counter == 345 && i_colony.size() == 345); i_colony.clear(); for (counter = 0; counter != 3000; ++counter) { i_colony.insert(counter); } for (colony::iterator it = i_colony.begin(); it < i_colony.end(); ++it) { it = i_colony.erase(it); } it2 = it1 = i_colony.begin(); advance(it1, 4); advance(it2, 600); i_colony.erase(it1, it2); counter = 0; for (colony::iterator it = i_colony.begin(); it != i_colony.end(); ++it) { ++counter; } failpass("Range-erase with colony already half-erased, alternating erasures", counter == 904 && i_colony.size() == 904); i_colony.clear(); for (counter = 0; counter != 3000; ++counter) { i_colony.insert(counter); } for (colony::iterator it = i_colony.begin(); it < i_colony.end(); ++it) { if ((plf::rand() & 1) == 0) { it = i_colony.erase(it); } } if (i_colony.size() < 400) { for (counter = 0; counter != 400; ++counter) { i_colony.insert(counter); } } it1 = i_colony.begin(); it2 = i_colony.end(); advance(it1, 400); i_colony.erase(it1, it2); counter = 0; for (colony::iterator it = i_colony.begin(); it != i_colony.end(); ++it) { ++counter; } failpass("Range-erase with colony already third-erased, randomized erasures", counter == 400 && i_colony.size() == 400); unsigned int size, range1, range2, internal_loop_counter; for (unsigned int loop_counter = 0; loop_counter != 50; ++loop_counter) { i_colony.clear(); for (counter = 0; counter != 1000; ++counter) { i_colony.insert(counter); } internal_loop_counter = 0; while (!i_colony.empty()) { it2 = it1 = i_colony.begin(); size = static_cast(i_colony.size()); range1 = plf::rand() % size; range2 = range1 + 1 + (plf::rand() % (size - range1)); advance(it1, static_cast(range1)); advance(it2, static_cast(range2)); i_colony.erase(it1, it2); counter = 0; for (colony::iterator it = i_colony.begin(); it != i_colony.end(); ++it) { ++counter; } if (i_colony.size() != static_cast(counter)) { printf("Fuzz-test range-erase randomly Fail: loop counter: %u, internal_loop_counter: %u.\n", loop_counter, internal_loop_counter); getchar(); abort(); } if (i_colony.size() != i_colony.group_size_sum()) { printf("Fuzz-test range-erase randomly Fail - group_size_sum failure: loop counter: %u, internal_loop_counter: %u, size: %u, group_size_sum: %u.\n", loop_counter, internal_loop_counter, static_cast(i_colony.size()), static_cast(i_colony.group_size_sum())); getchar(); abort(); } if (i_colony.size() > 2) { // Test to make sure our stored erased_locations are valid i_colony.insert(1); i_colony.insert(10); } ++internal_loop_counter; } } failpass("Fuzz-test range-erase randomly until empty", i_colony.size() == 0); for (unsigned int loop_counter = 0; loop_counter != 50; ++loop_counter) { i_colony.clear(); internal_loop_counter = 0; i_colony.insert(10000, 10); while (!i_colony.empty()) { it2 = it1 = i_colony.begin(); size = static_cast(i_colony.size()); range1 = plf::rand() % size; range2 = range1 + 1 + (plf::rand() % (size - range1)); advance(it1, static_cast(range1)); advance(it2, static_cast(range2)); i_colony.erase(it1, it2); counter = 0; for (colony::iterator it = i_colony.begin(); it != i_colony.end(); ++it) { ++counter; } if (i_colony.size() != i_colony.group_size_sum()) { printf("Fuzz-test range-erase + fill-insert randomly Fails during erase - group_size_sum failure: loop counter: %u, internal_loop_counter: %u, size: %u, group_size_sum: %u.\n", loop_counter, internal_loop_counter, static_cast(i_colony.size()), static_cast(i_colony.group_size_sum())); getchar(); abort(); } if (i_colony.size() != static_cast(counter)) { printf("Fuzz-test range-erase + fill-insert randomly Fails during erase: loop counter: %u, internal_loop_counter: %u.\n", loop_counter, internal_loop_counter); getchar(); abort(); } if (i_colony.size() > 100) { // Test to make sure our stored erased_locations are valid & fill-insert is functioning properly in these scenarios const unsigned int extra_size = plf::rand() & 127; i_colony.insert(extra_size, 5); if (i_colony.size() != i_colony.group_size_sum()) { printf("Fuzz-test range-erase + fill-insert randomly Fails during insert - group_size_sum failure: loop counter: %u, internal_loop_counter: %u, size: %u, group_size_sum: %u.\n", loop_counter, internal_loop_counter, static_cast(i_colony.size()), static_cast(i_colony.group_size_sum())); getchar(); abort(); } if (i_colony.size() != static_cast(counter) + extra_size) { printf("Fuzz-test range-erase + fill-insert randomly Fails during fill-insert: loop counter: %u, internal_loop_counter: %u.\n", loop_counter, internal_loop_counter); getchar(); abort(); } counter = 0; for (colony::iterator it = i_colony.begin(); it != i_colony.end(); ++it) { ++counter; } if (i_colony.size() != static_cast(counter)) { printf("Fuzz-test range-erase + fill-insert randomly Fails during counter-test fill-insert: loop counter: %u, internal_loop_counter: %u.\n", loop_counter, internal_loop_counter); getchar(); abort(); } } ++internal_loop_counter; } } failpass("Fuzz-test range-erase + fill-insert randomly until empty", i_colony.size() == 0); i_colony.erase(i_colony.begin(), i_colony.end()); failpass("Range-erase when colony is empty test (crash test)", i_colony.size() == 0); i_colony.insert(10, 1); i_colony.erase(i_colony.begin(), i_colony.begin()); failpass("Range-erase when range is empty test (crash test)", i_colony.size() == 10); } { title1("Non-trivial type tests"); colony ss_nt; colony::iterator ss_it1, ss_it2; small_struct_non_trivial ss(5); unsigned int size, range1 = 0, range2 = 0, internal_loop_counter; int counter, sum1 = 0; ss_nt.insert(10000, ss); failpass("Non-trivial type insert test", ss_nt.size() == 10000); for (colony::iterator ss_it = ss_nt.begin(); ss_it != ss_nt.end(); ++ss_it) { ss_it = ss_nt.erase(ss_it); sum1 += ss_it->number; ++range1; } failpass("Non-trivial type erase half of all elements", ss_nt.size() == 5000); for (unsigned int loop_counter = 0; loop_counter != 50; ++loop_counter) { ss_nt.clear(); for (counter = 0; counter != 1000; ++counter) { ss_nt.insert(counter); } internal_loop_counter = 0; while (!ss_nt.empty()) { ss_it2 = ss_it1 = ss_nt.begin(); size = static_cast(ss_nt.size()); range1 = plf::rand() % size; range2 = range1 + 1 + (plf::rand() % (size - range1)); advance(ss_it1, static_cast(range1)); advance(ss_it2, static_cast(range2)); ss_nt.erase(ss_it1, ss_it2); counter = 0; for (colony::iterator ss_it = ss_nt.begin(); ss_it != ss_nt.end(); ++ss_it) { ++counter; } if (ss_nt.size() != static_cast(counter)) { printf("Fuzz-test range-erase randomly Fail: loop counter: %u, internal_loop_counter: %u.\n", loop_counter, internal_loop_counter); getchar(); abort(); } if (ss_nt.size() != ss_nt.group_size_sum()) { printf("Fuzz-test range-erase randomly Fail - group_size_sum failure: loop counter: %u, internal_loop_counter: %u, size: %u, group_size_sum: %u.\n", loop_counter, internal_loop_counter, static_cast(ss_nt.size()), static_cast(ss_nt.group_size_sum())); getchar(); abort(); } if (ss_nt.size() > 2) { // Test to make sure our stored erased_locations are valid ss_nt.insert(1); ss_nt.insert(10); } ++internal_loop_counter; } } failpass("Non-trivial type fuzz-test range-erase randomly until empty", ss_nt.size() == 0); } { title2("Sort tests"); colony i_colony; i_colony.reserve(50000); for (unsigned int temp = 0; temp != 50000; ++temp) { i_colony.insert(plf::rand() & 65535); } i_colony.sort(); bool sorted = true; int previous = 0; for (colony::iterator current = i_colony.begin(); current != i_colony.end(); ++current) { if (previous > *current) { sorted = false; break; } previous = *current; } failpass("Less-than sort test", sorted); i_colony.sort(std::greater()); previous = 65536; for (colony::iterator current = i_colony.begin(); current != i_colony.end(); ++current) { if (previous < *current) { sorted = false; break; } previous = *current; } failpass("Greater-than sort test", sorted); } { title2("Different insertion-style tests"); #ifdef PLF_TEST_INITIALIZER_LIST_SUPPORT colony i_colony({1, 2, 3}); failpass("Initializer-list constructor test", i_colony.size() == 3); #else colony i_colony(3, 1); #endif colony i_colony2(i_colony.begin(), i_colony.end()); failpass("Range constructor test", i_colony2.size() == 3); colony i_colony3(5000, 2, plf::colony_limits(100, 1000)); failpass("Fill construction test", i_colony3.size() == 5000); i_colony2.insert(500000, 5); failpass("Fill insertion test", i_colony2.size() == 500003); std::vector some_ints(500, 2); i_colony2.insert(some_ints.begin(), some_ints.end()); failpass("Range insertion test", i_colony2.size() == 500503); #ifdef PLF_TEST_CPP20_SUPPORT i_colony2.insert(some_ints.begin(), some_ints.cend()); failpass("Range insertion with differing iterators test", i_colony2.size() == 501003); #endif i_colony3.clear(); i_colony2.clear(); i_colony2.reserve(50000); i_colony2.insert(60000, 1); int total = 0; for (colony::iterator it = i_colony2.begin(); it != i_colony2.end(); ++it) { total += *it; } failpass("Reserve + fill insert test", i_colony2.size() == 60000 && total == 60000); i_colony2.clear(); i_colony2.reserve(5000); i_colony2.insert(60, 1); total = 0; for (colony::iterator it = i_colony2.begin(); it != i_colony2.end(); ++it) { total += *it; } failpass("Reserve + fill insert test 2", i_colony2.size() == 60 && total == 60); i_colony2.insert(6000, 1); total = 0; for (colony::iterator it = i_colony2.begin(); it != i_colony2.end(); ++it) { total += *it; } failpass("Reserve + fill + fill test", i_colony2.size() == 6060 && total == 6060); i_colony2.reserve(18000); i_colony2.insert(6000, 1); total = 0; for (colony::iterator it = i_colony2.begin(); it != i_colony2.end(); ++it) { total += *it; } failpass("Reserve + fill + fill + reserve + fill test", i_colony2.size() == 12060 && total == 12060); i_colony2.clear(); i_colony2.insert(6000, 2); failpass("Clear + fill test", i_colony2.size() == 6000 && *(i_colony2.begin()) == 2); i_colony.insert(i_colony2.begin(), i_colony2.end()); failpass("Range insert when not empty test", i_colony.size() == 6003); } { title2("Assign tests"); colony i_colony(50, 2); i_colony.assign(50, 1); int total = 0; for (colony::iterator it = i_colony.begin(); it != i_colony.end(); ++it) { total += *it; } failpass("Equal capacity assign test", i_colony.size() == 50 && total == 50); i_colony.assign(10, 2); total = 0; for (colony::iterator it = i_colony.begin(); it != i_colony.end(); ++it) { total += *it; } failpass("Lesser capacity assign test", i_colony.size() == 10 && total == 20); i_colony.assign(2000, 20); total = 0; for (colony::iterator it = i_colony.begin(); it != i_colony.end(); ++it) { total += *it; } failpass("Greater capacity assign test", i_colony.size() == 2000 && total == 40000); i_colony.clear(); for (unsigned int internal_loop_counter = 0; internal_loop_counter != 10; ++internal_loop_counter) { const unsigned int capacity = plf::rand() & 65535; i_colony.assign(capacity, 1); total = 0; for (colony::iterator it = i_colony.begin(); it != i_colony.end(); ++it) { total += *it; } if (i_colony.size() != capacity) { printf("Fuzz-test assign capacity Fail: global loop counter: %u, internal loop counter: %u.\n", looper, internal_loop_counter); getchar(); abort(); } if (i_colony.size() != static_cast(total)) { printf("Fuzz-test assign sum Fail: global loop counter: %u, internal loop counter: %u.\n", looper, internal_loop_counter); getchar(); abort(); } } message("Fuzz-test assign passed."); i_colony.clear(); std::vector i_vector; for (int counter = 1; counter != 11; ++counter) { i_vector.push_back(counter); } i_colony.assign(i_vector.begin(), i_vector.end()); colony::iterator it = i_colony.begin(); bool fail = false; for (int counter = 1; counter != 11; ++counter, ++it) { if (*it != counter) { fail = true; break; } } failpass("Range assign test", i_colony.size() == 10 && !fail); i_colony.clear(); for (unsigned int internal_loop_counter = 0; internal_loop_counter != 10; ++internal_loop_counter) { const unsigned int capacity = plf::rand() & 65535; i_vector.assign(capacity, 1); i_colony.assign(i_vector.begin(), i_vector.end()); total = 0; for (colony::iterator it3 = i_colony.begin(); it3 != i_colony.end(); ++it3) { total += *it3; } if (i_colony.size() != capacity) { printf("Fuzz-test range assign capacity Fail: global loop counter: %u, internal loop counter: %u.\n", looper, internal_loop_counter); getchar(); abort(); } if (i_colony.size() != static_cast(total)) { printf("Fuzz-test range assign sum Fail: global loop counter: %u, internal loop counter: %u.\n", looper, internal_loop_counter); getchar(); abort(); } } message("Fuzz-test range assign passed."); i_colony.clear(); #ifdef PLF_TEST_INITIALIZER_LIST_SUPPORT i_colony.assign({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); it = i_colony.begin(); for (int counter = 1; counter != 11; ++counter, ++it) { if (*it != counter) { fail = true; break; } } failpass("Initializer_list assign test", i_colony.size() == 10 && !fail); i_colony.clear(); #endif } #ifdef PLF_TEST_VARIADICS_SUPPORT { title2("Perfect Forwarding tests"); colony pf_colony; int lvalue = 0; int &lvalueref = lvalue; pf_colony.emplace(7, lvalueref); failpass("Perfect forwarding test", (*pf_colony.begin()).success); failpass("Perfect forwarding test 2", lvalueref == 1); } { title2("Basic emplace test"); colony ss_colony; int total1 = 0, total2 = 0; for (int counter = 0; counter != 100; ++counter) { ss_colony.emplace(counter); total1 += counter; } for (colony::iterator it = ss_colony.begin(); it != ss_colony.end(); ++it) { total2 += it->number; } failpass("Basic emplace test", total1 == total2); failpass("Basic emplace test 2", ss_colony.size() == 100); } { title2("Non-copyable type test"); colony temp; temp.emplace(1); temp.emplace(2); failpass("Non-copyable size test", temp.size() == 2); } #endif { title2("Misc function tests"); colony colony1; colony1.reshape(plf::colony_limits(50, 100)); colony1.insert(27); failpass("Change_group_sizes min-size test", colony1.capacity() == 50); for (int counter = 0; counter != 100; ++counter) { colony1.insert(counter); } failpass("Change_group_sizes max-size test", colony1.capacity() == 200); colony1.clear(); colony1.reshape(plf::colony_limits(200, 2000)); colony1.insert(27); failpass("Reinitialize min-size test", colony1.capacity() == 200); plf::colony_limits temp_limits = colony1.block_limits(); failpass("get_block_limits test", temp_limits.min == 200 && temp_limits.max == 2000); for (int counter = 0; counter != 3300; ++counter) { colony1.insert(counter); } failpass("Reinitialize max-size test", colony1.capacity() == 5200); colony1.reshape(plf::colony_limits(500, 500)); failpass("Change_group_sizes resize test", colony1.capacity() == 3500); colony1.reshape(plf::colony_limits(200, 200)); failpass("Change_maximum_group_size resize test", colony1.capacity() == 3400); } { title2("Splice tests"); { colony colony1, colony2; for(int number = 0; number != 20; ++number) { colony1.insert(number); colony2.insert(number + 20); } colony1.splice(colony2); int check_number = 0; bool fail = false; for (colony::iterator current = colony1.begin(); current != colony1.end(); ++current) { if (check_number++ != *current) { fail = true; } } failpass("Small splice test 1", fail == false); } { colony colony1, colony2; for(int number = 0; number != 100; ++number) { colony1.insert(number); colony2.insert(number + 100); } colony1.splice(colony2); int check_number = 0; bool fail = false; for (colony::iterator current = colony1.begin(); current != colony1.end(); ++current) { if (check_number++ != *current) { fail = true; } } failpass("Small splice test 2", fail == false); } { colony colony1, colony2; for(int number = 0; number != 100000; ++number) { colony1.insert(number); colony2.insert(number + 100000); } colony1.splice(colony2); int check_number = 0; bool fail = false; for (colony::iterator current = colony1.begin(); current != colony1.end(); ++current) { if (check_number++ != *current) { fail = true; } } failpass("Large splice test 1", fail == false); } { colony colony1, colony2; for(int number = 0; number != 100; ++number) { colony1.insert(number); colony2.insert(number + 100); } for (colony::iterator current = colony2.begin(); current != colony2.end();) { if ((plf::rand() & 7) == 0) { current = colony2.erase(current); } else { ++current; } } colony1.splice(colony2); int check_number = -1; bool fail = false; for (colony::iterator current = colony1.begin(); current != colony1.end(); ++current) { if (check_number >= *current) { fail = true; } check_number = *current; } failpass("Erase + splice test 1", fail == false); } { colony colony1, colony2; for(int number = 0; number != 100; ++number) { colony1.insert(number); colony2.insert(number + 100); } for (colony::iterator current = colony2.begin(); current != colony2.end();) { if ((plf::rand() & 3) == 0) { current = colony2.erase(current); } else { ++current; } } for (colony::iterator current = colony1.begin(); current != colony1.end();) { if ((plf::rand() & 1) == 0) { current = colony1.erase(current); } else { ++current; } } colony1.splice(colony2); int check_number = -1; bool fail = false; for (colony::iterator current = colony1.begin(); current != colony1.end(); ++current) { if (check_number >= *current) { fail = true; } check_number = *current; } failpass("Erase + splice test 2", fail == false); } { colony colony1, colony2; colony1.reshape(plf::colony_limits(200, 200)); colony2.reshape(plf::colony_limits(200, 200)); for(int number = 0; number != 100; ++number) { colony1.insert(number + 150); } for(int number = 0; number != 150; ++number) { colony2.insert(number); } colony1.splice(colony2); int check_number = -1; bool fail = false; for (colony::iterator current = colony1.begin(); current != colony1.end(); ++current) { if (check_number >= *current) { fail = true; } check_number = *current; } failpass("Unequal size splice test 1", fail == false); } { colony colony1(plf::colony_limits(200, 200)), colony2(plf::colony_limits(200, 200)); for(int number = 0; number != 100; ++number) { colony1.insert(100 - number); } for(int number = 0; number != 150; ++number) { colony2.insert(250 - number); } colony1.splice(colony2); int check_number = 255; bool fail = false; for (colony::iterator current = colony1.begin(); current != colony1.end(); ++current) { if (check_number < *current) { fail = true; } check_number = *current; } failpass("Unequal size splice test 2", fail == false); } { colony colony1, colony2; for(int number = 0; number != 100000; ++number) { colony1.insert(number + 200000); } for(int number = 0; number != 200000; ++number) { colony2.insert(number); } for (colony::iterator current = colony2.begin(); current != colony2.end();) { if ((plf::rand() & 1) == 0) { current = colony2.erase(current); } else { ++current; } } for (colony::iterator current = colony1.begin(); current != colony1.end();) { if ((plf::rand() & 1) == 0) { current = colony1.erase(current); } else { ++current; } } colony1.erase(--(colony1.end())); colony2.erase(--(colony2.end())); colony1.splice(colony2); // splice should swap the order at this point due to differences in numbers of unused elements at end of final group in each colony int check_number = -1; bool fail = false; for (colony::iterator current = colony1.begin(); current != colony1.end(); ++current) { if (check_number >= *current) { fail = true; break; } check_number = *current; } failpass("Large unequal size + erase splice test 1", fail == false); do { for (colony::iterator current = colony1.begin(); current != colony1.end();) { if ((plf::rand() & 3) == 0) { current = colony1.erase(current); } else if ((plf::rand() & 7) == 0) { colony1.insert(433); ++current; } else { ++current; } } } while (!colony1.empty()); failpass("Post-splice insert-and-erase randomly till-empty test", colony1.size() == 0); } } { title2("erase_if tests"); colony i_colony(100, 100); i_colony.insert(100, 200); colony i_colony2 = i_colony; erase(i_colony, 100); int total = std::accumulate(i_colony.begin(), i_colony.end(), 0); failpass("non-member erase test 1", total == 20000); erase(i_colony2, 200); total = std::accumulate(i_colony2.begin(), i_colony2.end(), 0); failpass("non-member erase test 2", total == 10000); i_colony.clear(); for(int count = 0; count != 1000; ++count) { i_colony.insert((plf::rand() & 1)); } i_colony2 = i_colony; const int count0 = static_cast(std::count(i_colony.begin(), i_colony.end(), 0)); const int count1 = 1000 - count0; erase(i_colony, 0); failpass("random non-member erase test 1", static_cast(i_colony.size()) == count1); erase(i_colony2, 1); failpass("random non-member erase test 2", static_cast(i_colony2.size()) == count0); i_colony.clear(); for(int count = 0; count != 1000; ++count) { i_colony.insert(count); } #ifdef PLF_TEST_MOVE_SEMANTICS_SUPPORT // approximating checking for C++11 here erase_if(i_colony, std::bind(std::greater(), std::placeholders::_1, 499)); #else // C++03 or lower erase_if(i_colony, std::bind2nd(std::greater(), 499)); #endif failpass("erase_if test", static_cast(i_colony.size()) == 500); } { title2("data() tests"); colony i_colony(10000, 5); int sum1 = 0, sum2 = 0, range1 = 0, range2 = 0; // Erase half of all elements and sum the rest: for (colony::iterator it = i_colony.begin(); it != i_colony.end(); ++it) { it = i_colony.erase(it); sum1 += *it; ++range1; } colony::colony_data *data = i_colony.data(); // Manually sum using raw memory blocks: for (unsigned int block_num = 0; block_num != data->number_of_blocks; ++block_num) { colony::aligned_element_type *current_element = data->block_pointers[block_num]; const unsigned char *bitfield_location = data->bitfield_pointers[block_num]; const size_t capacity = data->block_capacities[block_num]; size_t char_index = 0; unsigned char offset = 0; for (size_t index = 0; index != capacity; ++index, ++current_element) { if ((bitfield_location[char_index] >> offset) & 1) { sum2 += *(reinterpret_cast(current_element)); ++range2; } if (++offset == 8) { offset = 0; ++char_index; } } } delete data; failpass("Manual summing pass over elements obtained from data()", (sum1 == sum2) && (range1 == range2)); } } } } #ifdef TEST_MAIN int main() { sg14_test::plf_colony_test(); } #endif ================================================ FILE: SG14_test/ring_test.cpp ================================================ #include "SG14_test.h" #include "ring.h" #include #include #include #include static void basic_test() { std::array A; std::array B; sg14::ring_span Q(std::begin(A), std::end(A)); Q.push_back(7); Q.push_back(3); assert(Q.size() == 2); assert(Q.front() == 7); Q.pop_front(); assert(Q.size() == 1); Q.push_back(18); auto Q3 = std::move(Q); assert(Q3.front() == 3); assert(Q3.back() == 18); sg14::ring_span Q5(std::move(Q3)); assert(Q5.front() == 3); assert(Q5.back() == 18); assert(Q5.size() == 2); Q5.pop_front(); Q5.pop_front(); assert(Q5.empty()); sg14::ring_span Q6(std::begin(B), std::end(B)); Q6.push_back(6); Q6.push_back(7); Q6.push_back(8); Q6.push_back(9); Q6.emplace_back(10); Q6.swap(Q5); assert(Q6.empty()); assert(Q5.size() == 5); assert(Q5.front() == 6); assert(Q5.back() == 10); } static void filter_test() { std::array< double, 3 > A; sg14::ring_span< double > buffer( std::begin( A ), std::end( A ) ); buffer.push_back( 1.0 ); buffer.push_back( 2.0 ); buffer.push_back( 3.0 ); buffer.push_back( 5.0 ); assert( buffer.front() == 2.0 ); constexpr std::array< double, 3 > filter_coefficients = {{ 0.25, 0.5, 0.25 }}; // In an update loop, interrupt routine or the like buffer.push_back( 7.0 ); assert( std::inner_product( buffer.begin(), buffer.end(), filter_coefficients.begin(), 0.0 ) == 5.0 ); } static void iterator_regression_test() { std::array A; sg14::ring_span r(A.begin(), A.end()); r.push_back(1.0); decltype(r)::iterator it = r.end(); decltype(r)::const_iterator cit = r.end(); // test conversion from non-const to const assert(it == cit); // test comparison of const and non-const assert(it + 0 == it); assert(it - 1 == r.begin()); assert(cit + 0 == cit); assert(cit - 1 == r.cbegin()); assert(it - cit == 0); assert(cit - r.begin() == 1); std::array B; sg14::ring_span r2(B.begin(), B.end()); swap(r, r2); // test existence of ADL swap() // Set up the ring for the TEST_OP tests below. r = sg14::ring_span(A.begin(), A.end(), A.begin(), 2); assert(r.size() == 2); #define TEST_OP(op, a, b, c) \ assert(a(r.begin() op r.end())); \ assert(b(r.end() op r.begin())); \ assert(c(r.begin() op r.begin())) #define _ TEST_OP(==, !, !, _); TEST_OP(!=, _, _, !); TEST_OP(<, _, !, !); TEST_OP(<=, _, !, _); TEST_OP(>, !, _, !); TEST_OP(>=, !, _, _); #undef _ #undef TEST_OP } static void copy_popper_test() { std::vector v { "quick", "brown", "fox" }; sg14::ring_span> r(v.begin(), v.end(), {"popped"}); r.emplace_back("slow"); assert((v == std::vector{"slow", "brown", "fox"})); r.emplace_back("red"); assert((v == std::vector{"slow", "red", "fox"})); std::string result = r.pop_front(); assert((v == std::vector{"popped", "red", "fox"})); assert(result == "slow"); } static void reverse_iterator_test() { std::array A; sg14::ring_span r(A.begin(), A.end()); const sg14::ring_span c(A.begin(), A.end()); r.push_back(1); r.push_back(2); r.push_back(3); r.push_back(4); std::vector v(3); std::copy(r.begin(), r.end(), v.begin()); assert((v == std::vector{2,3,4})); std::copy(r.cbegin(), r.cend(), v.begin()); assert((v == std::vector{2,3,4})); std::copy(r.rbegin(), r.rend(), v.begin()); assert((v == std::vector{4,3,2})); std::copy(r.crbegin(), r.crend(), v.begin()); assert((v == std::vector{4,3,2})); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); } void sg14_test::ring_test() { basic_test(); filter_test(); iterator_regression_test(); copy_popper_test(); reverse_iterator_test(); } #ifdef TEST_MAIN int main() { sg14_test::ring_test(); } #endif ================================================ FILE: SG14_test/slot_map_test.cpp ================================================ #include "SG14_test.h" #include "slot_map.h" #include #include #include #include #include #include #include #include #include #include #include namespace TestKey { struct key_16_8_t { uint16_t index; uint8_t generation; }; struct key_11_5_t { // C++17 only uint16_t index : 11; uint8_t generation : 5; }; #if __cplusplus < 201703L template auto get(const K& k) { return get(k, std::integral_constant{}); } template auto& get(K& k) { return get(k, std::integral_constant{}); } const uint16_t& get(const key_16_8_t& k, std::integral_constant) { return k.index; } const uint8_t& get(const key_16_8_t& k, std::integral_constant) { return k.generation; } uint16_t& get(key_16_8_t& k, std::integral_constant) { return k.index; } uint8_t& get(key_16_8_t& k, std::integral_constant) { return k.generation; } #endif // __cplusplus < 201703L } // namespace TestKey namespace TestContainer { template struct Vector { using value_type = T; using reference = T&; using const_reference = const T&; using pointer = T*; using const_pointer = const T*; using size_type = unsigned; using iterator = T*; using const_iterator = const T*; using reverse_iterator = std::reverse_iterator; using const_reverse_iterator = std::reverse_iterator; Vector() = default; template::value>> Vector(const Vector& rhs) { *this = rhs; } Vector(Vector&& rhs) { *this = std::move(rhs); } template::value>> void operator=(const Vector& rhs) { size_ = rhs.size_; data_ = std::make_unique(size_); std::copy(rhs.begin(), rhs.end(), data_.get()); } void operator=(Vector&& rhs) { size_ = rhs.size_; data_ = std::move(rhs.data_); } unsigned size() const { return static_cast(size_); } T *begin() { return data_.get(); } T *end() { return data_.get() + size_; } const T *begin() const { return data_.get(); } const T *end() const { return data_.get() + size_; } void pop_back() { auto p = std::make_unique(size_ - 1); std::move(begin(), end() - 1, p.get()); size_ -= 1; data_ = std::move(p); } template::value>> void emplace_back(U t) { auto p = std::make_unique(size_ + 1); std::move(begin(), end(), p.get()); size_ += 1; data_ = std::move(p); *(end() - 1) = std::move(t); } void clear() { size_ = 0; data_ = nullptr; } friend void swap(Vector& a, Vector& b) { std::swap(a.size_, b.size_); std::swap(a.data_, b.data_); } private: size_t size_ = 0; std::unique_ptr data_ = nullptr; }; } // namespace TestContainer template struct Monad { using T = T_; static T value_of(const T& i) { return i; } template static T from_value(const U& v) { return static_cast(v); } }; template struct Monad> { using T = std::unique_ptr; static T_ value_of(const T& ptr) { return *ptr; } template static T from_value(const U& v) { return std::make_unique(v); } }; template class Container> void print_slot_map(const stdext::slot_map& sm) { printf("%d slots:", (int)sm.slots_.size()); for (auto&& slot : sm.slots_) { printf(" %d/%d", (int)slot.first, (int)slot.second); } printf("\n%d values:", (int)sm.values_.size()); for (auto&& value : sm) { printf(" %d", (int)value); } assert(sm.reverse_map_.size() == sm.size()); printf("\n%d reverse_map:", (int)sm.reverse_map_.size()); for (auto&& idx : sm.reverse_map_) { printf(" %d", (int)idx); } printf("\nnext_available_slot_index: %d\n", (int)sm.next_available_slot_index_); } template::value, const T&, T&&>> static U move_if_necessary(T& value) { return static_cast(value); } template static void BasicTests(T t1, T t2) { SM sm; assert(sm.empty()); assert(sm.size() == 0); SM sm2 = move_if_necessary(sm); assert(sm2.empty()); assert(sm2.size() == 0); auto k1 = sm.insert(std::move(t1)); auto k2 = sm.insert(move_if_necessary(t2)); assert(!sm.empty()); assert(sm.size() == 2); assert(std::next(sm.begin(), 2) == sm.end()); assert(sm.find(k1) == sm.begin()); assert(sm.find(k2) == std::next(sm.begin())); assert(sm2.empty()); assert(sm2.size() == 0); auto num_removed = sm.erase(k1); assert(num_removed == 1); assert(sm.size() == 1); assert(sm.find(k1) == sm.end()); // find an expired key try { (void)sm.at(k1); assert(false); } catch (const std::out_of_range&) {} assert(sm.find(k2) == sm.begin()); // find a non-expired key assert(sm.at(k2) == *sm.begin()); assert(sm.find_unchecked(k2) == sm.begin()); assert(sm[k2] == *sm.begin()); assert(sm.erase(k1) == 0); // erase an expired key sm.swap(sm2); assert(sm.empty()); assert(sm2.size() == 1); assert(sm2.find(k1) == sm2.end()); // find an expired key assert(sm2.find(k2) == sm2.begin()); // find a non-expired key assert(sm2.erase(k1) == 0); // erase an expired key } template static void FullContainerStressTest(TGen t) { const int total = 1000; SM sm; std::vector keys; for (int i=0; i < total; ++i) { auto k = sm.insert(t()); keys.push_back(k); } assert(sm.size() == total); std::mt19937 g; std::shuffle(keys.begin(), keys.end(), g); for (int i = 0; i < total; ++i) { assert(sm.size() == static_cast(total - i)); assert(sm.find(keys[i]) != sm.end()); assert(sm.find_unchecked(keys[i]) != sm.end()); for (int j = 0; j < i; ++j) { assert(sm.find(keys[j]) == sm.end()); } auto erased = sm.erase(keys[i]); assert(erased == 1); } assert(sm.empty()); } template static void InsertEraseStressTest(TGen t) { const int total = 1000; SM sm; std::vector valid_keys; std::vector expired_keys; std::mt19937 g; for (int i=0; i < total / 3; ++i) { auto k = sm.insert(t()); valid_keys.push_back(k); } for (int i = total / 3; i < total; ++i) { if (g() % 2 == 0 && !valid_keys.empty()) { std::shuffle(valid_keys.begin(), valid_keys.end(), g); auto k = valid_keys.back(); valid_keys.pop_back(); auto erased = sm.erase(k); assert(erased == 1); expired_keys.push_back(k); for (auto&& ek : expired_keys) { assert(sm.find(ek) == sm.end()); } } else { auto k = sm.insert(t()); valid_keys.push_back(k); } } } template static void EraseInLoopTest() { using T = typename SM::mapped_type; SM sm; for (int i=0; i < 100; ++i) { sm.insert(Monad::from_value(i)); } int total = 0; for (auto it = sm.begin(); it != sm.end(); /*nothing*/) { total += Monad::value_of(*it); if (Monad::value_of(*it) > 50) { it = sm.erase(it); } else { ++it; } } assert(total == 4950); int total2 = 0; for (auto&& elt : sm) { total2 += Monad::value_of(elt); } assert(total2 == 1275); } template static void EraseRangeTest() { using T = typename SM::mapped_type; SM sm; auto test = [&](int N, int first, int last) { sm.erase(sm.begin(), sm.end()); int expected_total = 0; for (int i=0; i < N; ++i) { expected_total += i; sm.insert(Monad::from_value(i)); } for (auto it = std::next(sm.begin(), first); it != std::next(sm.begin(), last); ++it) { expected_total -= Monad::value_of(*it); } sm.erase(std::next(sm.begin(), first), std::next(sm.begin(), last)); int actual_total = 0; for (auto it = sm.begin(); it != sm.end(); ++it) { actual_total += Monad::value_of(*it); } return (actual_total == expected_total); }; assert(test(10, 8, 8)); assert(test(10, 3, 7)); assert(test(10, 0, 10)); assert(test(10, 1, 10)); assert(test(10, 0, 9)); assert(test(10, 1, 9)); for (int N : { 2, 10, 100 }) { for (int i=0; i < N; ++i) { for (int j=i; j < N; ++j) { assert(test(N, i, j)); } } } } template static void ReserveTest() { using T = typename SM::mapped_type; SM sm; auto k = sm.emplace(Monad::from_value(1)); (void)k; assert(sm.size() == 1); auto original_cap = sm.slot_count(); static_assert(std::is_same::value, ""); assert(original_cap >= 1); sm.reserve_slots(original_cap + 3); assert(sm.slot_count() >= original_cap + 3); assert(sm.size() == 1); sm.emplace(Monad::from_value(2)); sm.emplace(Monad::from_value(3)); sm.emplace(Monad::from_value(4)); assert(sm.size() == 4); } template().capacity())> static void VerifyCapacityExists(bool expected) { assert(expected); SM sm; auto n = sm.capacity(); static_assert(std::is_same::value, ""); assert(n == 0); sm.reserve(100); assert(sm.capacity() >= 100); assert(sm.slot_count() >= 100); } template void VerifyCapacityExists(Bool expected) { assert(!expected); SM sm; sm.reserve(100); assert(sm.slot_count() >= 100); } static void TypedefTests() { if (true) { using SM = stdext::slot_map; static_assert(std::is_same>::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same>::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::iterator>::value, ""); static_assert(std::is_same::const_iterator>::value, ""); static_assert(std::is_same::reverse_iterator>::value, ""); static_assert(std::is_same::const_reverse_iterator>::value, ""); static_assert(std::is_same::size_type>::value, ""); static_assert(std::is_same::value, ""); } if (true) { using SM = stdext::slot_map; static_assert(std::is_same>::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same>::value, ""); static_assert(std::is_same::reference>::value, ""); static_assert(std::is_same::const_reference>::value, ""); static_assert(std::is_same::pointer>::value, ""); static_assert(std::is_same::const_pointer>::value, ""); static_assert(std::is_same::iterator>::value, ""); static_assert(std::is_same::const_iterator>::value, ""); static_assert(std::is_same::reverse_iterator>::value, ""); static_assert(std::is_same::const_reverse_iterator>::value, ""); static_assert(std::is_same::size_type>::value, ""); static_assert(std::is_same::value, ""); } if (true) { using SM = stdext::slot_map; static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same>::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::iterator>::value, ""); static_assert(std::is_same::const_iterator>::value, ""); static_assert(std::is_same::reverse_iterator>::value, ""); static_assert(std::is_same::const_reverse_iterator>::value, ""); static_assert(std::is_same::size_type>::value, ""); static_assert(std::is_same::value, ""); } if (true) { using SM = stdext::slot_map, TestContainer::Vector>; static_assert(std::is_same>::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same>::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::iterator>::value, ""); static_assert(std::is_same::const_iterator>::value, ""); static_assert(std::is_same::reverse_iterator>::value, ""); static_assert(std::is_same::const_reverse_iterator>::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); } #if __cplusplus >= 201703L if (true) { using SM = stdext::slot_map; static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same>::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::value, ""); static_assert(std::is_same::iterator>::value, ""); static_assert(std::is_same::const_iterator>::value, ""); static_assert(std::is_same::reverse_iterator>::value, ""); static_assert(std::is_same::const_reverse_iterator>::value, ""); static_assert(std::is_same::size_type>::value, ""); static_assert(std::is_same::value, ""); } #endif // __cplusplus >= 201703L } template void BoundsCheckingTest() { using T = typename SM::mapped_type; SM sm; const auto& csm = sm; sm.emplace(Monad::from_value(1)); typename SM::key_type k = sm.emplace(Monad::from_value(2)); sm.clear(); typename SM::iterator it = sm.find(k); assert(it == sm.end()); typename SM::const_iterator cit = csm.find(k); assert(cit == sm.end()); } template static void GenerationsDontSkipTest() { using T = typename SM::mapped_type; SM sm; auto k1 = sm.emplace(Monad::from_value(1)); int original_cap = static_cast(sm.slot_count()); for (int i=1; i < original_cap; ++i) { sm.emplace(Monad::from_value(i)); } assert(sm.size() == sm.slot_count()); sm.erase(k1); auto k2 = sm.emplace(Monad::from_value(2)); #if __cplusplus < 201703L using std::get; assert(get<0>(k2) == get<0>(k1)); assert(get<1>(k2) == get<1>(k1) + 1); #else auto [idx1, gen1] = k1; auto [idx2, gen2] = k2; assert(idx2 == idx1); assert(gen2 == gen1 + 1); #endif } template static void IndexesAreUsedEvenlyTest() { using T = typename SM::mapped_type; SM sm; auto k1 = sm.emplace(Monad::from_value(1)); auto k2 = sm.emplace(Monad::from_value(2)); int original_cap = static_cast(sm.slot_count()); for (int i=2; i < original_cap; ++i) { sm.emplace(Monad::from_value(i)); } assert(sm.size() == sm.slot_count()); sm.erase(k1); sm.erase(k2); assert(sm.size() == sm.slot_count() - 2); // There are now two slots available. // So consecutive insertions should prefer to // use different slots, rather than sticking to // just one slot and bumping its generation count. k1 = sm.emplace(Monad::from_value(1)); sm.erase(k1); k2 = sm.emplace(Monad::from_value(2)); sm.erase(k2); #if __cplusplus < 201703L using std::get; assert(get<0>(k2) != get<0>(k1)); #else auto [idx1, gen1] = k1; auto [idx2, gen2] = k2; assert(idx2 != idx1); #endif } void sg14_test::slot_map_test() { TypedefTests(); // Test the most basic slot_map. using slot_map_1 = stdext::slot_map; static_assert(std::is_nothrow_move_constructible::value, "preserve nothrow-movability of vector"); BasicTests(42, 37); BoundsCheckingTest(); FullContainerStressTest([]() { return 1; }); InsertEraseStressTest([i=3]() mutable { return ++i; }); EraseInLoopTest(); EraseRangeTest(); ReserveTest(); VerifyCapacityExists(true); GenerationsDontSkipTest(); IndexesAreUsedEvenlyTest(); // Test slot_map with a custom key type (C++14 destructuring). using slot_map_2 = stdext::slot_map; BasicTests(425, 375); BoundsCheckingTest(); FullContainerStressTest([]() { return 42; }); InsertEraseStressTest([i=5]() mutable { return ++i; }); EraseInLoopTest(); EraseRangeTest(); ReserveTest(); VerifyCapacityExists(true); GenerationsDontSkipTest(); IndexesAreUsedEvenlyTest(); #if __cplusplus >= 201703L // Test slot_map with a custom key type (C++17 destructuring). using slot_map_3 = stdext::slot_map; BasicTests(42, 37); BoundsCheckingTest(); FullContainerStressTest([]() { return 42; }); InsertEraseStressTest([i=3]() mutable { return ++i; }); EraseInLoopTest(); EraseRangeTest(); ReserveTest(); VerifyCapacityExists(true); GenerationsDontSkipTest(); IndexesAreUsedEvenlyTest(); #endif // __cplusplus >= 201703L // Test slot_map with a custom (but standard and random-access) container type. using slot_map_4 = stdext::slot_map, std::deque>; BasicTests(415, 315); BoundsCheckingTest(); FullContainerStressTest([]() { return 37; }); InsertEraseStressTest([i=7]() mutable { return ++i; }); EraseInLoopTest(); EraseRangeTest(); ReserveTest(); VerifyCapacityExists(false); GenerationsDontSkipTest(); IndexesAreUsedEvenlyTest(); // Test slot_map with a custom (non-standard, random-access) container type. using slot_map_5 = stdext::slot_map, TestContainer::Vector>; static_assert(!std::is_nothrow_move_constructible::value, "preserve non-nothrow-movability of Vector"); BasicTests(415, 315); BoundsCheckingTest(); FullContainerStressTest([]() { return 37; }); InsertEraseStressTest([i=7]() mutable { return ++i; }); EraseInLoopTest(); EraseRangeTest(); ReserveTest(); VerifyCapacityExists(false); GenerationsDontSkipTest(); IndexesAreUsedEvenlyTest(); // Test slot_map with a custom (standard, bidirectional-access) container type. using slot_map_6 = stdext::slot_map, std::list>; static_assert(std::is_nothrow_move_constructible::value == std::is_nothrow_move_constructible>::value, "preserve implementation-defined nothrow-movability of std::list"); BasicTests(415, 315); BoundsCheckingTest(); FullContainerStressTest([]() { return 37; }); InsertEraseStressTest([i=7]() mutable { return ++i; }); EraseInLoopTest(); EraseRangeTest(); ReserveTest(); VerifyCapacityExists(false); GenerationsDontSkipTest(); IndexesAreUsedEvenlyTest(); // Test slot_map with a move-only value_type. // Sadly, standard containers do not propagate move-only-ness, so we must use our custom Vector instead. using slot_map_7 = stdext::slot_map, std::pair, TestContainer::Vector>; static_assert(std::is_move_constructible::value, ""); static_assert(std::is_move_assignable::value, ""); static_assert(! std::is_copy_constructible::value, ""); static_assert(! std::is_copy_assignable::value, ""); BasicTests(std::make_unique(1), std::make_unique(2)); BoundsCheckingTest(); FullContainerStressTest([]() { return std::make_unique(1); }); InsertEraseStressTest([i=7]() mutable { return std::make_unique(++i); }); EraseInLoopTest(); EraseRangeTest(); ReserveTest(); VerifyCapacityExists(false); GenerationsDontSkipTest(); IndexesAreUsedEvenlyTest(); } #if defined(__cpp_concepts) template class Ctr, class T = int> concept bool SlotMapContainer = requires(Ctr c, const Ctr cc, T t) { { Ctr{} }; // default constructible, destructible { Ctr(cc) }; // copy constructible { Ctr(static_cast&&>(c)) }; // move constructible { c = cc }; // copy assignable { c = static_cast&&>(c) }; // move assignable typename Ctr::value_type; typename Ctr::size_type; typename Ctr::reference; typename Ctr::const_reference; typename Ctr::pointer; typename Ctr::const_pointer; typename Ctr::iterator; typename Ctr::const_iterator; typename Ctr::reverse_iterator; typename Ctr::const_reverse_iterator; { c.emplace_back(t) }; { c.pop_back() }; { c.begin() } -> typename Ctr::iterator; { c.end() } -> typename Ctr::iterator; { cc.size() } -> typename Ctr::size_type; { cc.begin() } -> typename Ctr::const_iterator; { cc.end() } -> typename Ctr::const_iterator; { std::next(c.begin()) } -> typename Ctr::iterator; { std::next(cc.begin()) } -> typename Ctr::const_iterator; }; static_assert(SlotMapContainer); static_assert(SlotMapContainer); static_assert(SlotMapContainer); static_assert(not SlotMapContainer); static_assert(not SlotMapContainer); #endif // defined(__cpp_concepts) #ifdef TEST_MAIN int main() { sg14_test::slot_map_test(); } #endif ================================================ FILE: SG14_test/uninitialized_test.cpp ================================================ #include "SG14_test.h" #include #include #include #include #include #include "algorithm_ext.h" #include #include namespace { struct lifetest { static uint64_t construct; static uint64_t destruct; static uint64_t move; lifetest() { ++construct; } lifetest(lifetest&& /*in*/) noexcept { ++move; } ~lifetest() { ++destruct; } static void reset() { construct = 0; destruct = 0; move = 0; } // To avoid "unused argument" error/warning. #ifdef NDEBUG static void test(uint64_t, uint64_t, uint64_t) { } #else static void test(uint64_t inconstruct, uint64_t indestruct, uint64_t inmove) { assert(construct == inconstruct); assert(destruct == indestruct); assert(move == inmove); } #endif }; uint64_t lifetest::construct; uint64_t lifetest::destruct; uint64_t lifetest::move; void value() { for (auto n = 0; n < 256; ++n) { auto m = (lifetest*)malloc(sizeof(lifetest) * n); lifetest::test(0, 0, 0); stdext::uninitialized_value_construct(m, m + n); lifetest::test(n, 0, 0); stdext::destruct(m, m + n); lifetest::test(n, n, 0); free(m); lifetest::reset(); } auto m = (int*)malloc(sizeof(int) * 5); stdext::uninitialized_value_construct(m, m + 5); assert(std::all_of(m, m + 5, [](int x) { return x == 0; })); free(m); }; void def() { for (auto n = 0; n < 256; ++n) { auto mem1 = (lifetest*)malloc(sizeof(lifetest) * n); lifetest::test(0, 0, 0); stdext::uninitialized_default_construct(mem1, mem1 + n); lifetest::test(n, 0, 0); auto mem2 = (lifetest*)malloc(sizeof(lifetest) * n); stdext::uninitialized_move(mem1, mem1 + n, mem2); lifetest::test(n, 0, n); stdext::destruct(mem2, mem2 + n); lifetest::test(n, n, n); free(mem1); free(mem2); lifetest::reset(); } } } void sg14_test::uninitialized_test() { value(); def(); } #ifdef TEST_MAIN int main() { sg14_test::uninitialized_test(); } #endif ================================================ FILE: SG14_test/unstable_remove_test.cpp ================================================ #include "SG14_test.h" #include "algorithm_ext.h" #include #include #include #include #include #include struct foo { std::array info; static foo make() { foo result; std::fill(result.info.begin(), result.info.end(), ::rand()); return result; } }; void sg14_test::unstable_remove_test() { size_t test_runs = 200; auto makelist = [] { std::vector list; std::generate_n(std::back_inserter(list), 30000, foo::make); return list; }; auto cmp = [](foo& f) {return f.info[0] & 1; }; auto partitionfn = [&](std::vector& f) { stdext::partition(f.begin(), f.end(), cmp); }; auto unstablefn = [&](std::vector& f) { stdext::unstable_remove_if(f.begin(), f.end(), cmp); }; auto removefn = [&](std::vector& f) { stdext::remove_if(f.begin(), f.end(), cmp); }; auto time = [&](auto&& f) { auto list = makelist(); auto t0 = std::chrono::high_resolution_clock::now(); f(list); auto t1 = std::chrono::high_resolution_clock::now(); return (t1 - t0).count(); }; using time_lambda_t = decltype(time(removefn)); auto median = [](std::vector& v) { auto b = v.begin(); auto e = v.end(); return *(b + ((e - b) / 2)); }; std::vector partition; std::vector unstable_remove_if; std::vector remove_if; partition.reserve(test_runs); unstable_remove_if.reserve(test_runs); remove_if.reserve(test_runs); for (decltype(test_runs) i = 0; i < test_runs; ++i) { remove_if.push_back(time(removefn)); unstable_remove_if.push_back(time(unstablefn)); partition.push_back(time(partitionfn)); } std::sort(partition.begin(), partition.end()); std::sort(unstable_remove_if.begin(), unstable_remove_if.end()); std::sort(remove_if.begin(), remove_if.end()); auto partition_med = median(partition); auto unstable_med = median(unstable_remove_if); auto remove_med = median(remove_if); std::cout << "partition: " << partition_med << "\n"; std::cout << "unstable: " << unstable_med << "\n"; std::cout << "remove_if: " << remove_med << "\n"; } #ifdef TEST_MAIN int main() { sg14_test::unstable_remove_test(); } #endif