Home > Net >  Why doesn't copy elision work in my static functional implementation?
Why doesn't copy elision work in my static functional implementation?

Time:07-29

I am trying to implement a "static" sized function, that uses preallocated store, unlike std::function that uses dynamic heap allocations.


#include <utility>
#include <cstddef>

#include <type_traits>

template <typename T, size_t StackSize = 64>
class static_function;

// TODO: move and swap
//  - can move smaller instance to larger instance
//  - only instances of the same size are swappable
// TODO: condiotnal dynamic storage?
template <typename Ret, typename ... Args, size_t StackSize>
class static_function<Ret(Args...), StackSize>
{
public:
    constexpr static size_t static_size = StackSize;
    using return_type = Ret;

    template <typename Callable>
    constexpr explicit static_function(Callable &&callable)
        : pVTable_(std::addressof(v_table::template get<Callable>()))
    {
        static_assert(sizeof(std::decay_t<Callable>) <= static_size, "Callable type is too big!");

        new (&data_) std::decay_t<Callable>(std::forward<Callable>(callable));
    }

    constexpr return_type operator()(Args ... args) const
    {
        return (*pVTable_)(data_, std::move(args)...);
    }

    ~static_function() noexcept
    {
        pVTable_->destroy(data_);
    }

private:
    using stack_data = std::aligned_storage_t<static_size>;

    struct v_table
    {
        virtual return_type operator()(const stack_data&, Args &&...) const = 0;
        virtual void destroy(const stack_data&) const = 0;

        template <typename Callable>
        static const v_table& get()
        {
            struct : v_table {
                return_type operator()(const stack_data &data, Args &&... args) const override 
                {
                    return (*reinterpret_cast<const Callable*>(&data))(std::move(args)...);
                }

                void destroy(const stack_data &data) const override
                {
                    reinterpret_cast<const Callable*>(&data)->~Callable();
                }

            } constexpr static vTable_{};

            return vTable_;
        }  
    };    

private:
    stack_data data_;
    const v_table *pVTable_;
};

However, it does not work as expected, since copies of the callable (copy elision does not kick in like in just lambda).
Here is what is expected vs actiual behavior with -O3:

#include <iostream>
#include <string>

int main()
{
    struct prisoner 
    {
        std::string name;

        ~prisoner()
        {
            if (!name.empty())
                std::cout << name << " has been executed\n";
        }
    };

    std::cout << "Expected:\n";
    {
        const auto &func = [captured = prisoner{"Pvt Ryan"}](int a, int b) -> std::string {
            std::cout << captured.name << " has been captured!\n";
            return std::string()   "oceanic "   std::to_string(a   b);
        };
        std::cout << func(4, 811) << '\n';
    }
    std::cout << "THE END\n\n";

    std::cout << "Actual:\n";
    {
        const auto &func = static_function<std::string(int, int)>([captured = prisoner{"Pvt Ryan"}](int a, int b) -> std::string {
            std::cout << captured.name << " has been captured!\n";
            return std::string()   "oceanic "   std::to_string(a   b);
        });

        std::cout << func(4, 811) << '\n';
    }
    std::cout << "THE END!\n";

    return 0;
}

Output:

Expected:
Pvt Ryan has been captured!
oceanic 815
Pvt Ryan has been executed
THE END

Actual:
Pvt Ryan has been executed
Pvt Ryan has been captured!
oceanic 815
Pvt Ryan has been executed
THE END!

https://godbolt.org/z/zc3d1Eave

What did I do wrong within the implementation?

CodePudding user response:

It is not possible to elide the copy/move. The capture is constructed when the lambda expression is evaluated in the caller resulting in a temporary object. That lambda is then passed to the constructor and the constructor explicitly constructs a new object of that type in the storage by copy/move from the passed lambda. You can't identify the object created by placement-new with the temporary object passed to the constructor.

The only way to resolve such an issue is by not constructing the lambda with the capture that should not be copied/moved in the caller at all, but to instead pass a generator for the lambda which is then evaluated by the constructor when constructing the new object with placement-new. Something like:

template <typename CallableGenerator, typename... Args>
constexpr explicit static_function(CallableGenerator&& generator, Args&&... args)
    : pVTable_(std::addressof(v_table::template get<std::invoke_result_t<CallableGenerator, Args...>>()))
{
    static_assert(sizeof(std::decay_t<std::invoke_result_t<CallableGenerator, Args...>>) <= static_size, "Callable type is too big!");

    new (&data_) auto(std::invoke(std::forward<CallableGenerator>(generator), std::forward<Args>(args)...);
}

//...

const auto &func = static_function<std::string(int, int)>([](auto str){
    return [captured = prisoner{str}](int a, int b) -> std::string {
        std::cout << captured.name << " has been captured!\n";
        return std::string()   "oceanic "   std::to_string(a   b);
    });
}, "Pvt Ryan");

// or

const auto &func = static_function<std::string(int, int)>([str="Pvt Ryan"]{
    return [captured = prisoner{str}](int a, int b) -> std::string {
        std::cout << captured.name << " has been captured!\n";
        return std::string()   "oceanic "   std::to_string(a   b);
    });
});

This requires C 17 or later to guarantee that the copy/move is elided. Before C 17 the elision cannot be guaranteed in this way with a lambda. Instead a manually-defined function object must be used so that its constructor can be passed the arguments like I am doing here with the generator. That would be the equivalent of emplace-type functions of standard library types.

  • Related