program tip

람다의 크기가 1 바이트 인 이유는 무엇입니까?

radiobox 2020. 9. 7. 08:00
반응형

람다의 크기가 1 바이트 인 이유는 무엇입니까?


C ++에서 일부 람다의 메모리로 작업하고 있지만 크기에 약간 의아해합니다.

내 테스트 코드는 다음과 같습니다.

#include <iostream>
#include <string>

int main()
{
  auto f = [](){ return 17; };
  std::cout << f() << std::endl;
  std::cout << &f << std::endl;
  std::cout << sizeof(f) << std::endl;
}

여기에서 실행할 수 있습니다 : http://fiddle.jyt.io/github/b13f682d1237eb69ebdc60728bb52598

출력은 다음과 같습니다.

17
0x7d90ba8f626f
1

이것은 내 람다의 크기가 1임을 나타냅니다.

  • 이것이 어떻게 가능한지?

  • 람다는 최소한 그 구현에 대한 포인터 여야하지 않습니까?


문제의 람다는 실제로 상태없습니다 .

검사 :

struct lambda {
  auto operator()() const { return 17; }
};

그리고 만약 우리가 가지고 있다면 lambda f;그것은 빈 클래스입니다. 위의 lambda기능 람다와 기능적으로 유사 할 뿐만 아니라 (기본적으로) 람다가 구현되는 방식입니다! (또한 함수 포인터 연산자에 대한 암시 적 캐스트가 필요하며 이름 lambda은 일부 컴파일러 생성 의사 GUI로 대체됩니다.)

C ++에서 개체는 포인터가 아닙니다. 그것들은 실제적인 것입니다. 데이터를 저장하는 데 필요한 공간 만 사용합니다. 개체에 대한 포인터는 개체보다 클 수 있습니다.

람다를 함수에 대한 포인터로 생각할 수 있지만 그렇지 않습니다. auto f = [](){ return 17; };다른 함수 나 람다에 다시 할당 할 수 없습니다 !

 auto f = [](){ return 17; };
 f = [](){ return -42; };

위의 내용은 불법 입니다. 호출 함수 f를 저장할 공간이 없습니다. 해당 정보는 ! 값이 아닌 형식 으로 저장됩니다 .ff

이 경우 :

int(*f)() = [](){ return 17; };

아니면 이거:

std::function<int()> f = [](){ return 17; };

you are no longer storing the lambda directly. In both of these cases, f = [](){ return -42; } is legal -- so in these cases, we are storing which function we are invoking in the value of f. And sizeof(f) is no longer 1, but rather sizeof(int(*)()) or larger (basically, be pointer sized or larger, as you expect. std::function has a min size implied by the standard (they have to be able to store "inside themselves" callables up to a certain size) which is at least as large as a function pointer in practice).

In the int(*f)() case, you are storing a function pointer to a function that behaves as-if you called that lambda. This only works for stateless lambdas (ones with an empty [] capture list).

In the std::function<int()> f case, you are creating a type-erasure class std::function<int()> instance that (in this case) uses placement new to store a copy of the size-1 lambda in an internal buffer (and, if a larger lambda was passed in (with more state), would use heap allocation).

As a guess, something like these is probably what you think is going on. That a lambda is an object whose type is described by its signature. In C++, it was decided to make lambdas zero cost abstractions over the manual function object implementation. This lets you pass a lambda into a std algorithm (or similar) and have its contents be fully visible to the compiler when it instantiates the algorithm template. If a lambda had a type like std::function<void(int)>, its contents would not be fully visible, and a hand-crafted function object might be faster.

The goal of C++ standardization is high level programming with zero overhead over hand-crafted C code.

Now that you understand that your f is in fact stateless, there should be another question in your head: the lambda has no state. Why does it not size have 0?


There is the short answer.

All objects in C++ must have a minimium size of 1 under the standard, and two objects of the same type cannot have the same address. These are connected, because an array of type T will have the elements placed sizeof(T) apart.

Now, as it has no state, sometimes it can take up no space. This cannot happen when it is "alone", but in some contexts it can happen. std::tuple and similar library code exploits this fact. Here is how it works:

As a lambda is equivalent to a class with operator() overloaded, stateless lambdas (with a [] capture list) are all empty classes. They have sizeof of 1. In fact, if you inherit from them (which is allowed!), they will take up no space so long as it doesn't cause a same-type address collision. (This is known as the empty base optimization).

template<class T>
struct toy:T {
  toy(toy const&)=default;
  toy(toy &&)=default;
  toy(T const&t):T(t) {}
  toy(T &&t):T(std::move(t)) {}
  int state = 0;
};

template<class Lambda>
toy<Lambda> make_toy( Lambda const& l ) { return {l}; }

the sizeof(make_toy( []{std::cout << "hello world!\n"; } )) is sizeof(int) (well, the above is illegal because you cannot create a lambda in a non-evaluated context: you have to create a named auto toy = make_toy(blah); then do sizeof(blah), but that is just noise). sizeof([]{std::cout << "hello world!\n"; }) is still 1 (similar qualifications).

If we create another toy type:

template<class T>
struct toy2:T {
  toy2(toy2 const&)=default;
  toy2(T const&t):T(t), t2(t) {}
  T t2;
};
template<class Lambda>
toy2<Lambda> make_toy2( Lambda const& l ) { return {l}; }

this has two copies of the lambda. As they cannot share the same address, sizeof(toy2(some_lambda)) is 2!


A lambda is not a function pointer.

A lambda is an instance of a class. Your code is approximately equivalent to:

class f_lambda {
public:

  auto operator() { return 17; }
};

f_lambda f;
std::cout << f() << std::endl;
std::cout << &f << std::endl;
std::cout << sizeof(f) << std::endl;

The internal class that represents a lambda has no class members, hence its sizeof() is 1 (it cannot be 0, for reasons adequately stated elsewhere).

If your lambda were to capture some variables, they'll be equivalent to class members, and your sizeof() will indicate accordingly.


Your compiler more or less translates the lambda to the following struct type:

struct _SomeInternalName {
    int operator()() { return 17; }
};

int main()
{
     _SomeInternalName f;
     std::cout << f() << std::endl;
}

Since that struct has no non-static members, it has the same size as an empty struct, which is 1.

That changes as soon as you add a non-empty capture list to your lambda:

int i = 42;
auto f = [i]() { return i; };

Which will translate to

struct _SomeInternalName {
    int i;
    _SomeInternalName(int outer_i) : i(outer_i) {}
    int operator()() { return i; }
};


int main()
{
     int i = 42;
     _SomeInternalName f(i);
     std::cout << f() << std::endl;
}

Since the generated struct now needs to store a non-static int member for the capture, its size will grow to sizeof(int). The size will keep growing as you capture more stuff.

(Please take the struct analogy with a grain of salt. While it's a nice way to reason about how lambdas work internally, this is not a literal translation of what the compiler will do)


Shouldn't the lambda be, at mimumum, a pointer to its implementation?

Not necessarily. According to the standard, the size of the unique, unnamed class is implementation-defined. Excerpt from [expr.prim.lambda], C++14 (emphasis mine):

The type of the lambda-expression (which is also the type of the closure object) is a unique, unnamed nonunion class type — called the closure type — whose properties are described below.

[ ... ]

An implementation may define the closure type differently from what is described below provided this does not alter the observable behavior of the program other than by changing:

— the size and/or alignment of the closure type,

— whether the closure type is trivially copyable (Clause 9),

— whether the closure type is a standard-layout class (Clause 9), or

— whether the closure type is a POD class (Clause 9)

In your case -- for the compiler you use -- you get a size of 1, which doesn't mean it's fixed. It can vary between different compiler implementations.


From http://en.cppreference.com/w/cpp/language/lambda:

The lambda expression constructs an unnamed prvalue temporary object of unique unnamed non-union non-aggregate class type, known as closure type, which is declared (for the purposes of ADL) in the smallest block scope, class scope, or namespace scope that contains the lambda expression.

If the lambda-expression captures anything by copy (either implicitly with capture clause [=] or explicitly with a capture that does not include the character &, e.g. [a, b, c]), the closure type includes unnamed non-static data members, declared in unspecified order, that hold copies of all entities that were so captured.

For the entities that are captured by reference (with the default capture [&] or when using the character &, e.g. [&a, &b, &c]), it is unspecified if additional data members are declared in the closure type

From http://en.cppreference.com/w/cpp/language/sizeof

When applied to an empty class type, always returns 1.

참고URL : https://stackoverflow.com/questions/37481767/why-does-a-lambda-have-a-size-of-1-byte

반응형