函数模板_构造函数栈溢出

前言

最近写一个任务队列,可以支持存入返回值为void的任意函数对象。需要定义一个Task模板,来存储函数对象以及参数。大致的实现如下:

class Task
{
public:
    template <typename Func, typename... Args>
    Task(Func&& f, Args &&...args)
        : func_(std::bind(std::forward<Func>(f), std::forward<Args>(args)...)) {}

    void operator()()
    {
        func_();
    }
private:
    std::function<void()> func_;
};

其中构造函数是一个函数模板,可以在编译的时候,根据传入的函数对象和参数,绑定生成std::function,存储在func_中。

支持形如

auto f1 = [](int i, int j)
{
    std::cout << i << j;
};
auto f2 = [](int i, double j)
{
    std::cout << i << j;
};
Task t(f1, 5, 6);
Task t2(f2, 1, 2.3);

复制栈溢出

但下面这个普通的“拷贝”,linux编译正常,用msvc编译器的话会造成栈溢出。
auto t3 = t;
用vs看调用堆栈,发现一直在执行Task::<Task&>(Task & f)--->bind---- > binder等等,一直在执行构造函数。

我尝试自定义拷贝构造,并在其中输出。但发现拷贝函数根本没有执行,而是在反复执行函数模板。

Task(const Task & other) :func_(other.func_)
{
    std::cout << "copy ctor";
}

为什么没有调用拷贝构造

仔细研究发现调用堆栈,发现调用的是Task::<Task&>(Task& f),这并不是拷贝构造,这是函数模板自动生成的。

在https://cppinsights.io/这个网址,提供了编译结果,可以看模板生成了那些函数。

#include <functional>
#include<iostream>

class Task
{

public:
    template<typename Func, typename ... Args>
    inline Task(Func&& f, Args &&... args)
        : func_{ std::bind(std::forward<Func>(f), std::forward<Args>(args)...) }
    {
    }


    /* First instantiated from: insights.cpp:37 */
#ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline Task<__lambda_29_13&, int, int>(__lambda_29_13& f, int&& __args1, int&& __args2)
        : func_{ std::function<void()>(std::bind(std::forward<__lambda_29_13&>(f), std::forward<int>(__args1), std::forward<int>(__args2))) }
    {
    }
#endif



    /* First instantiated from: insights.cpp:38 */
#ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline Task<__lambda_33_13&, int, double>(__lambda_33_13& f, int&& __args1, double&& __args2)
        : func_{ std::function<void()>(std::bind(std::forward<__lambda_33_13&>(f), std::forward<int>(__args1), std::forward<double>(__args2))) }
    {
    }
#endif



    /* First instantiated from: insights.cpp:39 */
#ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline Task<Task&>(Task& f)
        : func_{ std::function<void()>(std::bind(std::forward<Task&>(f))) }
    {
    }
#endif



#ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline Task<const Task&>(const Task& f);
#endif



    /* First instantiated from: functional:558 */
#ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline Task<Task>(Task&& f)
        : func_{ std::function<void()>(std::bind(std::forward<Task>(f))) }
    {
    }
#endif



#ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline Task<const std::_Bind<Task()>&>(const std::_Bind<Task()>& f);
#endif



#ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline Task<std::_Bind<Task()> >(std::_Bind<Task()>&& f);
#endif


    inline void operator()()
    {
        this->func_.operator()();
    }

    inline Task(const Task& other)
        : func_{ std::function<void()>(other.func_) }
    {
        std::operator<<(std::cout, "copy ctor");
    }


private:
    std::function<void()> func_;
public:
    // inline ~Task() noexcept = default;
};




int main()
{

    class __lambda_29_13
    {
    public:
        inline /*constexpr */ void operator()(int i, int j) const
        {
            std::cout.operator<<(i).operator<<(j);
        }

        using retType_29_13 = void (*)(int, int);
        inline constexpr operator retType_29_13 () const noexcept
        {
            return __invoke;
        };

    private:
        static inline /*constexpr */ void __invoke(int i, int j)
        {
            __lambda_29_13{}.operator()(i, j);
        }

    public:
        // inline /*constexpr */ __lambda_29_13 & operator=(const __lambda_29_13 &) /* noexcept */ = delete;
        // inline /*constexpr */ __lambda_29_13(const __lambda_29_13 &) noexcept = default;
        // inline /*constexpr */ __lambda_29_13(__lambda_29_13 &&) noexcept = default;

    };

    __lambda_29_13 f1 = __lambda_29_13{};

    class __lambda_33_13
    {
    public:
        inline /*constexpr */ void operator()(int i, double j) const
        {
            std::cout.operator<<(i).operator<<(j);
        }

        using retType_33_13 = void (*)(int, double);
        inline constexpr operator retType_33_13 () const noexcept
        {
            return __invoke;
        };

    private:
        static inline /*constexpr */ void __invoke(int i, double j)
        {
            __lambda_33_13{}.operator()(i, j);
        }

    public:
        // inline /*constexpr */ __lambda_33_13 & operator=(const __lambda_33_13 &) /* noexcept */ = delete;
        // inline /*constexpr */ __lambda_33_13(const __lambda_33_13 &) noexcept = default;
        // inline /*constexpr */ __lambda_33_13(__lambda_33_13 &&) noexcept = default;

    };

    __lambda_33_13 f2 = __lambda_33_13{};
    Task t = Task(f1, 5, 6);
    Task t2 = Task(f2, 1, 2.2999999999999998);
    Task t3 = Task(t);
    return 0;
}

可以看到最后t3是这么生成的。
Task t3 = Task(t);
而Task类中有两个形如这样的函数;

//这个是拷贝构造函数

inline Task(const Task& other)
    : func_{ std::function<void()>(other.func_) }
{
    std::operator<<(std::cout, "copy ctor");
}

//这个是函数模板生成的一个构造函数,参数为Task &f。

#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline Task<Task&>(Task& f)
    : func_{ std::function<void()>(std::bind(std::forward<Task&>(f))) }
{
}
#endif

这下明白为什么没有调用拷贝构造了,原来Task t3 = Task(t)中,等号右边的Task(t)并不是拷贝构造函数。因为t是非常量左值,所以编译器优先匹配模板函数的参数(Task & )。

为什么会栈溢出

接下来是为什么这个模板函数会递归构造,直至栈溢出。

观察这个模板函数,发现其形参中有std::bind。而这个函数会复制拷贝传入参数(这里就是Task)。而复制Task并不会调用构造函数,而是调用这个函数模板,因此,一直递归调用直至栈溢出。

解决

既然非常量左值匹配不上拷贝构造,那就把返回值转换成常量左值, 改成下面这种形式。就没问题了

auto t3 = static_cast<Task&>(t);

posted @ 2023-09-25 22:01  chendaxian  阅读(34)  评论(0编辑  收藏  举报