Skip to content

Commit

Permalink
updated README
Browse files Browse the repository at this point in the history
  • Loading branch information
KRM7 committed Feb 16, 2024
1 parent e767c39 commit 976a840
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/sanitizers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
pkgs: clang-15 llvm-15

env:
ASAN_OPTIONS: check_initialization_order=1:strict_init_order=1:detect_stack_use_after_return=1:detect_leaks=1
ASAN_OPTIONS: check_initialization_order=1:strict_init_order=1:detect_stack_use_after_return=1:detect_leaks=1:detect_invalid_pointer_pairs=2
UBSAN_OPTIONS: print_stacktrace=1:print_summary=1

defaults:
Expand Down
114 changes: 105 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,119 @@
A fully constexpr unique_ptr implementation in C++20 with small object optimization.

### `small_unique_ptr<T>`

A constexpr `unique_ptr` implementation in C++20 with small object optimization.

```cpp
small_unique_ptr<Base> p = make_unique_small<Derived>();
```

Objects created with `make_unique_small<T>` are allocated on the stack if:

- Their size is not greater than `64 - 2 * sizeof(void*)`
- Their required alignment is not greater than `64`
- Their move constructor is declared as `noexcept`
- Their size is not greater than the size of the internal stack buffer
- Their required alignment is not greater than 64
- Their move constructor is `noexcept`

The size of a `small_unique_ptr<T>` object is:
The size of the stack buffer is architecture dependent, but on 64 bit architectures it will
generally be:

- `64` if T can be allocated in the stack buffer
- 48 for polymorphic types
- 56 for polymorphic types that implement a virtual `small_unique_ptr_move` method
- `sizeof(T)` for non-polymophic types, with an upper limit of 56

The overall size of a `small_unique_ptr<T>` object is:

- 64 if `T` may be allocated in the stack buffer
- `sizeof(T*)` otherwise

The interface matches `std::unique_ptr<T>`, except for:

- There is no `Deleter` template parameter or any of the associated methods
- Constructors from pointers are not provided except for the `nullptr` constructor
- Constructors from pointers are not provided except for the nullptr constructor
- `release()` is not implemented
- `T` can't be an incomplete type or an array type
- There are a couple of extra methods for checking where objects are allocated

The stack buffer is not used in constant evaluated contexts, so any constexpr usage
is subject to the same transient allocation requirements that `std::unique_ptr<T>` would be.
Everything is constexpr, but the stack buffer is not used in constant evaluated contexts,
so any constexpr usage is subject to the same transient allocation requirements that a constexpr
`std::unique_ptr<T>` would be.

--------------------------------------------------------------------------------------------------

<details>
<summary>
Example of a simplified <code>move_only_function</code> implementation with small object
optimization using <code>small_unique_ptr</code>
</summary>

```cpp
template<typename...>
class move_only_function;

template<typename Ret, typename... Args>
class move_only_function<Ret(Args...)>
{
public:
constexpr move_only_function() noexcept = default;
constexpr move_only_function(std::nullptr_t) noexcept {}

template<typename F>
requires(!std::is_same_v<F, move_only_function> && std::is_invocable_r_v<Ret, F&, Args...>)
constexpr move_only_function(F f) noexcept(noexcept(make_unique_small<Impl<F>>(std::move(f)))) :
fptr_(make_unique_small<Impl<F>>(std::move(f)))
{}

template<typename F>
requires(!std::is_same_v<F, move_only_function> && std::is_invocable_r_v<Ret, F&, Args...>)
constexpr move_only_function& operator=(F f) noexcept(noexcept(make_unique_small<Impl<F>>(std::move(f))))
{
fptr_ = make_unique_small<Impl<F>>(std::move(f));
return *this;
}

constexpr move_only_function(move_only_function&&) = default;
constexpr move_only_function& operator=(move_only_function&&) = default;

constexpr Ret operator()(Args... args) const
{
return fptr_->invoke(std::forward<Args>(args)...);
}

constexpr void swap(move_only_function& other) noexcept
{
fptr_.swap(other.fptr_);
}

constexpr explicit operator bool() const noexcept { return static_cast<bool>(fptr_); }

private:
struct ImplBase
{
constexpr virtual Ret invoke(Args...) = 0;
constexpr virtual void small_unique_ptr_move(void* dst) noexcept = 0;
constexpr virtual ~ImplBase() = default;
};

template<typename Callable>
struct Impl : public ImplBase
{
constexpr Impl(Callable func) noexcept(std::is_nothrow_move_constructible_v<Callable>) :
func_(std::move(func))
{}

constexpr void small_unique_ptr_move(void* dst) noexcept override
{
std::construct_at(static_cast<Impl*>(dst), std::move(*this));
}

constexpr Ret invoke(Args... args) override
{
return std::invoke(func_, std::forward<Args>(args)...);
}

Callable func_;
};

small_unique_ptr<ImplBase> fptr_ = nullptr;
};
```
</details>
2 changes: 1 addition & 1 deletion core-guidelines.ruleset
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Rules for GeneticAlgorithm" Description="Code analysis rules for GeneticAlgorithm.vcxproj." ToolsVersion="17.0">
<RuleSet Name="Rules" Description="Code analysis rules." ToolsVersion="17.0">
<Include Path="cppcorecheckarithmeticrules.ruleset" Action="Default" />
<Include Path="cppcorecheckboundsrules.ruleset" Action="Default" />
<Include Path="cppcorecheckclassrules.ruleset" Action="Default" />
Expand Down
33 changes: 18 additions & 15 deletions src/small_unique_ptr.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,20 +116,22 @@ namespace detail

pointer buffer(std::ptrdiff_t offset = 0) const noexcept
{
assert(0 <= offset && offset < buffer_size_v<T>);
return std::launder(reinterpret_cast<pointer>(std::addressof(buffer_[offset])));
return std::launder(reinterpret_cast<pointer>(static_cast<unsigned char*>(buffer_) + offset));
}

template<typename U>
void move_buffer_to(small_unique_ptr_base<U>& dst) noexcept
{
move_(this->buffer(), dst.buffer());
move_(buffer(), dst.buffer());
dst.move_ = move_;
}

constexpr bool is_stack_allocated() const noexcept { return static_cast<bool>(this->move_); }
constexpr bool is_stack_allocated() const noexcept
{
return static_cast<bool>(move_);
}

alignas(buffer_alignment_v<T>) mutable buffer_t buffer_ = {};
alignas(buffer_alignment_v<T>) mutable buffer_t buffer_;
T* data_ = nullptr;
move_fn move_ = nullptr;
};
Expand All @@ -152,19 +154,21 @@ namespace detail

pointer buffer(std::ptrdiff_t offset = 0) const noexcept
{
assert(0 <= offset && offset < buffer_size_v<T>);
return std::launder(reinterpret_cast<pointer>(std::addressof(buffer_[offset])));
return std::launder(reinterpret_cast<pointer>(static_cast<unsigned char*>(buffer_) + offset));
}

template<typename U>
void move_buffer_to(small_unique_ptr_base<U>& dst) noexcept
{
std::construct_at(dst.buffer(), std::move(*this->buffer()));
std::construct_at(dst.buffer(), std::move(*buffer()));
}

constexpr bool is_stack_allocated() const noexcept { return !std::is_constant_evaluated() && (data_ == this->buffer()); }
constexpr bool is_stack_allocated() const noexcept
{
return !std::is_constant_evaluated() && (data_ == buffer());
}

alignas(buffer_alignment_v<T>) mutable buffer_t buffer_ = {};
alignas(buffer_alignment_v<T>) mutable buffer_t buffer_;
T* data_ = nullptr;
};

Expand All @@ -177,8 +181,7 @@ namespace detail

pointer buffer(std::ptrdiff_t offset = 0) const noexcept
{
assert(0 <= offset && offset < buffer_size_v<T>);
return std::launder(reinterpret_cast<pointer>(std::addressof(buffer_[offset])));
return std::launder(reinterpret_cast<pointer>(static_cast<unsigned char*>(buffer_) + offset));
}

template<typename U>
Expand All @@ -194,15 +197,15 @@ namespace detail
if (std::is_constant_evaluated()) return false;

const volatile unsigned char* data = reinterpret_cast<const volatile unsigned char*>(data_);
const volatile unsigned char* buffer_first = std::addressof(this->buffer_[0]);
const volatile unsigned char* buffer_first = static_cast<unsigned char*>(buffer_);
const volatile unsigned char* buffer_last = buffer_first + buffer_size_v<T>;

assert(reinterpret_cast<std::uintptr_t>(buffer_last) - reinterpret_cast<std::uintptr_t>(buffer_first) == buffer_size_v<T>);

return std::less_equal{}(buffer_first, data) && std::less{}(data, buffer_last);
}

alignas(buffer_alignment_v<T>) mutable buffer_t buffer_ = {};
alignas(buffer_alignment_v<T>) mutable buffer_t buffer_;
T* data_ = nullptr;
};

Expand Down Expand Up @@ -453,7 +456,7 @@ namespace detail
struct make_unique_small_impl
{
template<typename T, typename... Args>
static constexpr small_unique_ptr<T> invoke(Args... args)
static constexpr small_unique_ptr<T> invoke(Args&&... args)
noexcept(std::is_nothrow_constructible_v<T, Args...> && !detail::is_always_heap_allocated_v<T>)
{
small_unique_ptr<T> ptr;
Expand Down

0 comments on commit 976a840

Please sign in to comment.