std :: function如何实现?


99

根据我发现的消息来源,lambda表达式实际上是由编译器实现的,该编译器创建了一个带有重载函数调用运算符并将引用变量作为成员的类。这表明lambda表达式的大小是变化的,并且给定足够多的引用变量,大小可能会任意大

大小std::function应为固定,但必须能够包装任何类型的可调用对象,包括任何同类型的lambda。如何实施?如果std::function内部使用指向其目标的指针,那么在std::function复制或移动实例时会发生什么?是否涉及任何堆分配?


2
我前std::function一段时间研究了gcc / stdlib的实现。本质上,它是多态对象的句柄类。创建内部基类的派生类以保存分配在堆上的参数-然后,指向该对象的指针作为的子对象保存std::function。我相信它使用引用计数std::shared_ptr来处理复制和移动。
安德鲁·托马佐斯

4
请注意,实现可能使用魔术,即依赖于您不可用的编译器扩展。实际上,这对于某些类型特征是必需的。特别是蹦床是标准C ++中不可用的已知技术。
MSalters

Answers:


79

std::function一个实现与另一个实现可能有所不同,但是核心思想是它使用类型擦除。尽管有多种方法可以做到,但您可以想象一个简单的(不是最佳的)解决方案可能是这样的(为std::function<int (double)>简单起见,为特定情况进行了简化):

struct callable_base {
   virtual int operator()(double d) = 0;
   virtual ~callable_base() {}
};
template <typename F>
struct callable : callable_base {
   F functor;
   callable(F functor) : functor(functor) {}
   virtual int operator()(double d) { return functor(d); }
};
class function_int_double {
   std::unique_ptr<callable_base> c;
public:
   template <typename F>
   function(F f) {
      c.reset(new callable<F>(f));
   }
   int operator()(double d) { return c(d); }
// ...
};

用这种简单的方法 function对象仅unique_ptr将a 存储为基本类型。对于与一起使用的每个不同的函子,将function创建一个从基派生的新类型,并动态实例化该类型的对象。该std::function对象始终具有相同的大小,并将根据需要为堆中的不同函子分配空间。

在现实生活中,有多种优化措施可提供性能优势,但使答案复杂化。该类型可以使用小型对象优化,可以将动态分配替换为以函子为参数的自由函数指针,以避免一个间接级别……但是思想基本相同。


关于如何复制 std::function行为行为,快速测试表明内部可调用对象的副本已完成,而不是共享状态。

// g++4.8
int main() {
   int value = 5;
   typedef std::function<void()> fun;
   fun f1 = [=]() mutable { std::cout << value++ << '\n' };
   fun f2 = f1;
   f1();                    // prints 5
   fun f3 = f1;
   f2();                    // prints 5
   f3();                    // prints 6 (copy after first increment)
}

测试表明f2获取了可调用实体的副本,而不是引用。如果可调用实体由不同的std::function<>对象共享,则程序的输出将为5、6、7。


@Cole“ Cole9” Johnson猜测他是自己写的
aaronman

8
@Cole“ Cole9” Johnson:这是真实代码的过分简化,我只是在浏览器中键入了它,所以它可能有错别字和/或由于不同的原因而无法编译。答案中的代码仅用于说明如何/可以实现类型擦除,这显然不是生产质量代码。
大卫·罗德里格斯(DavidRodríguez)-dribeas

2
@MooingDuck:我确实相信lambda是可复制的(5.1.2 / 19),但这不是问题,而是std::function如果复制内部对象,的语义是否正确,我认为情况并非如此(想了拉姆达捕获的值,是可变的,里面存放的一个std::function,如果功能状态被复制的拷贝数std::function这是不希望内部的标准算法可能会导致不同的结果。
大卫·罗德里格斯- dribeas

1
@MiklósHomolya:我使用g ++ 4.8进行了测试,该实现确实复制了内部状态。如果可调用实体足够大以至于需要动态分配,则的副本std::function将触发分配。
大卫·罗德里格斯(DavidRodríguez)-dribeas 2013年

4
@DavidRodríguez-dribeas的共享状态将是不希望的,因为小对象优化将意味着您将在编译器和编译器版本确定的大小阈值下从共享状态进入非共享状态(因为小对象优化会阻止共享状态)。这似乎有问题。
Yakk-Adam Nevraumont

23

@DavidRodríguez的答案-dribeas很好地证明了类型擦除,但效果还不够好,因为类型擦除还包括如何复制类型(在该答案中,功能对象将不可复制构造)。这些行为function除了函子数据外,还存储在对象中。

在Ubuntu 14.04 gcc 4.8的STL实现中使用的技巧是编写一个泛型函数,使用每种可能的函子类型对其进行专用化,然后将其转换为通用函数指针类型。因此,类型信息被擦除

我已经整理了一个简化的版本。希望对你有帮助

#include <iostream>
#include <memory>

template <typename T>
class function;

template <typename R, typename... Args>
class function<R(Args...)>
{
    // function pointer types for the type-erasure behaviors
    // all these char* parameters are actually casted from some functor type
    typedef R (*invoke_fn_t)(char*, Args&&...);
    typedef void (*construct_fn_t)(char*, char*);
    typedef void (*destroy_fn_t)(char*);

    // type-aware generic functions for invoking
    // the specialization of these functions won't be capable with
    //   the above function pointer types, so we need some cast
    template <typename Functor>
    static R invoke_fn(Functor* fn, Args&&... args)
    {
        return (*fn)(std::forward<Args>(args)...);
    }

    template <typename Functor>
    static void construct_fn(Functor* construct_dst, Functor* construct_src)
    {
        // the functor type must be copy-constructible
        new (construct_dst) Functor(*construct_src);
    }

    template <typename Functor>
    static void destroy_fn(Functor* f)
    {
        f->~Functor();
    }

    // these pointers are storing behaviors
    invoke_fn_t invoke_f;
    construct_fn_t construct_f;
    destroy_fn_t destroy_f;

    // erase the type of any functor and store it into a char*
    // so the storage size should be obtained as well
    std::unique_ptr<char[]> data_ptr;
    size_t data_size;
public:
    function()
        : invoke_f(nullptr)
        , construct_f(nullptr)
        , destroy_f(nullptr)
        , data_ptr(nullptr)
        , data_size(0)
    {}

    // construct from any functor type
    template <typename Functor>
    function(Functor f)
        // specialize functions and erase their type info by casting
        : invoke_f(reinterpret_cast<invoke_fn_t>(invoke_fn<Functor>))
        , construct_f(reinterpret_cast<construct_fn_t>(construct_fn<Functor>))
        , destroy_f(reinterpret_cast<destroy_fn_t>(destroy_fn<Functor>))
        , data_ptr(new char[sizeof(Functor)])
        , data_size(sizeof(Functor))
    {
        // copy the functor to internal storage
        this->construct_f(this->data_ptr.get(), reinterpret_cast<char*>(&f));
    }

    // copy constructor
    function(function const& rhs)
        : invoke_f(rhs.invoke_f)
        , construct_f(rhs.construct_f)
        , destroy_f(rhs.destroy_f)
        , data_size(rhs.data_size)
    {
        if (this->invoke_f) {
            // when the source is not a null function, copy its internal functor
            this->data_ptr.reset(new char[this->data_size]);
            this->construct_f(this->data_ptr.get(), rhs.data_ptr.get());
        }
    }

    ~function()
    {
        if (data_ptr != nullptr) {
            this->destroy_f(this->data_ptr.get());
        }
    }

    // other constructors, from nullptr, from function pointers

    R operator()(Args&&... args)
    {
        return this->invoke_f(this->data_ptr.get(), std::forward<Args>(args)...);
    }
};

// examples
int main()
{
    int i = 0;
    auto fn = [i](std::string const& s) mutable
    {
        std::cout << ++i << ". " << s << std::endl;
    };
    fn("first");                                   // 1. first
    fn("second");                                  // 2. second

    // construct from lambda
    ::function<void(std::string const&)> f(fn);
    f("third");                                    // 3. third

    // copy from another function
    ::function<void(std::string const&)> g(f);
    f("forth - f");                                // 4. forth - f
    g("forth - g");                                // 4. forth - g

    // capture and copy non-trivial types like std::string
    std::string x("xxxx");
    ::function<void()> h([x]() { std::cout << x << std::endl; });
    h();

    ::function<void()> k(h);
    k();
    return 0;
}

STL版本中也有一些优化

  • construct_fdestroy_f混合成一个函数指针(以告诉做什么额外的参数),以节省一些字节
  • 原始指针用于将函子对象与函数指针一起存储在中union,因此,当function从函数指针构造对象时,它将直接存储在union而不是堆空间中

也许STL的实施不是最好的解决方案,因为我听说一些更快的实施。但是,我认为基本机制是相同的。


20

对于某些类型的参数(“如果f的目标是通过它传递的可调用对象reference_wrapper或函数指针”),std::function的构造函数不允许出现任何异常,因此使用动态内存是不可能的。在这种情况下,所有数据都必须直接存储在std::function对象。

在一般情况下(包括lambda情况),std::function允许使用动态内存(通过标准分配器或传递给构造函数的分配器)以实现合适的实现。该标准建议,如果可以避免的话,请不要使用动态内存,但是正如您正确地说的那样,如果函数对象(不是std::function对象,而是包裹在其中的对象)足够大,则无法阻止它,以来std::function尺寸固定

普通构造函数和复制构造函数均具有这种引发异常的权限,它们也明确地允许在复制期间进行动态内存分配。对于移动,没有理由需要动态内存。该标准似乎没有明确禁止它,并且如果移动可能调用包装对象的类型的move构造函数,则可能不会,但是您应该能够假定,如果实现和您的对象都是明智的,则移动不会导致任何分配。


-6

一个std::function过载operator()使其成为一个仿函数对象,拉姆达的工作方式相同。它基本上会创建一个带有成员变量的结构,该成员变量可以在operator()函数内部访问。因此要记住的基本概念是,lambda是对象(称为函子或函数对象)而不是函数。该标准说,如果可以避免,则不要使用动态内存。


1
任意大的lambda如何适应固定大小std::function?这是这里的关键问题。
米克洛什Homolya

2
@aaronman:我保证每个std::function对象的大小都是相同的,并且不是所包含的lambda的大小。
Mooing Duck 2013年

5
@aaronman的方式相同,即每个std::vector<T...> 对象都有一个(copiletime)固定大小,而与实际分配器实例/元素数无关。
sehe

3
@aaronman:好吧,也许您应该找到一个stackoverflow问题,该问题回答了std :: function如何以可以包含任意大小的lambda的方式实现的:P
Mooing Duck 2013年

1
@aaronman:在设置可调用实体时,在构造中进行赋值…… std::function<void ()> f;无需在那里分配,std::function<void ()> f = [&]() { /* captures tons of variables */ };很可能是在分配。std::function<void()> f = &free_function;可能也不会分配...
大卫·罗德里格斯(DavidRodríguez)-dribeas
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.