내가 찾은 소스에 따르면 람다 식은 기본적으로 컴파일러에서 오버로드 된 함수 호출 연산자와 참조 된 변수를 멤버로 사용하는 클래스를 생성하여 구현됩니다. 이는 람다 식의 크기가 다양하고 참조 변수가 충분히 주어지면 크기가 임의로 클 수 있음을 나타 냅니다.
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
객체는 항상 같은 크기이며, 힙의 다른 펑에 필요한 공간을 할당합니다.
실제 생활에서는 성능상의 이점을 제공하지만 답을 복잡하게 만드는 다양한 최적화가 있습니다. 유형은 작은 객체 최적화를 사용할 수 있으며, 동적 디스패치는 한 수준의 간접 지정을 피하기 위해 functor를 인수로 사용하는 자유 함수 포인터로 대체 될 수 있지만 기본적으로 아이디어는 동일합니다.
복사본의 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이됩니다.
답변
@David Rodrí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_f
및destroy_f
일부 바이트를 저장할 수로 (무엇을 알려주는 추가 매개 변수를 사용하여) 하나의 함수 포인터로 혼합- 원시 포인터는 함수 포인터와 함께 functor 객체를 저장하는 데 사용
union
되므로function
객체가 함수 포인터에서 생성 될 때union
힙 공간 이 아닌 직접 저장됩니다.
더 빠른 구현 에 대해 들었던 것처럼 STL 구현이 최상의 솔루션이 아닐 수도 있습니다 . 그러나 기본 메커니즘은 동일하다고 생각합니다.
답변
특정 유형의 인수 ( “f의 대상이 reference_wrapper
또는 함수 포인터 를 통해 전달 된 호출 가능한 객체 인 경우”)의 경우 std::function
의 생성자는 예외를 허용하지 않으므로 동적 메모리를 사용하는 것은 문제가되지 않습니다. 이 경우 모든 데이터는 std::function
객체 내부에 직접 저장되어야 합니다.
일반적인 경우 (람다 경우 포함), 동적 메모리 사용 (표준 할당 자 또는 std::function
생성자에 전달 된 할당자를 통해 )은 구현이 적합하다고 판단 될 때 허용됩니다. 표준은 피할 수있는 경우 동적 메모리를 사용하지 않는 구현을 권장하지만 올바르게 말했듯이 함수 개체 ( std::function
개체가 아니라 내부에 래핑되는 개체)가 충분히 크면이를 방지 할 방법이 없습니다. std::function
크기가 고정되어 있기 때문 입니다.
예외를 던질 수있는이 권한은 일반 생성자와 복사 생성자 모두에게 부여되며 복사 중에도 동적 메모리 할당을 상당히 명시 적으로 허용합니다. 움직임의 경우 동적 메모리가 필요한 이유가 없습니다. 표준은이를 명시 적으로 금지하는 것 같지 않으며, 이동이 래핑 된 객체 유형의 이동 생성자를 호출 할 수 있다면 불가능할 수 있지만 구현과 객체가 모두 합리적이라면 이동이 원인이되지 않는다고 가정 할 수 있어야합니다. 모든 할당.
답변
std::function
오버로드는 operator()
그것을 펑 개체, 람다의 작품 같은 방법을 만드는. 기본적으로 operator()
함수 내에서 액세스 할 수있는 멤버 변수로 구조체를 만듭니다 . 따라서 기억해야 할 기본 개념은 람다는 함수가 아니라 객체 (펑터 또는 함수 객체라고 함)라는 것입니다. 표준은 피할 수 있다면 동적 메모리를 사용하지 말라고 말합니다.