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


89

나는 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임을 나타냅니다.

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

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


17
그것의 함수 객체로 구현 됨 (a struct와 함께 operator())
george_ptr

14
그리고 빈 구조체는 크기가 0이 될 수 없으므로 결과가 1입니다. 무언가를 캡처하고 크기가 어떻게되는지 확인하십시오.
Mohamad Elghawi

2
왜 람다는 포인터가되어야합니까 ??? 호출 연산자가있는 개체입니다.
Kerrek SB

7
C ++의 Lambda는 컴파일 타임에 존재하며 호출은 컴파일 또는 링크 타임에 연결 (또는 인라인)됩니다. 따라서 개체 자체에 런타임 포인터 가 필요하지 않습니다 . @KerrekSB 람다를 구현하는 대부분의 언어가 C ++보다 동적이기 때문에 람다에 함수 포인터가 포함될 것이라고 예상하는 것은 부 자연스러운 추측이 아닙니다.
Kyle Strand

2
@KerrekSB "중요한 것"-어떤 의미에서? 이유 (오히려 함수 포인터를 포함하는 것보다) 폐쇄 개체가 비어있을 수는 있기 때문에 호출되는 함수가 컴파일 / 링크시에 알려져있다. 이것은 OP가 오해 한 것 같습니다. 나는 당신의 의견이 어떻게 일을 명확히하는지 보지 못합니다.
Kyle Strand

답변:


107

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

검사 :

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; };

더 이상 람다를 직접 저장하지 않습니다. 이 두 경우 모두 f = [](){ return -42; }합법적입니다. 따라서이 경우 우리는 의 값에서 호출 하는 함수를 저장 f합니다. 그리고 sizeof(f)더 이상 1은 아니지만 오히려 sizeof(int(*)())더 큽니다 (기본적으로 예상대로 포인터 크기 이상이어야합니다. std::function표준에서 암시하는 최소 크기 ( "내부"콜 러블을 특정 크기까지 저장할 수 있어야 함)). 실제로는 함수 포인터만큼 큽니다).

int(*f)()경우 해당 람다를 호출 한 것처럼 동작하는 함수에 대한 함수 포인터를 저장합니다. 이것은 상태 비 저장 람다 (빈 []캡처 목록 이있는 람다)에서만 작동합니다 .

std::function<int()> f경우에는, 형태 소거 클래스 생성된다 std::function<int()>(이 경우)의 내부 버퍼 사이즈 -1 람다의 복사본을 저장하는 새로운 (및 배치를 사용하는 예를보다 큰 람다보다 상태로 (전달되었는지 ), 힙 할당을 사용합니다).

추측으로, 이와 같은 것이 아마도 당신이 생각하는 일입니다. 람다는 서명으로 유형이 설명되는 객체입니다. C ++에서는 수동 함수 개체 구현 에 대해 람다를 비용없이 추상화 하기로 결정했습니다 . 이를 통해 람다를 std알고리즘 (또는 유사)에 전달하고 알고리즘 템플릿을 인스턴스화 할 때 해당 내용을 컴파일러에 완전히 표시 할 수 있습니다. 람다에와 같은 유형이 있으면 std::function<void(int)>해당 내용이 완전히 표시되지 않으며 손으로 만든 함수 객체가 더 빠를 수 있습니다.

C ++ 표준화의 목표는 수작업으로 만든 C 코드에 대한 오버 헤드가없는 고수준 프로그래밍입니다.

이제 f실제로 무국적자 임을 이해 했으므로 머리 속에 또 다른 질문이 있어야합니다. 람다는 상태가 없습니다. 왜 크기가 0없습니까?


짧은 대답이 있습니다.

C ++의 모든 객체는 표준에 따라 최소 크기가 1이어야하며 동일한 유형의 두 객체는 ​​동일한 주소를 가질 수 없습니다. 유형의 배열은 T요소가 sizeof(T)분리 되어 있기 때문에 연결 됩니다 .

이제 상태가 없기 때문에 때때로 공간을 차지하지 않을 수 있습니다. 이것은 "혼자"일 때는 발생할 수 없지만 일부 상황에서는 발생할 수 있습니다. std::tuple유사한 라이브러리 코드가이 사실을 악용합니다. 작동 방식은 다음과 같습니다.

람다는 operator()오버로드 된 클래스와 동일하므로 상태 비 저장 람다 ( []캡처 목록 포함)는 모두 빈 클래스입니다. 그들은이 sizeof1. 사실, 당신이 그들로부터 상속한다면 (허용됩니다!), 같은 유형의 주소 충돌을 일으키지 않는 한 그들은 공간을 차지 하지 않을 것 입니다. (이를 빈 기본 최적화라고합니다).

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}; }

sizeof(make_toy( []{std::cout << "hello world!\n"; } ))것입니다 sizeof(int)(: 당신이이 이름을 만들 필요가 아니라, 위가 아닌 평가 맥락에서 람다를 만들 수 없기 때문에 불법 auto toy = make_toy(blah);을 수행 한 후 sizeof(blah), 그러나 그것은 단지 잡음). sizeof([]{std::cout << "hello world!\n"; })여전히 1(유사한 자격)입니다.

다른 장난감 유형을 만드는 경우 :

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}; }

이것은 람다의 두 복사본 을 가지고 있습니다. 그들은 같은 주소를 공유 할 수 없으므로, sizeof(toy2(some_lambda))입니다 2!


6
Nit : 함수 포인터는 void *보다 작을 수 있습니다. 두 가지 역사적 예 : sizeof (void *) == sizeof (char *)> sizeof (struct *) == sizeof (int *) 인 첫 번째 단어 주소 지정 기계. (void * 및 char *는 단어 내에서 오프셋을 유지하기 위해 약간의 추가 비트가 필요합니다.) 두 번째로 void * / int *가 세그먼트 + 오프셋이고 모든 메모리를 포함 할 수있는 8086 메모리 모델이지만 단일 64K 세그먼트에 맞는 함수 ( 따라서 함수 포인터는 16 비트에 불과했습니다).
마틴 보너 모니카 지원

1
@martin 사실입니다. 추가 ()되었습니다.
Yakk-Adam Nevraumont

50

람다는 함수 포인터가 아닙니다.

람다는 클래스의 인스턴스입니다. 코드는 다음과 거의 동일합니다.

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;

람다를 나타내는 내부 클래스에는 클래스 멤버가 없으므로 sizeof()1입니다 ( 다른 곳에 적절하게 언급 된 이유로 0 일 수 없음 ).

람다가 일부 변수를 캡처하는 경우 클래스 멤버와 동일하며 sizeof()그에 따라 표시됩니다.


3
sizeof()0이 될 수없는 이유를 설명하는 "elsewhere"에 연결할 수 있습니까?
user1717828

26

컴파일러는 람다를 다음 구조체 유형으로 거의 변환합니다.

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

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

이 구조체에는 비 정적 멤버가 없으므로 빈 구조체 인 1.

비어 있지 않은 캡처 목록을 람다에 추가하자마자 변경됩니다.

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

번역 할

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;
}

생성 된 구조체는 이제 int캡처를 위해 비 정적 멤버 를 저장해야 하므로 크기가 sizeof(int). 더 많은 것을 캡처할수록 크기가 계속 커집니다.

(구조체 비유를 소금과 함께 생각해보세요. 람다가 내부적으로 작동하는 방식을 추론하는 좋은 방법이지만 컴파일러가 수행 할 작업을 문자 그대로 번역 한 것은 아닙니다)


12

람다는 mimumum에서 구현에 대한 포인터가되어야하지 않습니까?

반드시 그런 것은 아닙니다. 표준에 따르면 이름이 지정되지 않은 고유 한 클래스의 크기는 구현에 따라 정의됩니다 . [expr.prim.lambda] , C ++ 14 (강조 내) 에서 발췌 :

람다 표현식의 유형 (클로저 객체의 유형이기도 함)은 아래에 설명 된 속성을 가진 고유 한 명명되지 않은 비 유니온 클래스 유형 (클로저 유형이라고 함)입니다.

[...]

구현은 아래설명 된 것과 다르게 클로저 유형을 정의 할 수 있습니다. 단, 다음을 변경하는 것 외에는 프로그램의 관찰 가능한 동작을 변경하지 않습니다 .

- 폐쇄 형의 크기 및 / 또는 정렬 ,

— 클로저 유형이 간단하게 복사 가능한지 여부 (9 절),

— 클로저 유형이 표준 레이아웃 클래스인지 여부 (Clause 9) 또는

— 클로저 유형이 POD 클래스인지 여부 (Clause 9)

귀하의 경우-사용하는 컴파일러의 경우-크기가 1로 표시되지만 고정 된 것은 아닙니다. 컴파일러 구현에 따라 다를 수 있습니다.


이 비트가 적용되는 것이 확실합니까? 캡처 그룹이없는 람다는 실제로 "폐쇄"가 아닙니다. (표준은 어쨌든 빈 캡처 그룹 람다를 "클로저"로 지칭합니까?)
Kyle Strand

1
네, 그렇습니다. 이것이 표준이 말하는 것입니다. " 람다 표현식의 평가는 prvalue 임시를 생성합니다.이 임시를 클로저 객체라고합니다. "캡처 여부에 관계없이 클로저 객체입니다. 단지 상승 값이 없습니다.
legends2k

나는 반대표를 던지지 않았지만 아마도 반대 투표자는이 답변이 가치 있다고 생각하지 않습니다. 왜냐하면 (표준 관점이 아닌 이론적 관점에서) 실행 시간 포인터를 포함하지 않고 람다를 구현할 수있는 이유를 설명하지 않기 때문입니다 . 호출 연산자 기능. (질문에서 KerrekSB 내 설명을 참조하십시오.)
카일 스트랜드에게

7

에서 http://en.cppreference.com/w/cpp/language/lambda :

람다 식은 다음 을 포함하는 가장 작은 블록 범위, 클래스 범위 또는 네임 스페이스 범위에서 선언되는 클로저 유형으로 알려진 고유 한 명명되지 않은 비 결합 비 집계 클래스 유형의 명명되지 않은 prvalue 임시 개체를 구성합니다. 람다 식.

lambda-expression이 사본으로 무엇이든 캡처하는 경우 (암시 적으로 캡처 절 [=]을 사용하거나 문자 &를 포함하지 않는 캡처 (예 : [a, b, c])를 사용 하여 명시 적으로), 클로저 유형에는 이름이 지정되지 않은 비 정적 데이터가 포함됩니다. 지정되지 않은 순서로 선언 된 members 는 캡처 된 모든 엔티티의 사본을 보유합니다.

참조캡처 된 엔티티의 경우 (기본 캡처 [&] 사용 또는 & 문자를 사용할 때 (예 : [& a, & b, & c])) 클로저 유형에서 추가 데이터 멤버가 선언되면 지정되지 않습니다.

에서 http://en.cppreference.com/w/cpp/language/sizeof

빈 클래스 유형에 적용하면 항상 1을 반환합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.