なぜ一意の匿名型で言語を設計するのですか?


92

これは、C ++ラムダ式の機能として常に私を悩ませてきたものです。C++ラムダ式のタイプは一意で匿名であり、単に書き留めることはできません。構文的にまったく同じ2つのラムダを作成した場合でも、結果の型は異なるものとして定義されます。その結果、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);
maximum_prime_is_4630358 1820

3
しかし、ジェネリックラムダのタイプはどうあるべき[](auto) {}ですか?そもそもタイプが必要ですか?
平均

回答:


78

多くの標準(特にC ++)は、コンパイラーに要求する量を最小限に抑えるアプローチを採用しています。率直に言って、彼らはすでに十分に要求しています!それを機能させるために何かを指定する必要がない場合、実装を定義したままにする傾向があります。

ラムダが匿名でない場合は、それらを定義する必要があります。これは、変数がどのようにキャプチャされるかについて多くを語る必要があります。ラムダの場合を考えてみましょう[=](){...}。タイプは、ラムダによって実際にキャプチャされたタイプを指定する必要がありますが、これを決定するのは簡単ではありません。また、コンパイラが変数を正常に最適化した場合はどうなりますか?考えてみましょう:

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

最適化コンパイラーはi、キャプチャーできる唯一の可能な値が5であることを簡単に認識し、これをauto f = []() { return 5; }。に置き換えます。ただし、型が匿名でない場合、これにより型が変更されたり、コンパイラーの最適化が少なくなり、i実際には必要ない場合でも保存される可能性があります。これは複雑さとニュアンスの全体の袋であり、ラムダが意図したことには必要ありません。

また、実際に非匿名型が必要な場合は、いつでも自分でクロージャクラスを作成し、ラムダ関数ではなくファンクターを使用できます。したがって、ラムダに99%のケースを処理させ、1%で独自のソリューションをコーディングすることができます。


Deduplicatorはコメントの中で、私は匿名性ほど一意性については触れていないと指摘しました。一意性の利点についてはあまり確信がありませんが、タイプが一意である場合、次の動作が明確であることに注意してください(アクションは2回インスタンス化されます)。

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
他のみんなと同じように、あなたは匿名の説得力のある主張をします。しかし、それを強制的ユニークにするのは本当に良い考えですか?
デュプリケータ

ここで重要なのはコンパイラの複雑さではなく、生成されたコードの複雑さです。重要なのは、コンパイラーを単純化することではなく、すべてのケースを最適化し、ターゲットプラットフォーム用の自然なコードを生成するのに十分な小刻みに動く余地を与えることです。
JanHudec20年

静的変数をキャプチャすることはできません。
ルスラン

70

ラムダは単なる関数ではなく、関数と状態です。したがって、C ++とRustはどちらも、呼び出し演算子を使用してオブジェクトとして実装します(operator()C ++では、Fn*Rustの3つの特性)。

基本的に[a] { return a + 1; }、C ++では次のようなものに脱糖します

struct __SomeName {
    int a;

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

次に__SomeName、ラムダが使用される場所のインスタンスを使用します。

Rustにいる間、Rustでは|| a + 1次のようなものに脱糖します

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

これは、ほとんどのラムダが異なるタイプでなければならないことを意味します。

今、私たちがそれを行うことができるいくつかの方法があります:

  • 匿名型の場合、これは両方の言語が実装するものです。その別の結果は、すべてのラムダが異なるタイプでなければならないということです。しかし、言語設計者にとって、これには明らかな利点がありますラムダは、言語の他の既存のより単純な部分を使用して簡単に記述できます。それらは、言語の既存のビットの周りの単なる構文糖衣です。
  • ラムダタイプに名前を付けるためのいくつかの特別な構文を使用する場合:ただし、ラムダは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ソート関数のコンパレータ引数としてラムダを渡すことを検討してください。ここで仮想関数呼び出しのオーバーヘッドを本当に課したいですか?
DanielLangr20年

5
@ cmaster-reinstatemonica C ++ではデフォルトで仮想化されているものがないため
Caleth 2010

4
@ cmaster-ラムダのすべてのユーザーに、必要がない場合でも動的ディスパッチの支払いを強制するという意味ですか?
StoryTeller-UnslanderMonica20年

4
@ cmaster-reinstatemonicaあなたが得る最良のものは仮想へのオプトインです。何を推測しますか、それstd::functionは何
ですか

9
cmaster-reinstatemonica @任意のあなたは再びポイントと呼ばれる関数が実行時のオーバーヘッドを持つ状況がありますすることができますメカニズム。それはC ++の方法ではありません。オプトインstd::function
Caleth 2010

13

(カレスの答えに追加しますが、コメントに入れるには長すぎます。)

ラムダ式は、匿名構造体(名前が言えないため、ヴォルデモート型)の単なる構文糖衣です。

このコードスニペットで、匿名構造体とラムダの匿名性の類似性を確認できます。

#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 ++で定義される方法の背後にある理由に少し光を当てています(私は「ヴォルデモートタイプ」という用語を覚えています:-))。ただし、疑問は残ります。言語設計者にとって、これの利点は何ですか。
cmaster -モニカ回復

1
int& operator()(){ return x; }これらの構造体に追加することもできます
Caleth 2010

2
@ cmaster-reinstatemonica•投機的...残りのC ++はそのように動作します。ラムダにある種の「表面形状」のダックタイピングを使用させることは、他の言語とは非常に異なるものになります。ラムダの言語にその種の機能を追加すると、おそらく言語全体で一般化されていると見なされ、それは潜在的に大きな重大な変更になるでしょう。ラムダだけのためにそのような機能を省略することは、C ++の残りの部分の強くっぽいタイピングに適合します。
Eljay

技術的には、ヴォルデモートのタイプは次のようになりますauto foo(){ struct DarkLord {} tom_riddle; return tom_riddle; }foo何も外では識別子を使用できないためですDarkLord
Caleth

@ cmaster-効率を回復します。代わりに、すべてのラムダをボックス化して動的にディスパッチします(ヒープに割り当てて、正確なタイプを消去します)。お気づきのように、コンパイラは匿名タイプのラムダを重複排除できますが、それでもそれらを書き留めることはできず、わずかな利益のために多大な作業が必要になるため、オッズは実際には有利ではありません。
マスクリン

10

なぜ一意の匿名型で言語を設計するのですか?

名前が無関係で役に立たない、あるいは逆効果でさえある場合があるからです。この場合、それらの存在を抽象化する機能は、名前の汚染を減らし、コンピューターサイエンスの2つの難しい問題(名前の付け方)の1つを解決するので便利です。同じ理由で、一時オブジェクトは便利です。

ラムダ

一意性は特別なラムダのものではなく、匿名型にとっても特別なものではありません。言語の名前付きタイプにも適用されます。次のことを検討してください。

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

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

void foo(A);

私は渡すことができないことに注意Bfooクラスが同じであっても、。これと同じプロパティが名前のないタイプに適用されます。

ラムダは、コンパイル時の無名型をオブジェクトと一緒に渡すことができるテンプレート関数にのみ渡すことができます... std :: function <>を介して消去されます。

ラムダのサブセットには3番目のオプションがあります。キャプチャーしないラムダは関数ポインターに変換できます。


匿名型の制限がユースケースの問題である場合、解決策は単純であることに注意してください。代わりに名前付き型を使用できます。ラムダは、名前付きクラスでは実行できないことは何もしません。


10

Cort Ammonの受け入れられた答えは良いですが、実装可能性についてもう1つ重要な点があると思います。

「one.cpp」と「two.cpp」の2つの異なる翻訳単位があるとします。

// 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);

の2つのオーバーロードは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」ではのマングルされた名前と同じであるextern void foo(A2)私たちが一緒に2つのオブジェクト・ファイルをリンクすることができそうという、「one.cpp」インチ これは 2つのタイプが「同じタイプ」であることの物理的な意味です。本質的には、別々にコンパイルされたオブジェクトファイル間のABI互換性に関するものです。

C ++コンパイラは、とが「同じタイプ」であることを確認する必要はありません。(実際、それらが異なるタイプであることを確認する必要がありますが、それは今のところそれほど重要ではありません。)B1B2


どのような物理的メカニズムのことを確保するためのコンパイラを使用しないA1と、A2「同じタイプ」ですか?

typedefを掘り下げてから、型の完全修飾名を調べます。これは、という名前のクラスタイプAです。(まあ、::Aそれはグローバル名前空間にあるので。)したがって、どちらの場合も同じタイプです。それは理解しやすいです。さらに重要なのは、実装が簡単なことです。2つのクラスタイプが同じタイプであるかどうかを確認するには、それらの名前を取得してstrcmp。を実行します。クラス型を関数のマングル名にマングルするには、名前に文字数を記述し、その後にそれらの文字を記述します。

そのため、名前付きタイプは簡単にマングルできます。

どのような物理的なメカニズムかもしれないことを確実にするために、コンパイラの使用B1およびB2C ++は、同じタイプであることを、それらを必要と架空の世界では「同じタイプ」ですか?

タイプがいないのでまあ、それは、型の名前を使用することができませんでし持って名前を。

多分それはラムダの本体のテキストをどういうわけかエンコードすることができます。しかし、実際にbは「one.cpp」のinは「two.cpp」のinと微妙に異なるため、これは少し厄介ですb。「one.cpp」にはがx+1あり、「two.cpp」にはがありx + 1ます。私たちは、この空白の差があることのどちらかと言うルールを思い付くする必要があると思いますので、ない問題、またはそれがあることない(それらすべての後に、異なる種類にする)、またはことを多分ない(多分プログラムの有効性は、実装定義であります、または多分それは「不正な形式の診断は必要ありません」)。とにかく、A

この問題を解決する最も簡単な方法は、各ラムダ式が一意の型の値を生成すると言うことです。その場合、異なる変換単位で定義された2つのラムダタイプは間違いなく同じタイプではありません。単一の翻訳ユニット内で、ソースコードの先頭から数えるだけでラムダタイプに「名前を付ける」ことができます。

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、このTUstruct 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でもまだ整形式ではありません。


また、それがするのは簡単だと予告与える使用して、名前のタイプにローカル別名をtypedef/をusing。このように解決できることをやろうとしたことから、あなたの質問が生まれたのではないかと思います。

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はデフォルトで初期化された関数ポインタになり、おそらくセグメンテーション違反になります。それは、役に立たないようなものです。


実際、それらが異なるタイプであることを確認する必要がありますが、それは今のところそれほど重要ではありません。」同等に定義されている場合、一意性を強制する正当な理由があるのではないかと思います。
デュプリケータ

個人的には、完全に定義された動作は、(ほぼ?)常に不特定の動作よりも優れていると思います。「これらの2つの関数ポインターは等しいですか?まあ、これら2つのテンプレートのインスタンス化が同じ関数である場合にのみ、これはこれら2つのラムダ型が同じ型である場合にのみ当てはまります。コンパイラーがそれらをマージすることを決定した場合にのみ当てはまります。」イッキー!(ただし、文字列リテラルのマージとまったく同じ状況があり、その状況について誰も混乱してないことに注意してください。したがって、コンパイラが同一の型をマージできるようにすることは壊滅的ではないかと思います。)
Quuxplusone

さて、2つの同等の関数(as-ifを除く)が同一であるかどうかも良い質問です。標準の言語は、自由関数や静的関数ではあまり明白ではありません。しかし、それはここでは範囲外です。
デュプリケータ

偶然にも、今月、LLVMメーリングリストで関数のマージについて議論がありました。Clangのcodegenにより、本体が完全に空の関数がほぼ「偶然に」マージされます。godbolt.org / z / obT55bこれは技術的に不適合であり、LLVMにパッチを適用してこれを停止する可能性が高いと思います。しかし、うん、同意した、関数アドレスのマージも重要です。
Quuxplusone

その例には他の問題があります。つまり、returnステートメントがありません。彼らだけですでにコードが不適合になっているのではないですか?また、私は議論を探しますが、同等の関数のマージが標準、文書化された動作、gccに準拠していないことを示したり、想定したりしましたか、それとも一部の人はそれが起こらないことに依存していますか?
デュプリケータ

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。私の推測では、技術的な理由と同様に、覚えておく必要のあるコーナーケースのセットがさらに多くなるのを避けることと同じくらい関係があります。
カレス

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

それは2つの異なるタイプの(違法な)統一につながるからです。

これを回避するために、ユーザーはを__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 */するのかg2fそれとも他の関数に由来するのか、およびその出所が保持される条件を判別する必要があります。多くの状況で、コンパイラはあきらめる可能性があります。おそらく人間はそれg2fすべての状況から来ていると言うことができますが、からfへのパスg2はコンパイラが解読するには複雑すぎてg2、悲観的なパフォーマンスでへの仮想呼び出しが発生しました。

これは、そのようなオブジェクトがジェネリック(テンプレート)関数に配信されると、より明白になります。

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

h(f)whereを呼び出すとf: __UniqueFunc042hは一意のインスタンスに特化します。

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 - REINSTATEモニカ

3

まず、キャプチャなしのラムダは関数ポインタに変換できます。したがって、それらは何らかの形の汎用性を提供します。

では、キャプチャ付きのラムダがポインターに変換できないのはなぜですか?関数はラムダの状態にアクセスする必要があるため、この状態は関数の引数として表示される必要があります。


さて、キャプチャはラムダ自体の一部になるはずですよね?それらが内にカプセル化されているのと同じようにstd::function<>
cmaster -モニカ復活

3

ユーザーコードとの名前の衝突を避けるため。

同じ実装の2つのラムダでさえ、異なるタイプになります。オブジェクトのメモリレイアウトが同じであっても、オブジェクトのタイプを変えることができるので、これは問題ありません。


のようなタイプでint (*)(Foo*, int, double)は、名前がユーザーコードと衝突するリスクはありません。
cmaster -モニカ復活

あなたの例はあまり一般化されていません。ラムダ式は構文にすぎませんが、特にキャプチャ句を使用すると、いくつかの構造体に評価されます。明示的に名前を付けると、既存の構造体の名前が衝突する可能性があります。
knivil

繰り返しますが、この質問は言語設計に関するものであり、C ++に関するものではありません。ラムダの型がデータ構造型よりも関数ポインター型に似ている言語を確実に定義できます。C ++の関数ポインタ構文とCの動的配列型構文は、これが可能であることを証明しています。そして、それは疑問を投げかけます、なぜラムダは同様のアプローチを使用しなかったのですか?
cmaster -モニカ回復

1
いいえ、カリー化(捕獲)が可変であるため、できません。それを機能させるには、関数とデータの両方が必要です。
ブリンディ

@Blindyああ、はい、できます。ラムダを、キャプチャオブジェクト用とコード用の2つのポインターを含むオブジェクトとして定義できます。このようなラムダオブジェクトは、値で簡単に渡すことができます。または、実際のラムダコードにジャンプする前に、独自のアドレスを取得するキャプチャオブジェクトの開始時にコードのスタブを使用してトリックをプルすることもできます。これにより、ラムダポインターが単一のアドレスに変わります。しかし、PPCプラットフォームが証明しているように、これは不要です。PPCでは、関数ポインターは実際にはポインターのペアです。そのため、標準のC / C ++でキャストvoid(*)(void)void*たり戻したりすることはできません。
cmaster -モニカ復活
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.