Search code examples
c++lambdastdvectorfunctorstd-function

Pushing back lambda to vector of functors produces infinite constructor calls


I've got an interesting puzzle that I can't seem to completely solve. The following code is a snipped for my own function implementation. When I try to push_back a lambda into a vector of this function type, it should be converted to the function type. This seems to happen, but strangely the converting constructor is called an infinite amount of times. I tried to boil down the problem to the minimum example which I show below: It works when I either comment out the allocation of the lambda in the memory resource, the destructor or the operator() return value... But I can't find the commen denominator. I bet it's something stupid but I just can't find it.

Demo

#include <concepts>
#include <cstdio>
#include <memory_resource>

template <typename Fn, typename R, typename... Args>
concept invocable_r = std::is_invocable_r<R, Fn, Args...>::value;

template <typename R, typename... Args>
class function;

template <typename R, typename... Args>
class function<R(Args...)>
{
public:
    using allocator_type = std::pmr::polymorphic_allocator<std::byte>;
    auto get_allocator() {
        return allocator_;
    }

    template <invocable_r<R, Args...> Cb>
    function(Cb&& fn, allocator_type allocator = {})
        :   allocator_{ allocator }
    {
        printf("Converting constructor invoked!\n");
        // Comment this out
        mem_ptr_ = static_cast<void*>(allocator_.new_object<Cb>(std::forward<Cb>(fn)));
    }

    // Or this
    ~function() {}

    auto operator()(Args... args) {
        // or this
        return R{};
    }

private:
    allocator_type allocator_;
    void* mem_ptr_ = nullptr;
};

int main()
{
    using foo_t = function<int()>;
    std::vector<foo_t> myvec;
    myvec.push_back([]() -> int { printf("Hello World1!\n"); return 10; });
}

Yields:

Converting constructor invoked!
Converting constructor invoked!
Converting constructor invoked!
Converting constructor invoked!
Converting constructor invoked!
Converting constructor invoked!
Converting constructor invoked!
Converting constructor invoked!
Converting constructor invoked!
Converting constructor invoked!
Converting constructor invoked!
... (inifinte)

Solution

  • The problem was that an invisible templated method

    function(Cb&& fn, allocator_type allocator = {})
    

    was instantiated as requested by vector::push_back() with the following signature:

      template<>
      inline function<function<int ()> >(function<int ()> && fn, allocator_type allocator)
    

    As can be seen in cppinsights. But why is instantiated? Well, vector::push_back has an overload for const T& and T&& whereas T is the actual value type of the vector. So in order to call those functions, an object of type T must first be instantiated before the call which is done using the converting constructor instantiated with the lambda parameter like this:

    myvec.push_back(function<int ()>(__lambda_134_21{}, allocator_type{}));
    

    The resulting r-value reference of type function<int()> is then supposed to be plugged into the push_back method and forwarded to allocator::construct() within the internals of std::vector eventually, but now arises an interesting situation: Afaik what happens is that because the default move constructor of function is implicitely deleted as ~function() is defined, it can't actually pass function as an rvalue-reference and has to use push_back's const T& overload.

    Here's something I still don't quite understand though: An rvalue-reference could actually be bound to const T&, but somehow the compiler decides to "create a new function instance from scratch" using the converting constructor with the temporary function()-object as template parameter, hence we get the above mentioned template instantiation. This however produces the same scenario again: We still have an rvalue reference, and the compiler thinks it better converts it again, and again, and again...

    So the fact that function<> is itself invokable actually makes it viable for overloads with its own template which is something I should prevent against. This is also why excluding operator()() will work, because now function<> is not invokable anymore. As for why the inifinite calls don't happen when I exclude the allocator call within the converting constructor is still a mistery to me. Could be related to optimizations, but honestly idk, maybe someone can shed some light on this.