std :: function은 어떻게 구현됩니까?
내가 찾은 소스에 따르면 람다 식은 기본적으로 컴파일러에서 오버로드 된 함수 호출 연산자와 참조 된 변수를 멤버로 사용하는 클래스를 생성하여 구현됩니다. 이는 람다 식의 크기가 다양하고 참조 변수가 충분히 주어지면 크기가 임의로 클 수 있음을 나타 냅니다.
An std::function
은 고정 된 크기를 가져야 하지만 같은 종류의 람다를 포함하여 모든 종류의 콜 러블을 래핑 할 수 있어야합니다. 어떻게 구현됩니까? std::function
내부적으로 대상에 대한 포인터를 사용하는 경우 std::function
인스턴스를 복사하거나 이동할 때 어떻게됩니까 ? 관련된 힙 할당이 있습니까?
의 구현은 구현 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이됩니다.
특정 유형의 인수 ( "f의 대상이 reference_wrapper
또는 함수 포인터 를 통해 전달 된 호출 가능한 객체 인 경우")의 경우 std::function
의 생성자는 예외를 허용하지 않으므로 동적 메모리를 사용하는 것은 문제가되지 않습니다. 이 경우 모든 데이터는 std::function
객체 내부에 직접 저장되어야 합니다.
일반적인 경우 (람다 경우 포함), 동적 메모리 사용 (표준 할당 자 또는 std::function
생성자에 전달 된 할당자를 통해 )은 구현이 적합하다고 판단 될 때 허용됩니다. 표준은 피할 수있는 경우 동적 메모리를 사용하지 않는 구현을 권장하지만 올바르게 말했듯이 함수 객체 ( std::function
객체가 아니라 그 안에 래핑되는 객체)가 충분히 크면이를 방지 할 방법이 없습니다. std::function
크기가 고정되어 있기 때문 입니다.
This permission to throw exceptions is granted to both the normal constructor and the copy constructor, which fairly explicitly allows dynamic memory allocations during copying too. For moves, there is no reason why dynamic memory would be necessary. The standard does not seem to explicitly prohibit it, and probably cannot if the move might call the move constructor of the wrapped object's type, but you should be able to assume that if both the implementation and your objects are sensible, moving won't cause any allocations.
The answer from @David Rodríguez - dribeas is good for demonstrating the type-erasure but not good enough since type-erasure also includes how types are copied (in that answer the function object won't be copy-constructible). Those behaviors are also stored in the function
object, besides the functor data.
The trick, used in the STL implementation from Ubuntu 14.04 gcc 4.8, is to write one generic function, specialize it with each possible functor type, and cast them to a universal function pointer type. Therefore the type information is erased.
I've cobbled up a simplified version of that. Hope it'll help
#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;
}
There are also some optimizations in the STL version
- the
construct_f
anddestroy_f
are mixed into one function pointer (with an additional parameter that tells what to do) as to save some bytes - raw pointers are used to store the functor object, along with a function pointer in a
union
, so that when afunction
object is constructed from an function pointer, it will be stored directly in theunion
rather than heap space
Maybe the STL implementation is not the best solution as I've heard about some faster implementation. However I believe the underlying mechanism is the same.
An std::function
overloads the operator()
making it a functor object, lambda's work the same way. It basically creates a struct with member variables that can be accessed inside the operator()
function. So the basic concept to keep in mind is that a lambda is an object (called a functor or function object) not a function. The standard says not to use dynamic memory if it can be avoided.
참고URL : https://stackoverflow.com/questions/18453145/how-is-stdfunction-implemented
'IT박스' 카테고리의 다른 글
파이썬에서 좋은 __hash__ 함수를 구현하는 방법 (0) | 2020.09.04 |
---|---|
Math.floor가 왜 double을 반환합니까? (0) | 2020.09.04 |
libc ++의 벡터는 왜 (0) | 2020.09.04 |
PHP를 사용하여 크론 작업을 만드는 방법은 무엇입니까? (0) | 2020.09.04 |
GitHub에서 프로젝트 다운로드 수를 확인하는 방법은 무엇입니까? (0) | 2020.09.04 |