9
\$\begingroup\$

The Unique Function is supposed to be able to replace std::function in most situations where you do not need to be able to copy the functions, just move them. This has the primary advantage of being able to take move-only function objects, including lambdas that have std::promise moved into them. It also features small-buffer optimization.

unique_function.h

#pragma once

#include "core/memory/small_buffer.h"

namespace alchemist
{
    namespace core
    {
        template <typename Signature>
        class UniqueFunction;

        /**
        * Move only function implementation that assumes ownership over passed functions,
        * mainly useful in that it accepts function objects that themselves are move-only.
        * Utilizes small buffer optimization internally
        * @tparam RetT  the return type of any functions
        * @tparam Args  the types of the arguments to the function
        **/
        template<typename RetT, typename... Args>
        class UniqueFunction<RetT(Args...)>
        {
        public:
            /**
            * Constructor
            * @tparam FuncT the type of the passed function
            * @param function   the function held by this UniqueFunction
            **/
            template <typename FuncT>
            UniqueFunction(FuncT&& function);

            UniqueFunction(const UniqueFunction<RetT(Args...)>& rhs) = delete;
            UniqueFunction(UniqueFunction<RetT(Args...)>&& rhs) = default;

            UniqueFunction<RetT(Args...)>& operator=(const UniqueFunction<RetT(Args...)>& rhs) = delete;
            UniqueFunction<RetT(Args...)>& operator=(UniqueFunction<RetT(Args...)> && rhs) = default;

            /**
            * Calls the function held by this object.
            * @param args   the arguments passed to the held function
            * @returns the return value of the held function
            **/
            RetT operator()(Args&&... args);
        private:
            class IFunctionHolder
            {
            public:
                virtual RetT operator()(Args&&...) = 0;
            };

            template<typename FuncT> 
            class FunctionHolder : public IFunctionHolder
            {
            public:
                virtual ~FunctionHolder() = default;

                FunctionHolder(FuncT&& function);

                virtual RetT operator()(Args&&...) override;
            private:
                FuncT m_function;
            };
            SmallBuffer<sizeof(void*) * 4, IFunctionHolder> m_function_buffer;
        };
    }
}

#include "core/functional/detail/unique_function.hpp"

detail/unique_function.hpp

#pragma once

#include "core/functional/unique_function.h"

namespace alchemist
{
    namespace core
    {
            template<typename RetT, typename... Args>
            template <typename FuncT>
            UniqueFunction<RetT(Args...)>::UniqueFunction(FuncT&& function)
            {
                m_function_buffer.template emplace < FunctionHolder<FuncT> >(std::forward<FuncT>(function));
            }

            template<typename RetT, typename... Args>
            RetT UniqueFunction<RetT(Args...)>::operator()(Args&&... args)
            {
                return (*m_function_buffer.get())(std::forward<Args>(args)...);
            }

            template<typename RetT, typename... Args>
            template<typename FuncT> 
            UniqueFunction<RetT(Args...)>::FunctionHolder<FuncT>::FunctionHolder(FuncT&& function)
                : m_function(std::forward<FuncT>(function))
            {
            }

            template<typename RetT, typename... Args>
            template<typename FuncT>
            RetT UniqueFunction<RetT(Args...)>::FunctionHolder<FuncT>::operator()(Args&&... args)
            {
                return m_function(std::forward<Args>(args)...);
            }

    }
}

small_buffer.h

#pragma once

#include <variant>
#include <array>
#include <memory>

namespace alchemist
{
    namespace core
    {
        /**
        * Small buffer optimization implementation intended for RTTI-types, ie types that have a virtual function.
        * Move only.
        *
        * @tparam MaxSize   the maximum size of the object before it no longer will be stored locally and
        *                   put on the heap instead
        * @tparam Interface the interface type for objects put in the buffer
        **/
        template<size_t MaxSize, typename Interface>
        class SmallBuffer
        {
        public:
            
            /**
            * Constructor
            * Produces an empty buffer
            **/
            SmallBuffer();
            template<typename T>
            
            /**
            * Constructor
            * @param obj    moves the passed object into the buffer, holding it there
            **/
            explicit SmallBuffer(T&& obj);

            SmallBuffer(SmallBuffer<MaxSize, Interface>&& rhs);

            ~SmallBuffer();

            /**
            * Checks if the buffer holds any value or not
            * @returns true if it is empty, false otherwise
            **/
            bool is_empty() const;

            /**
            * Constructs a new concrete object in-place inside the buffer, destroying any
            * old ones held.
            * @tparam T the type of the concrete object to constructor
            * @tparam Args  the types of the arguments to the concrete objects constructor
            * @param args   the args to pass to the concrete objects constructor
            **/
            template<typename T, typename... Args> 
            void emplace(Args&&... args);

            SmallBuffer<MaxSize, Interface>& operator=(SmallBuffer<MaxSize, Interface>&& rhs);

            /**
            * Gets the held object
            * @returns  a pointer to the interface type of the held object
            * @throws   if the buffer is empty
            **/
            Interface* get() const;

            /**
            * Gets the held object
            * @returns  a pointer to the interface type of the held object
            * @throws   if the buffer is empty
            **/
            Interface* operator*() const;

            /**
            * Gets and dereferences the held object immediately
            * @returns  a pointer to the interface type of the held object
            * @throws   if the buffer is empty
            **/
            Interface* operator->() const;
        private:
            void destroy_held();

            class EmptyBuffer
            {
            };

            using buffer_t = std::array<uint8_t, MaxSize>;
            using VariantType = std::variant<EmptyBuffer, Interface*, buffer_t>;

            mutable VariantType m_buffer;
        };
    }
}

#include "core/memory/detail/small_buffer.hpp"

detail/small_buffer.hpp

#pragma once

#include "core/memory/small_buffer.h"

#include <stdexcept>
#include <type_traits>

namespace alchemist
{
    namespace core
    {
        template<size_t MaxSize, typename Interface>
        SmallBuffer<MaxSize, Interface>::SmallBuffer()
            : m_buffer(EmptyBuffer())
        {

        }
        template<size_t MaxSize, typename Interface>
        template<typename T>
        SmallBuffer<MaxSize, Interface>::SmallBuffer(T&& obj)
            : m_buffer(EmptyBuffer())
        {
            emplace<T>(std::forward<T>(obj));
        }
        template<size_t MaxSize, typename Interface>
        SmallBuffer<MaxSize, Interface>::SmallBuffer(SmallBuffer<MaxSize, Interface>&& rhs)
        {
            destroy_held();
            m_buffer = std::move(rhs.m_buffer);
            rhs.m_buffer = EmptyBuffer();
        }

        template<size_t MaxSize, typename Interface>
        SmallBuffer<MaxSize, Interface>::~SmallBuffer()
        {
            destroy_held();
        }

        template<size_t MaxSize, typename Interface>
        bool SmallBuffer<MaxSize, Interface>::is_empty() const
        {
            return std::holds_alternative<EmptyBuffer>(m_buffer);
        }

        template<size_t MaxSize, typename Interface>
        template<typename T, typename... Args>
        void SmallBuffer<MaxSize, Interface>::emplace(Args&&... args)
        {
            static_assert(std::is_base_of<Interface, T>::value, "T is not a type that can be put into this small buffer object!");

            if (sizeof(T) > MaxSize)
            {
                m_buffer.template emplace<Interface*>(new T(std::forward<Args>(args)...));
            }
            else
            {
                new (m_buffer.template emplace<buffer_t>().data()) T(std::forward<Args>(args)...);
            }
        }

        template<size_t MaxSize, typename Interface>
        SmallBuffer<MaxSize, Interface>& SmallBuffer<MaxSize, Interface>::operator=(SmallBuffer<MaxSize, Interface>&& rhs)
        {
            destroy_held();
            m_buffer = std::move(rhs.m_buffer);
            rhs.m_buffer = EmptyBuffer();
            return *this;
        }


        template<size_t MaxSize, typename Interface>
        Interface* SmallBuffer<MaxSize, Interface>::get() const
        {
            if (std::holds_alternative<Interface*>(m_buffer))
            {
                return std::get<Interface*>(m_buffer);
            }
            else if (std::holds_alternative<buffer_t>(m_buffer))
            {
                return reinterpret_cast<Interface*>(std::get<buffer_t>(m_buffer).data());
            }
            else
            {
                throw std::logic_error("Attempted to access an Empty small buffer.");
            }
        }

        template<size_t MaxSize, typename Interface>
        Interface* SmallBuffer<MaxSize, Interface>::operator*() const
        {
            return get();
        }

        template<size_t MaxSize, typename Interface>
        Interface* SmallBuffer<MaxSize, Interface>::operator->() const
        {
            return get();
        }

        template<size_t MaxSize, typename Interface>
        void SmallBuffer<MaxSize, Interface>::destroy_held()
        {
            if (std::holds_alternative<Interface*>(m_buffer))
            {
                delete std::get<Interface*>(m_buffer);
                m_buffer = EmptyBuffer();
            }
            else if (std::holds_alternative<buffer_t>(m_buffer))
            {
                reinterpret_cast<Interface*>(std::get<buffer_t>(m_buffer).data())->~Interface();
                m_buffer = EmptyBuffer();
            }
            else
            {
                // Holds nothing, do nothing
            }
        }

    }
}

I would be thankful for any feedback!

\$\endgroup\$
0

1 Answer 1

2
\$\begingroup\$

Small things:

  • std::size_t misspelt throughout.
  • std::uint8_t misspelt in one place. It's not obvious why we need an 8-bit type, and it looks like std::byte was intended there anyway. In any case, we'll need to include <cstddef> to declare it.

Please don't put multi-line comments in the middle of declarations like this:

            template<typename T>
            
            /**
            * Constructor
            * @param obj    moves the passed object into the buffer, holding it there
            **/
            explicit SmallBuffer(T&& obj);

There's a good case for using auto&& in that declaration instead of explicitly stating T.


The move-constructor of SmallBuffer isn't self-assignment-safe. I recommend implementing this and move-assignment using a swap operator.

#include <utility>

template<std::size_t MaxSize, typename Interface>
void alchemist::core::SmallBuffer<MaxSize, Interface>::swap(SmallBuffer& other)
{
    std::swap(m_buffer, other.m_buffer);
}
        template<size_t MaxSize, typename Interface>
        SmallBuffer<MaxSize, Interface>::SmallBuffer(SmallBuffer&& rhs)
            : SmallBuffer{}
        {
            swap(rhs);
        }

        template<size_t MaxSize, typename Interface>
        auto SmallBuffer<MaxSize, Interface>::operator=(SmallBuffer&& rhs)
            -> SmallBuffer&
        {
            swap(rhs);
            return *this;
        }

Declaring m_buffer mutable isn't necessary. Fight the laziness and implement both versions:

        template<size_t MaxSize, typename Interface>
        Interface* SmallBuffer<MaxSize, Interface>::get()
        {
            if (std::holds_alternative<Interface*>(m_buffer))
            {
                return std::get<Interface*>(m_buffer);
            }
            else if (std::holds_alternative<buffer_t>(m_buffer))
            {
                return reinterpret_cast<Interface*>(std::get<buffer_t>(m_buffer).data());
            }
            else
            {
                throw std::logic_error("Attempted to access an Empty small buffer.");
            }
        }
        template<size_t MaxSize, typename Interface>
        const Interface* SmallBuffer<MaxSize, Interface>::get() const
        {
            if (std::holds_alternative<Interface*>(m_buffer))
            {
                return std::get<const Interface*>(m_buffer);
            }
            else if (std::holds_alternative<buffer_t>(m_buffer))
            {
                return reinterpret_cast<const Interface*>(std::get<buffer_t>(m_buffer).data());
            }
            else
            {
                throw std::logic_error("Attempted to access an Empty small buffer.");
            }
        }

C++23 might let you write a single function again, by deducing this.


The base class IFunctionHolder needs a virtual destructor:

            class IFunctionHolder
            {
            public:
                virtual ~FunctionHolder();
                virtual RetT operator()(Args&&...) = 0;
            };

Its subclasses can let the compiler-generated default destructor be implement.


We should be providing deduction guides for UniqueFunction(). We certainly should be ensuring that the function argument is compatible. In C++20, we can constrain the constructor:

#include <concepts>

template<class F, class R, class... Args>
concept invocable_r = std::is_invocable_r_v<R, F, Args...>;
            template <invocable_r<RetT, Args...> FuncT>
            UniqueFunction(FuncT&& function);

If we implement move constructor and assignment, we don't get compiler-generated copy members, so no need to explicitly delete them.

We don't need to spell out the template arguments to UniqueFunction inside its own context. Omitting them makes for much more readable code:

            UniqueFunction(UniqueFunction&& rhs) = default;
            UniqueFunction& operator=(UniqueFunction&& rhs) = default;

I'm not sure whether it's intentional that we can appear to copy-construct a UniqueFunction like this:

    auto uf = alchemist::core::UniqueFunction<void()>([]{ return; });
    auto uf2 = uf;

It took a while to work out what's happening here: I think that uf2 is wrapping uf, rather than copying it.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.