고유 한 익명 유형으로 언어를 디자인하는 이유는 무엇입니까?


90

이것은 항상 C ++ 람다 식의 기능으로 나를 괴롭히는 것입니다. C ++ 람다 식의 유형은 독특하고 익명이므로 간단히 적을 수 없습니다. 구문 적으로 정확히 동일한 두 개의 람다를 생성하더라도 결과 유형은 구별되도록 정의됩니다. 그 결과, a) 람다는 컴파일 시간을 허용하는 템플릿 함수에만 전달 될 수 있으며, 말할 수없는 유형은 객체와 함께 전달 될 수 있으며 b) 람다는를 통해 유형이 지워진 후에 만 ​​유용합니다 std::function<>.

좋아,하지만 그것이 C ++이하는 방식이다. 나는 그 언어의 성가신 기능으로 쓸 준비가되었다. 그러나 Rust가 겉보기에 똑같은 일을한다는 것을 방금 배웠습니다. 각 Rust 함수 또는 람다는 고유 한 익명 유형을 가지고 있습니다. 그리고 지금 궁금합니다. 왜?

그래서, 제 질문은 이것입니다.
언어 디자이너의 관점에서 언어에 고유 한 익명 유형의 개념을 도입하는 이점은 무엇입니까?


6
항상 그렇듯이 더 좋은 질문은 왜 안 되는가입니다.
Stargateur

31
"그 람다는 std :: function <>을 통해 유형이 지워진 후에 만 ​​유용합니다."-아니요, std::function. 템플릿 함수에 전달 된 람다는를 포함하지 않고 직접 호출 할 수 있습니다 std::function. 그런 다음 컴파일러는 람다를 템플릿 함수에 인라인하여 런타임 효율성을 향상시킬 수 있습니다.
Erlkoenig

1
내 생각에, 그것은 람다를 내포하는 것을 더 쉽게 만들고 언어를 이해하기 쉽게 만듭니다. 똑같은 람다 식을 같은 유형으로 접을 수 있도록 허용했다면, { int i = 42; auto foo = [&i](){ return i; }; } { int i = 13; auto foo = [&i](){ return i; }; }참조하는 변수가 텍스트 상 동일하더라도 다르기 때문에 처리 할 특별한 규칙이 필요 합니다. 그것들이 모두 독특하다고 말하면 그것을 알아 내려고 걱정할 필요가 없습니다.
NathanOliver

5
하지만 람다 유형에도 이름을 부여 할 수 있고 모두 똑같이 할 수 있습니다. lambdas_type = decltype( my_lambda);
idclev 463035818

3
그러나 일반적인 람다의 유형은 무엇 [](auto) {}입니까? 시작하려면 유형이 있어야합니까?
Evg

답변:


78

많은 표준 (특히 C ++)은 컴파일러에서 요구하는 양을 최소화하는 접근 방식을 취합니다. 솔직히, 그들은 이미 충분히 요구합니다! 작동하기 위해 무언가를 지정할 필요가 없다면 구현을 정의한 채로 두는 경향이 있습니다.

람다가 익명이 아니었다면 정의해야합니다. 이것은 변수가 캡처되는 방법에 대해 많은 것을 말해야 할 것입니다. 람다의 경우를 고려하십시오 [=](){...}. 유형은 실제로 람다에 의해 캡처 된 유형을 지정해야하는데, 이는 사소하지 않을 수 있습니다. 또한 컴파일러가 변수를 성공적으로 최적화하면 어떻게 될까요? 중히 여기다:

static const int i = 5;
auto f = [i]() { return i; }

최적화 컴파일러는 i캡처 할 수있는 유일한 값 이 5라는 것을 쉽게 인식 할 수 있으며이를 auto f = []() { return 5; }. 그러나 유형이 익명이 아닌 경우 유형이 변경 되거나 컴파일러가 i실제로 필요하지 않더라도 저장하여 덜 최적화하도록 강제 할 수 있습니다. 이것은 람다가 의도 한 일에 단순히 필요하지 않은 복잡성과 뉘앙스의 전체 가방입니다.

그리고 실제로 익명이 아닌 유형이 필요한 오프 케이스에서는 항상 클로저 클래스를 직접 구성하고 람다 함수가 아닌 펑터로 작업 할 수 있습니다. 따라서 그들은 람다가 99 %의 경우를 처리하도록 만들 수 있으며 1 %에서 자신의 솔루션을 코딩 할 수 있습니다.


Deduplicator는 내가 익명 성만큼 고유성을 다루지 않았다는 의견을 지적했습니다. 고유성의 이점은 확실하지 않지만 유형이 고유 한 경우 다음 동작이 분명하다는 점에 주목할 가치가 있습니다 (동작은 두 번 인스턴스화 됨).

int counter()
{
    static int count = 0;
    return count++;
}

template <typename FuncT>
void action(const FuncT& func)
{
    static int ct = counter();
    func(ct);
}

...
for (int i = 0; i < 5; i++)
    action([](int j) { std::cout << j << std::endl; });

for (int i = 0; i < 5; i++)
    action([](int j) { std::cout << j << std::endl; });

유형이 고유하지 않은 경우이 경우 어떤 동작이 발생해야하는지 지정해야합니다. 까다로울 수 있습니다. 익명 성을 주제로 제기 된 문제 중 일부는이 경우 독창성에 대한 추악한 머리를 제기하기도합니다.


이것은 실제로 컴파일러 구현자를위한 작업을 저장하는 것이 아니라 표준 유지 관리자를위한 작업을 저장하는 것입니다. 컴파일러는 여전히 특정 구현에 대해 위의 모든 질문에 답해야하지만 표준에 지정되어 있지 않습니다.
ComicSansMS

2
@ComicSansMS 컴파일러를 구현할 때 이러한 것들을 모으는 것은 다른 사람의 표준에 맞게 구현할 필요가 없을 때 훨씬 쉽습니다. 경험으로 말하면 표준 유지 관리자가 기능을 과도하게 지정하는 것이 언어에서 원하는 기능을 가져 오면서 지정할 최소량을 찾는 것보다 훨씬 쉽습니다. 훌륭한 사례 연구로서 memory_order_consume을 과도하게 지정하는 것을 피하면서 여전히 유용하게 만드는 데 얼마나 많은 작업을했는지 살펴보십시오 (일부 아키텍처에서)
Cort Ammon

1
다른 모든 사람과 마찬가지로 익명의 . 하지만 정말 그런 좋은 아이디어입니다 강제 로 그것을 독특한 뿐만 아니라?
Deduplicator

여기서 중요한 것은 컴파일러의 복잡성이 아니라 생성 된 코드의 복잡성입니다. 요점은 컴파일러를 더 단순하게 만드는 것이 아니라 모든 경우를 최적화하고 대상 플랫폼에 대한 자연스러운 코드를 생성 할 수있는 충분한 공간을 제공하는 것입니다.
Jan Hudec

정적 변수를 캡처 할 수 없습니다.
Ruslan

70

Lambda는 단순한 함수가 아니라 함수 이자 상태 입니다. 따라서 C ++와 Rust는 모두 호출 연산자 ( operator()C ++에서는 Fn*Rust 의 3 가지 특성) 를 사용하여 객체로 구현합니다 .

기본적으로 [a] { return a + 1; }C ++에서는 다음과 같이 설탕을 제거합니다.

struct __SomeName {
    int a;

    int operator()() {
        return a + 1;
    }
};

그런 다음 __SomeName람다가 사용되는 인스턴스를 사용합니다.

Rust에 || a + 1있는 동안 Rust에서는 다음과 같이 설탕을 제거합니다.

{
    struct __SomeName {
        a: i32,
    }

    impl FnOnce<()> for __SomeName {
        type Output = i32;
        
        extern "rust-call" fn call_once(self, args: ()) -> Self::Output {
            self.a + 1
        }
    }

    // And FnMut and Fn when necessary

    __SomeName { a }
}

이 방법은 대부분의 람다가 있어야다른 유형.

이제 우리가 할 수있는 몇 가지 방법이 있습니다.

  • 익명 유형을 사용하면 두 언어가 모두 구현합니다. 그것의 또 다른 결과는 모든 람다 다른 유형을 가져야 한다는 것입니다. 그러나 언어 설계자에게는 분명한 이점이 있습니다. Lambda는 이미 존재하는 더 단순한 언어 부분을 사용하여 간단히 설명 할 수 있습니다. 그들은 이미 존재하는 언어의 일부에 대한 구문 설탕 일뿐입니다.
  • 람다 유형 이름 지정을위한 몇 가지 특수 구문 : 그러나 람다는 C ++의 템플릿 또는 Fn*Rust의 제네릭 및 특성 과 함께 이미 사용할 수 있기 때문에 필요하지 않습니다 . 두 언어 모두 람다를 입력하여 사용하도록 강제하지 않습니다 ( std::functionC ++ 또는 Box<Fn*>Rust에서).

또한 두 언어 모두 컨텍스트 캡처하지 않는 사소한 람다 를 함수 포인터로 변환 할 수 있다는 데 동의합니다 .


더 간단한 기능을 사용하여 언어의 복잡한 기능을 설명하는 것은 매우 일반적입니다. 예를 들어 C ++와 Rust에는 모두 range-for 루프가 있으며 둘 다 다른 기능에 대한 구문 설탕으로 설명합니다.

C ++ 정의

for (auto&& [first,second] : mymap) {
    // use first and second
}

동등한 것으로

{

    init-statement
    auto && __range = range_expression ;
    auto __begin = begin_expr ;
    auto __end = end_expr ;
    for ( ; __begin != __end; ++__begin) {

        range_declaration = *__begin;
        loop_statement

    }

} 

그리고 Rust는

for <pat> in <head> { <body> }

동등한 것으로

let result = match ::std::iter::IntoIterator::into_iter(<head>) {
    mut iter => {
        loop {
            let <pat> = match ::std::iter::Iterator::next(&mut iter) {
                ::std::option::Option::Some(val) => val,
                ::std::option::Option::None => break
            };
            SemiExpr(<body>);
        }
    }
};

인간에게는 더 복잡해 보이지만 언어 디자이너 나 컴파일러에게는 더 간단합니다.


15
@ cmaster-reinstatemonica 정렬 함수에 대한 비교기 인수로 람다를 전달하는 것을 고려하십시오. 여기에 가상 함수 호출의 오버 헤드를 부과하고 싶습니까?
Daniel Langr

5
@ cmaster-reinstatemonica 왜냐하면 C ++에서는 기본적으로 가상이 없기 때문입니다
Caleth

4
@cmaster-람다의 모든 사용자가 필요하지 않을 때에도 동적 디 패치를 지불하도록 강요한다는 의미입니까?
StoryTeller-Unslander Monica

4
@ cmaster-reinstatemonica 당신이 얻을 수있는 최선의 방법은 가상을 선택하는 것입니다. 무엇을 추측, std::function그 수행
Caleth

9
@ cmaster-reinstatemonica 호출 할 함수를 다시 가리킬 수있는 모든 메커니즘에는 런타임 오버 헤드가있는 상황이 있습니다. 그것은 C ++ 방식이 아닙니다. 당신은과에서 선택std::function
Caleth

13

(Caleth의 답변에 추가하지만 주석에 맞추기에는 너무 깁니다.)

람다 식은 익명 구조체 (이름을 말할 수 없기 때문에 Voldemort 유형)에 대한 구문 설탕 일뿐입니다.

이 코드 조각에서 익명 구조체와 람다의 익명 성 사이의 유사성을 확인할 수 있습니다.

#include <iostream>
#include <typeinfo>

using std::cout;

int main() {
    struct { int x; } foo{5};
    struct { int x; } bar{6};
    cout << foo.x << " " << bar.x << "\n";
    cout << typeid(foo).name() << "\n";
    cout << typeid(bar).name() << "\n";
    auto baz = [x = 7]() mutable -> int& { return x; };
    auto quux = [x = 8]() mutable -> int& { return x; };
    cout << baz() << " " << quux() << "\n";
    cout << typeid(baz).name() << "\n";
    cout << typeid(quux).name() << "\n";
}

람다가 여전히 만족스럽지 않다면 익명 구조체도 만족스럽지 않습니다.

일부 언어는 좀 더 융통성있는 일종의 덕 타이핑을 허용하며, C ++에는 람다를 사용하는 대신 직접 람다를 대체 할 수있는 멤버 필드가있는 템플릿에서 개체를 만드는 데 도움이되지 않는 템플릿이 있지만 std::function싸개.


3
감사합니다. 실제로 람다가 C ++에서 정의되는 방식에 대한 추론을 약간 밝힙니다 ( "Voldemort 유형"이라는 용어를 기억해야합니다. :-)). 그러나 질문은 남아 있습니다. 언어 디자이너의 눈에 이것 의 장점 은 무엇입니까 ?
cmaster-모니카 복원

1
int& operator()(){ return x; }그 구조체에 추가 할 수도 있습니다
Caleth

2
@ cmaster-reinstatemonica • 추측 적으로 ... 나머지 C ++는 그런 방식으로 작동합니다. 람다가 일종의 "표면 모양"을 사용하도록하려면 오리 타이핑은 나머지 언어와는 매우 다릅니다. 람다 용 언어에 이러한 종류의 기능을 추가하는 것은 아마도 전체 언어에 대해 일반화되는 것으로 간주 될 것이며 잠재적으로 큰 변경 사항이 될 것입니다. 람다에 대한 이러한 기능을 생략하는 것은 나머지 C ++의 강력한 타이핑에 적합합니다.
Eljay

기술적으로 Voldemort 유형은입니다 auto foo(){ struct DarkLord {} tom_riddle; return tom_riddle; }. 왜냐하면 foo아무것도 외부 에서 식별자를 사용할 수 없기 때문입니다DarkLord
Caleth

@ cmaster-reinstatemonica 효율성, 대안은 모든 람다를 박스 및 동적으로 디스패치하는 것입니다 (힙에 할당하고 정확한 유형을 지 웁니다). 이제 컴파일러 익명 유형의 람다를 중복 제거 할 수 있지만 여전히 적을 수 없으며 거의 ​​이득을 얻기 위해 상당한 작업이 필요하므로 확률이 실제로 유리하지 않습니다.
Masklinn

10

고유 한 익명 유형 으로 언어를 디자인하는 이유는 무엇 입니까?

이름이 무관하고 유용하지 않거나 심지어 비생산적인 경우가 있기 때문입니다. 이 경우 자신의 존재를 추상화하는 능력은 이름 오염을 줄이고 컴퓨터 과학의 두 가지 어려운 문제 (이름을 지정하는 방법) 중 하나를 해결하기 때문에 유용합니다. 같은 이유로 임시 개체가 유용합니다.

람다

고유성은 특별한 람다가 아니며 익명 유형에 대한 특별한 것도 아닙니다. 언어의 명명 된 유형에도 적용됩니다. 다음을 고려하십시오.

struct A {
    void operator()(){};
};

struct B {
    void operator()(){};
};

void foo(A);

클래스가 동일하더라도 B으로 전달할 수 없습니다 foo. 이 동일한 속성이 이름이 지정되지 않은 유형에 적용됩니다.

람다는 컴파일 시간, 말할 수없는 유형이 객체와 함께 전달되도록 허용하는 템플릿 함수에만 전달 될 수 있습니다. std :: function <>을 통해 지워집니다.

람다 하위 집합에 대한 세 번째 옵션이 있습니다. 캡처하지 않는 람다를 함수 포인터로 변환 할 수 있습니다.


익명 형식의 제한이 사용 사례에 문제가되는 경우 솔루션은 간단합니다. 대신 명명 된 형식을 사용할 수 있습니다. Lambda는 명명 된 클래스로 수행 할 수없는 작업을 수행하지 않습니다.


10

Cort Ammon의 대답 은 훌륭하지만 구현 가능성에 대해 한 가지 더 중요한 점이 있다고 생각합니다.

"one.cpp"와 "two.cpp"라는 두 개의 다른 번역 단위가 있다고 가정합니다.

// one.cpp
struct A { int operator()(int x) const { return x+1; } };
auto b = [](int x) { return x+1; };
using A1 = A;
using B1 = decltype(b);

extern void foo(A1);
extern void foo(B1);

의 두 오버로드 foo는 동일한 식별자 ( foo)를 사용하지만 다른 이름을 엉망으로 만듭니다. (POSIX 시스템에서 사용되는 Itanium ABI에서 잘린 이름은이며이 _Z3foo1A경우에는 _Z3fooN1bMUliE_E.)

// two.cpp
struct A { int operator()(int x) const { return x + 1; } };
auto b = [](int x) { return x + 1; };
using A2 = A;
using B2 = decltype(b);

void foo(A2) {}
void foo(B2) {}

C ++ 컴파일러 void foo(A1) "two.cpp"의 변경된 이름이 "one.cpp"의 변경된 이름과 동일한 지 확인 extern void foo(A2)해야 두 개체 파일을 함께 연결할 수 있습니다. 이것은 "동일한 유형"인 두 유형 의 물리적 의미 입니다. 본질적으로 별도로 컴파일 된 개체 파일 간의 ABI 호환성에 관한 것입니다.

C ++ 컴파일러는 및 이 "동일한 유형" 인지 확인하는 데 필요 하지 않습니다 . (사실 서로 다른 유형인지 확인해야하지만 지금은 그렇게 중요하지 않습니다.)B1B2


어떤 물리적 메커니즘을하는 것을 보장하기 위해 컴파일러 사용을 수행 A1하고A2 "동일 유형"인가?

단순히 typedef를 통해 잠복 한 다음 형식의 정규화 된 이름을 확인합니다. 라는 클래스 유형 A입니다. ( ::A글로벌 네임 스페이스에 있기 때문입니다.) 따라서 두 경우 모두 동일한 유형입니다. 이해하기 쉽습니다. 더욱 중요한 것은 쉽다 구현 . 두 클래스 유형이 동일한 유형인지 확인하려면 이름을 가져 와서strcmp . 클래스 유형을 함수의 이름을 변경하려면 이름에 문자 수를 쓰고 그 뒤에 해당 문자를 입력합니다.

따라서 명명 된 유형은 쉽게 조작 할 수 있습니다.

어떤 물리적 인 메커니즘을 수있는 컴파일러 사용을 보장하기 위하여 B1B2C ++가 동일한 유형으로 그들을 필요한 경우 "동일 유형은"가상의 세계에?

글쎄, 유형의 이름을 사용할 수 없습니다. 유형 에는 이름을.

람다 본문의 텍스트 를 어떻게 든 인코딩 할 수 있습니다. 그러나 그것은 다소 어색 할 것입니다. 왜냐하면 실제로 b"one.cpp"는 "two.cpp"에서와 미묘하게 다르기 때문입니다 b: "one.cpp"는 가지고 x+1있고 "two.cpp"는 x + 1. 우리는이 공백 차이가 있음 중 하나라는 규칙을 마련 할 것 그래서 하지 않는 문제, 또는 그 수행 (결국 그들에게 다른 유형을), 또는 어쩌면 않습니다 (아마 프로그램의 유효성이 구현 정의 , 또는 "진단이 필요하지 않은 형식이 잘못되었습니다"). 어쨌든,A

가장 쉬운 방법은 각 람다식이 고유 한 유형의 값을 생성한다고 말하는 것입니다. 그러면 서로 다른 번역 단위로 정의 된 두 개의 람다 유형은 확실히 동일한 유형아닙니다 . 단일 번역 단위 내에서 소스 코드의 시작 부분부터 계산하여 람다 유형을 "이름"할 수 있습니다.

auto a = [](){};  // a has type $_0
auto b = [](){};  // b has type $_1
auto f(int x) {
    return [x](int y) { return x+y; };  // f(1) and f(2) both have type $_2
} 
auto g(float x) {
    return [x](int y) { return x+y; };  // g(1) and g(2) both have type $_3
} 

물론 이러한 이름은이 번역 단위 내에서만 의미가 있습니다. 이 TU $_0는 항상 다른 TU와 다른 유형 $_0이지만,이 TU struct A는 항상 다른 TU와 동일한 유형 struct A입니다.

그건 그렇고, 우리의 "람다 텍스트 인코딩"아이디어에는 또 다른 미묘한 문제가 있습니다. 람다 $_2이고 $_3정확히 동일한 텍스트 로 구성 되지만 분명히 동일한 유형 으로 간주되어서는 안됩니다 !


그건 그렇고, C ++는 컴파일러가 임의의 C ++ 표현식 의 텍스트를 조작하는 방법을 알아야합니다 .

template<class T> void foo(decltype(T())) {}
template void foo<int>(int);  // _Z3fooIiEvDTcvT__EE, not _Z3fooIiEvT_

그러나 C ++는 컴파일러가 임의의 C ++ 을 조작하는 방법을 알 필요가 없습니다 . decltype([](){ ...arbitrary statements... })C ++ 20에서도 여전히 잘못된 형식입니다.


또한 /를 사용하여 이름이 지정되지 않은 유형에 로컬 별칭을 제공 하는 것이 쉽습니다 . 당신의 질문이 이렇게 해결 될 수있는 일을하려고해서 나온 것 같다고 생각합니다.typedefusing

auto f(int x) {
    return [x](int y) { return x+y; };
}

// Give the type an alias, so I can refer to it within this translation unit
using AdderLambda = decltype(f(0));

int of_one(AdderLambda g) { return g(1); }

int main() {
    auto f1 = f(1);
    assert(of_one(f1) == 2);
    auto f42 = f(42);
    assert(of_one(f42) == 43);
}

추가 편집 : 다른 답변에 대한 귀하의 의견 중 일부를 읽어 보니 이유가 궁금하신 것 같습니다.

int add1(int x) { return x + 1; }
int add2(int x) { return x + 2; }
static_assert(std::is_same_v<decltype(add1), decltype(add2)>);
auto add3 = [](int x) { return x + 3; };
auto add4 = [](int x) { return x + 4; };
static_assert(not std::is_same_v<decltype(add3), decltype(add4)>);

캡처없는 람다는 기본 구성이 가능하기 때문입니다. (C ++에서는 C ++ 20에서만 가능하지만 항상 개념적으로 사실이었습니다.)

template<class T>
int default_construct_and_call(int x) {
    T t;
    return t(x);
}

assert(default_construct_and_call<decltype(add3)>(42) == 45);
assert(default_construct_and_call<decltype(add4)>(42) == 46);

당신이 시도하는 경우 default_construct_and_call<decltype(&add1)>, t기본 초기화 함수 포인터가 될 것이며, 당신은 아마 세그 폴트 것입니다. 그것은 유용하지 않은 것 같습니다.


" 사실, 서로 다른 유형인지 확인하는 것이 필요하지만 지금은 그다지 중요하지 않습니다. "동등하게 정의 된 경우 고유성을 강요 할 좋은 이유가 있는지 궁금합니다.
Deduplicator

개인적으로 나는 완전히 정의 된 행동이 (거의?) 불특정 행동보다 항상 낫다고 생각합니다. "이 두 함수 포인터가 같습니까?이 두 템플릿 인스턴스화가 동일한 함수 인 경우에만 해당됩니다.이 두 람다 유형이 동일한 유형 인 경우에만 해당되며 컴파일러가 병합하기로 결정한 경우에만 해당됩니다." 이키! (그러나 우리는 문자열 리터럴 병합과 정확히 유사한 상황을 가지고 있으며 아무도 상황 에 대해 혼란스러워하지 않습니다 . 따라서 컴파일러가 동일한 유형을 병합하도록 허용하는 것이 재앙이 될 것
같지 않습니다

두 개의 동등한 기능 (만약 제외)이 동일한 지 여부도 좋은 질문입니다. 표준의 언어는 자유 및 / 또는 정적 함수에 대해 명확하지 않습니다. 그러나 그것은 여기서 범위를 벗어납니다.
Deduplicator

놀랍게도 이번 달 LLVM 메일 링리스트에서 병합 기능에 대한 논의가있었습니다 . Clang의 코드 생성은 완전히 빈 본문이있는 함수를 거의 "우연히"병합되도록합니다. godbolt.org/z/obT55b 이것은 기술적으로 부적합하며 LLVM을 패치하여이 작업을 중단 할 가능성이 높습니다. 그러나 네, 동의합니다. 함수 주소를 병합하는 것도 문제입니다.
Quuxplusone

이 예에는 반환 문 누락이라는 다른 문제가 있습니다. 그들은 혼자서 이미 코드를 부적합하게 만들지 않습니까? 또한 토론을 살펴볼 것입니다. 그러나 동등한 기능을 병합하는 것이 표준, 문서화 된 동작, gcc에 맞지 않는 것을 보여 주거나 가정 했습니까?
Deduplicator

9

C ++ 람다 는 C ++가 정적으로 바인딩하기 때문에 고유 한 작업을 위해 고유 한 유형이 필요합니다 . 복사 / 이동 만 구성 할 수 있으므로 대부분 유형의 이름을 지정할 필요가 없습니다. 그러나 그것은 모두 다소 구현 세부 사항입니다.

C # 람다는 "익명 함수 식"이므로 형식이 있는지 확실하지 않으며 즉시 호환되는 대리자 형식 또는 식 트리 형식으로 변환됩니다. 그렇다면 아마도 발음 할 수없는 유형일 것입니다.

C ++에는 또한 각 정의가 고유 한 유형으로 이어지는 익명 구조체가 있습니다. 여기에서 이름은 발음 할 수없는 것이 아니라 표준에 관한 한 존재하지 않습니다.

C #에는 익명 데이터 형식 이 있으므로 정의 된 범위에서 이스케이프하는 것을주의 깊게 금지합니다. 구현은 그들에게도 고유하고 발음 할 수없는 이름을 제공합니다.

익명 유형을 갖는 것은 프로그래머에게 구현 내부를 찌르지 말아야한다는 신호를 보냅니다.

곁에:

람다 유형에 이름을 지정할있습니다 .

auto foo = []{}; 
using Foo_t = decltype(foo);

캡처가없는 경우 함수 포인터 유형을 사용할 수 있습니다.

void (*pfoo)() = foo;

1
첫 번째 예제 코드는 여전히 후속 허용하지 않습니다 Foo_t = []{};, 단지 Foo_t = foo다른 아무것도.
cmaster-모니카 복원

1
@ cmaster-reinstatemonica는 유형이 익명 성 때문이 아니라 기본적으로 구성 가능하지 않기 때문입니다. 내 생각에 그것은 기술적 인 이유와 마찬가지로 기억해야 할 더 큰 코너 케이스 세트를 피하는 것과 관련이 있습니다.
Caleth

6

익명 유형을 사용하는 이유는 무엇입니까?

컴파일러에 의해 자동으로 생성되는 유형의 경우 (1) 유형 이름에 대한 사용자의 요청을 따르거나 (2) 컴파일러가 자체적으로 선택하도록하는 것입니다.

  1. 전자의 경우 사용자는 이러한 구조가 나타날 때마다 명시 적으로 이름을 제공해야합니다 (C ++ / Rust : 람다가 정의 될 때마다; Rust : 함수가 정의 될 때마다). 이것은 사용자가 매번 제공하는 지루한 세부 사항이며 대부분의 경우 이름이 다시 언급되지 않습니다. 따라서 컴파일러가 자동으로 이름을 알아 내고 decltype또는 유형 추론 과 같은 기존 기능을 사용 하여 필요한 몇 곳에서 유형을 참조하는 것이 합리적 입니다.

  2. 후자의 경우 컴파일러는 유형에 대해 고유 한 이름을 선택해야합니다.이 이름은 아마도 __namespace1_module1_func1_AnonymousFunction042. 언어 설계자는이 이름이 어떻게 훌륭하고 섬세하게 구성되는지 정확하게 지정할 수 있지만, 이는 사소한 리팩터링에도 불구하고 이름이 의심의 여지없이 부서지기 때문에 현명한 사용자가 신뢰할 수없는 구현 세부 사항을 사용자에게 불필요하게 노출합니다. 이는 또한 언어의 진화를 불필요하게 제한합니다. 향후 기능 추가로 인해 기존 이름 생성 알고리즘이 변경되어 이전 버전과의 호환성 문제가 발생할 수 있습니다. 따라서이 세부 사항을 생략하고 자동 생성 유형이 사용자가 말할 수 없다고 주장하는 것이 합리적입니다.

고유 한 (고유 한) 유형을 사용하는 이유는 무엇입니까?

값에 고유 한 유형이있는 경우 최적화 컴파일러는 보장 된 충실도로 모든 사용 사이트에서 고유 한 유형을 추적 할 수 있습니다. 결과적으로 사용자는이 특정 값의 출처가 컴파일러에 완전히 알려진 위치를 확신 할 수 있습니다.

예를 들어 컴파일러가 다음을 보는 순간 :

let f: __UniqueFunc042 = || { ... };  // definition of __UniqueFunc042 (assume it has a nontrivial closure)

/* ... intervening code */

let g: __UniqueFunc042 = /* some expression */;
g();

컴파일러는 의 출처를 알지 못해도 g반드시 시작해야하는 완전한 확신을 가지고 있습니다. 이렇게하면 호출 이 비 가상화 될 수 있습니다. 사용자는 데이터 흐름을 통해 고유 한 유형을 보존하기 위해 세심한주의를 기울 였기 때문에이를 알고있을 것입니다.fggfg 입니다.

필연적으로 이것은 사용자가으로 할 수있는 작업을 제한합니다 f. 사용자는 다음과 같이 작성할 자유가 없습니다.

let q = if some_condition { f } else { || {} };  // ERROR: type mismatch

두 가지 유형의 (불법) 통일로 이어질 것이기 때문입니다.

이 문제를 해결하기 위해 사용자는를 __UniqueFunc042고유하지 않은 유형으로 업 캐스트 할 수 있습니다 &dyn Fn().

let f2 = &f as &dyn Fn();  // upcast
let q2 = if some_condition { f2 } else { &|| {} };  // OK

이 유형 삭제로 &dyn Fn()인한 장단점은 컴파일러의 추론을 복잡하게한다는 것입니다. 주어진:

let g2: &dyn Fn() = /*expression */;

컴파일러는에서 유래 /*expression */했는지 또는 다른 함수 g2에서 유래 했는지 f, 그리고 그 출처가 유지되는 조건 을 결정 하기 위해 열심히 조사해야합니다 . 많은 상황에서, 컴파일러가 제공 할 수 있습니다 : 그 말할 수 아마도 인간 g2정말로에서 오는 f모든 상황에서 만에서 경로 f에가 g2도에 가상 호출의 결과, 해독 컴파일러에 대한 뒤얽힌했다 g2비관적 성능을.

이는 이러한 객체가 일반 (템플릿) 함수에 전달 될 때 더욱 분명해집니다.

fn h<F: Fn()>(f: F);

하나를 호출하는 경우 h(f)경우 f: __UniqueFunc042, 다음 h고유 한 인스턴스에 전문입니다 :

h::<__UniqueFunc042>(f);

이를 통해 컴파일러 h는의 특정 인수에 맞게 조정 된에 대한 특수 코드를 생성 할 수 f있으며에 대한 디스패치 f는 인라인되지 않은 경우 정적 일 가능성이 높습니다.

하나의 호출 반대의 시나리오에서 h(f)f2: &Fn()의이 h같은 인스턴스화

h::<&Fn()>(f);

유형의 모든 기능간에 공유 &Fn()됩니다. 내부 h에서 컴파일러는 불투명 한 유형의 함수에 대해 거의 알지 못 &Fn()하므로 f가상 디스패치를 ​​사용하여 보수적으로 만 호출 할 수 있습니다. 정적으로 디스패치하려면 컴파일러가 h::<&Fn()>(f)호출 사이트에서에 대한 호출 을 인라인해야합니다 . 이는 h너무 복잡 하면 보장되지 않습니다 .


이름 선택에 대한 첫 번째 부분은 요점을 놓칩니다. void(*)(int, double) 에는 이름이 없을 수 있지만 적어 둘 수 있습니다. 나는 그것을 익명의 유형이 아닌 이름없는 유형이라고 부를 것입니다. 그리고 저는 __namespace1_module1_func1_AnonymousFunction042이 질문의 범위에 속하지 않는 이름 맹 글링 과 같은 비밀스러운 것들을 부를 것 입니다. 이 질문은 이러한 유형을 유용한 방식으로 표현할 수있는 유형 구문을 도입하는 것과는 반대로 표준에 의해 기록이 불가능하다고 보장하는 유형에 관한 것입니다.
cmaster-monica

3

첫째, 캡처가없는 람다는 함수 포인터로 변환 할 수 있습니다. 그래서 그들은 어떤 형태의 일반성을 제공합니다.

이제 캡처 기능이있는 람다를 포인터로 변환 할 수없는 이유는 무엇입니까? 함수는 람다 상태에 액세스해야하므로이 상태는 함수 인수로 표시되어야합니다.


음, 캡처는 람다 자체의 일부가되어야합니다. 그것들이 std::function<>.
cmaster-모니카 복원

3

사용자 코드와 이름 충돌을 방지합니다.

동일한 구현을 가진 두 개의 람다조차도 유형이 다릅니다. 메모리 레이아웃이 동일하더라도 객체에 대해 다른 유형을 가질 수 있기 때문에 괜찮습니다.


유사 유형은 int (*)(Foo*, int, double)사용자 코드와 이름이 충돌 할 위험이 없습니다.
cmaster-monica 복원

귀하의 예는 잘 일반화되지 않습니다. 람다 식은 구문 일 뿐이지 만 특히 캡처 절을 사용하여 일부 구조체로 평가됩니다. 명시 적으로 이름을 지정하면 이미 존재하는 구조체의 이름 충돌이 발생할 수 있습니다.
knivil

다시 말하지만,이 질문은 C ++가 아니라 언어 디자인에 관한 것입니다. 람다의 유형이 데이터 구조 유형보다 함수 포인터 유형에 더 가까운 언어를 확실히 정의 할 수 있습니다. C ++의 함수 포인터 구문과 C의 동적 배열 유형 구문은 이것이 가능함을 증명합니다. 그리고 람다가 비슷한 접근 방식을 사용하지 않은 이유 는 무엇입니까?
cmaster-모니카 복원

1
아니요, 가변 커링 (캡처) 때문에 할 수 없습니다. 작동하려면 함수와 데이터가 모두 필요합니다.
Blindy

@Blindy 오, 예, 할 수 있습니다. 람다는 두 개의 포인터를 포함하는 객체로 정의 할 수 있습니다. 하나는 캡처 객체 용이고 다른 하나는 코드 용입니다. 이러한 람다 객체는 값으로 쉽게 전달할 수 있습니다. 또는 실제 람다 코드로 이동하기 전에 자체 주소를 사용하는 캡처 개체의 시작 부분에 코드 스텁으로 트릭을 가져올 수 있습니다. 그러면 람다 포인터가 단일 주소로 바뀝니다. 그러나 이것은 PPC 플랫폼이 증명 했으므로 불필요합니다. PPC에서 함수 포인터는 실제로 포인터 쌍입니다. 당신이 캐스팅 할 수없는 이유 void(*)(void)void*다시 표준 C / C ++ 및.
cmaster-모니카 복원
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.