web-dev-qa-db-ja.com

別の関数内で関数を定義できないのはなぜですか?

これはラムダ関数の質問ではなく、ラムダを変数に割り当てることができることを知っています。

コード内で関数を宣言することはできますが、定義することはできません。

例えば:

#include <iostream>

int main()
{
    // This is illegal
    // int one(int bar) { return 13 + bar; }

    // This is legal, but why would I want this?
    int two(int bar);

    // This gets the job done but man it's complicated
    class three{
        int m_iBar;
    public:
        three(int bar):m_iBar(13 + bar){}
        operator int(){return m_iBar;}
    }; 

    std::cout << three(42) << '\n';
    return 0;
}

だから私が知りたいのは、なぜC++がtwoを許可しないのか、threeははるかに複雑に見えるが、oneを許可しないのか?

編集:

答えから、コード内宣言があると名前空間の汚染を防ぐことができるように思えますが、私が聞きたいのは、関数を宣言する機能は許可されているが、関数を定義する機能は許可されていない理由です。

77
Jonathan Mee

oneが許可されない理由は明らかではありません。ネストされた関数は、かなり前に N0295 で提案されました。

ネストされた関数のC++への導入について説明します。入れ子関数はよく理解されており、その導入にはコンパイラベンダー、プログラマー、または委員会のいずれの努力もほとんど必要ありません。入れ子関数は大きな利点を提供します、[...]

明らかにこの提案は拒否されましたが、会議議事録がオンラインで利用できないため、1993この拒否の理由のソースはありません。

実際、この提案はC++のラムダ式とクロージャに可能な代替として記載されています:

1つの記事[Bre88]およびC++委員会への提案N0295 [SH93]は、ネストされた関数をC++に追加することを提案しています。入れ子関数はラムダ式に似ていますが、関数本体内のステートメントとして定義され、その関数がアクティブでない限り、結果のクロージャーは使用できません。これらの提案には、各ラムダ式に新しい型を追加することも含まれていませんが、代わりに、特別な種類の関数ポインターがそれらを参照できるようにするなど、通常の関数のように実装します。これらの提案はどちらも、C++にテンプレートを追加する以前のものであるため、汎用アルゴリズムと組み合わせたネストされた関数の使用については言及していません。また、これらの提案にはローカル変数をクロージャーにコピーする方法がないため、それらが生成するネストされた関数は、それらを囲む関数の外では完全に使用できません。

ラムダがあると考えると、ネストされた関数を見る可能性は低いと思われます。これは、論文の概要からわかるように、同じ問題の代替であり、ネストされた関数にはラムダに関する制限がいくつかあるためです。

あなたの質問のこの部分に関して:

// This is legal, but why would I want this?
int two(int bar);

これは、必要な関数を呼び出す便利な方法になる場合があります。ドラフトC++標準セクション3.4.1[basic.lookup.unqual]は興味深い例を示します。

namespace NS {
    class T { };
    void f(T);
    void g(T, int);
}

NS::T parm;
void g(NS::T, float);

int main() {
    f(parm); // OK: calls NS::f
    extern void g(NS::T, float);
    g(parm, 1); // OK: calls g(NS::T, float)
}
40
Shafik Yaghmour

まあ、答えは「歴史的理由」です。 Cでは、ブロックスコープで関数宣言を行うことができますが、C++デザイナーはそのオプションを削除する利点を認識していませんでした。

使用例は次のとおりです。

#include <iostream>

int main()
{
    int func();
    func();
}

int func()
{
    std::cout << "Hello\n";
}

IMOこれは、関数の実際の定義と一致しない宣言を提供することで間違いを犯しやすく、コンパイラによって診断されない未定義の動作につながるため、悪い考えです。

31
M.M

あなたが与える例では、void two(int)は外部関数として宣言されており、その宣言main関数のスコープ内でのみ有効です。

名前twomain()内でのみ使用可能にして、現在のコンパイル単位内のグローバル名前空間を汚染しないようにする場合、これは妥当です。

コメントへの応答の例:

main.cpp:

int main() {
  int foo();
  return foo();
}

foo.cpp:

int foo() {
  return 0;
}

ヘッダーファイルは必要ありません。コンパイルしてリンク

c++ main.cpp foo.cpp 

コンパイルして実行され、プログラムは期待どおりに0を返します。

23
Richard Hodges

これらのことを行うことができるのは、主に、それほど難しくないからです。

コンパイラの観点からは、別の関数内に関数宣言を持つことは実装するのが非常に簡単です。コンパイラには、とにかく関数内の宣言が関数内の他の宣言(_int x;_など)を処理できるようにするメカニズムが必要です。

通常、宣言を解析するための一般的なメカニズムがあります。コンパイラを書いている人にとって、そのメカニズムが別の関数の内部または外部でコードを解析するときに呼び出されるかどうかはまったく関係ありません-それは単なる宣言ですので、宣言があることを知るのに十分なときは、宣言を処理するコンパイラの一部を呼び出します。

実際、関数内でこれらの特定の宣言を禁止すると、おそらく余分な複雑さが追加されます。コンパイラは、関数定義内のコードを既に見ているかどうかを確認するために完全に無償のチェックを必要とし、それに基づいてこの特定の許可または禁止を決定するためです宣言。

ネストされた関数がどのように異なるのかという疑問が残ります。ネストされた関数は、コード生成にどのように影響するかによって異なります。ネストされた関数(Pascalなど)を許可する言語では、通常、ネストされた関数のコードが、ネストされた関数の変数に直接アクセスできることを期待しています。例えば:

_int foo() { 
    int x;

    int bar() { 
        x = 1; // Should assign to the `x` defined in `foo`.
    }
}
_

ローカル関数がなければ、ローカル変数にアクセスするコードは非常に簡単です。典型的な実装では、実行が関数に入ると、ローカル変数用のスペースのブロックがスタックに割り当てられます。すべてのローカル変数はその単一のブロックに割り当てられ、各変数はブロックの先頭(または末尾)からの単なるオフセットとして扱われます。たとえば、次のような関数を考えてみましょう。

_int f() { 
   int x;
   int y;
   x = 1;
   y = x;
   return y;
}
_

コンパイラー(余分なコードを最適化しないと仮定した場合)は、これとほぼ同等のコードを生成します:

_stack_pointer -= 2 * sizeof(int);      // allocate space for local variables
x_offset = 0;
y_offset = sizeof(int);

stack_pointer[x_offset] = 1;                           // x = 1;
stack_pointer[y_offset] = stack_pointer[x_offset];     // y = x;
return_location = stack_pointer[y_offset];             // return y;
stack_pointer += 2 * sizeof(int);
_

特に、ローカル変数のブロックの先頭を指すoneロケーションを持ち、ローカル変数へのすべてのアクセスは、そのロケーションからのオフセットとして行われます。

ネストされた関数では、そうではありません。代わりに、関数は自身のローカル変数だけでなく、ネストされているすべての関数のローカル変数にもアクセスできます。 1つの「stack_pointer」からオフセットを計算する代わりに、スタックをさかのぼって、ネストされている関数にローカルなstack_pointersを見つける必要があります。

さて、ささいなケースでもそれほどひどいわけではありません-barfoo内にネストされている場合、barは前のスタックポインターでスタックを検索できますfooの変数にアクセスします。右?

間違っています!まあ、これは真実である場合もありますが、必ずしもそうではありません。特に、barは再帰的である可能性があります。その場合、barの特定の呼び出しは、周囲の関数の変数を見つけるために、スタックをバックアップするほぼ任意の数のレベルを探す必要があります。一般的に、次の2つのいずれかを実行する必要があります:スタックに余分なデータを配置して、実行時にスタックを検索して周囲の関数のスタックフレームを見つけるか、または効果的にネストされた関数の非表示パラメーターとしての周囲の関数のスタックフレーム。ああ、しかし、必ずしも1つの周囲の関数だけではありません。関数をネストできる場合、おそらく多かれ少なかれそれらを任意の深さでネストできるため、任意の数の非表示パラメーターを渡す準備ができている必要があります。つまり、通常、周囲の関数へのスタックフレームのリンクリストのようなものになり、周囲の関数の変数へのアクセスは、そのリンクリストを歩いてスタックポインターを見つけ、そのスタックポインターからのオフセットにアクセスすることで行われます。

ただし、これは、「ローカル」変数へのアクセスが些細な問題ではないことを意味します。変数にアクセスするための正しいスタックフレームを見つけるのは簡単ではないため、周囲の関数の変数へのアクセスは、(少なくとも通常)真のローカル変数へのアクセスよりも遅くなります。そして、もちろん、コンパイラは適切なスタックフレームを見つけるためのコードを生成し、任意の数のスタックフレームのいずれかを介して変数にアクセスする必要があります。

Thisは、ネストされた関数を禁止することでCが回避していた複雑さです。さて、現在のC++コンパイラが1970年代のビンテージCコンパイラとはかなり異なる種類の獣であることは確かです。複数の仮想継承のようなものを使用すると、C++コンパイラは、どのような場合でもこれと同じ一般的な性質のものを処理する必要があります(つまり、そのような場合にベースクラス変数の場所を見つけることも重要です)。割合に基づいて、ネストされた関数をサポートしても、現在のC++コンパイラにそれほど複雑さを加えることはありません(gccなどの一部は既にサポートしています)。

同時に、それはめったに多くのユーティリティを追加しません。特に、関数内の関数のようなactsを定義したい場合は、ラムダ式を使用できます。これが実際に作成するのは、関数呼び出し演算子(operator())をオーバーロードするオブジェクト(つまり、あるクラスのインスタンス)ですが、それでも関数のような機能を提供します。ただし、周囲のコンテキストからデータをキャプチャする(またはキャプチャしない)ようにすることで、まったく新しいメカニズムとその使用法のセットを考案するのではなく、既存のメカニズムを使用できるようにします。

結論:ネストされた宣言は難しいように見え、ネストされた関数は自明であるように見えますが、多かれ少なかれその逆です:ネストされた関数は、ネストされた宣言よりもサポートするのがはるかに複雑です。

18
Jerry Coffin

最初のものは関数定義であり、許可されていません。明らかに、wtは、関数の定義を別の関数内に配置する使用法です。

しかし、他の2つは単なる宣言です。 mainメソッド内でint two(int bar);関数を使用する必要があると想像してください。ただし、この関数はmain()関数の下で定義されているため、関数内の関数宣言により、その関数を宣言で使用できます。

同じことが3番目にも当てはまります。関数内のクラス宣言により、適切なヘッダーまたは参照を提供せずに関数内でクラスを使用できます。

int main()
{
    // This is legal, but why would I want this?
    int two(int bar);

    //Call two
    int x = two(7);

    class three {
        int m_iBar;
        public:
            three(int bar):m_iBar(13 + bar) {}
            operator int() {return m_iBar;}
    };

    //Use class
    three *threeObj = new three();

    return 0;
}
5
ANjaNA

この言語機能はCから継承されたもので、Cの初期の頃には何らかの目的を果たしていました(多分関数宣言のスコープ??)。この機能が現代のCプログラマーによって多く使用されているかどうかはわかりませんが、心から疑います。

したがって、答えをまとめると:

modernC++(少なくとも私が知っていること)には、この機能の目的はありません。これは、C++からCへの下位互換性のためです。 (私は考えます :) )。


以下のコメントのおかげで:

関数プロトタイプは、それが宣言された関数にスコープされるため、#includeなしで外部関数/シンボルを参照することで、よりきれいなグローバル名前空間を持つことができます。

4
mr.pd

実際、おそらく有用なユースケースが1つあります。特定の関数が呼び出されること(およびコードがコンパイルされること)を確認したい場合は、周囲のコードが宣言するものに関係なく、独自のブロックを開き、その中で関数プロトタイプを宣言できます。 (インスピレーションはもともとヨハネス・シャウブ、 https://stackoverflow.com/a/929902/3150802 からTeKa経由、 https://stackoverflow.com/a/8821992/3150802 )。

これは、制御しないヘッダーを含める必要がある場合や、不明なコードで使用される可能性のある複数行マクロがある場合に特に便利です。

重要なのは、ローカル宣言が最も内側の囲みブロック内の以前の宣言に優先することです。それは微妙なバグを引き起こす可能性がありますが(C#では禁止されていると思います)、意識的に使用することができます。考慮してください:

_// somebody's header
void f();

// your code
{   int i;
    int f(); // your different f()!
    i = f();
    // ...
}
_

ヘッダーはライブラリに属する​​可能性があるため、リンクは興味深いかもしれませんが、ライブラリが考慮されるまでにf()が関数に解決されるようにリンカー引数を調整できると思います。または、重複するシンボルを無視するように指示します。または、ライブラリにリンクしません。

4

これは、OPの質問に対する答えではなく、いくつかのコメントへの返信です。

私はコメントと回答のこれらの点に同意しません:1ネストされた宣言は無害であると言われ、2ネストされた定義は役に立ちません。

1ネストされた関数宣言の無害性の主な反例は、 悪名高い最も厄介な解析 です。 IMOによって引き起こされる混乱の広がりは、ネストされた宣言を禁止する特別なルールを保証するのに十分です。

2ネストされた関数定義の役に立たないという最初の反例は、1つの関数内の複数の場所で同じ操作を頻繁に実行する必要があることです。これには明らかな回避策があります。

private:
inline void bar(int abc)
{
    // Do the repeating operation
}

public: 
void foo()
{
    int a, b, c;
    bar(a);
    bar(b);
    bar(c);
}

ただし、このソリューションは、多くのプライベート関数でクラス定義を汚染することがよくあり、各関数はそれぞれ1つの呼び出し元で使用されます。ネストされた関数宣言は、はるかにきれいです。

3
Michael

この質問への具体的な回答:

答えから、コード内宣言があると名前空間の汚染を防ぐことができるように思えますが、私が聞きたいのは、関数を宣言する機能は許可されているが、関数を定義する機能は許可されていない理由です。

このコードを考慮してください:

_int main()
{
  int foo() {

    // Do something
    return 0;
  }
  return 0;
}
_

言語デザイナーへの質問:

  1. foo()を他の関数で使用できるようにする必要がありますか?
  2. もしそうなら、その名前は何ですか? int main(void)::foo()
  3. (C++では、C++では2が使用できないことに注意してください)
  4. ローカル関数が必要な場合、既に方法があります-ローカル定義クラスの静的メンバーにします。それでは、同じ結果を得るための別の構文方法を追加する必要がありますか?どうしてですか? C++コンパイラ開発者のメンテナンスの負担が増えませんか?
  5. 等々...
2
Richard Hodges

GCCコンパイラを使用すると、関数内で関数を宣言できることを指摘したかっただけです。詳細については here を参照してください。また、C++への lambdas の導入により、この質問は少し時代遅れになりました。


他の関数内で関数ヘッダーを宣言する機能は、次の場合に役立ちます。

void do_something(int&);

int main() {
    int my_number = 10 * 10 * 10;
    do_something(my_number);

    return 0;
}

void do_something(int& num) {
    void do_something_helper(int&); // declare helper here
    do_something_helper(num);

    // Do something else
}

void do_something_helper(int& num) {
    num += std::abs(num - 1337);
}

ここには何がありますか?基本的に、mainから呼び出されることになっている関数があるので、通常のように前方宣言するだけです。しかし、この機能には、その機能を支援する別の機能も必要です。そのため、メインの上のヘルパー関数を宣言するのではなく、それを必要とする関数内で宣言すると、その関数とその関数からのみ呼び出すことができます。

私のポイントは、関数内の関数ヘッダーを宣言することは、関数カプセル化の間接的な方法になる可能性があることです。これにより、関数は、それだけが知っている他の関数に委任することで、ほとんどネストされた関数の錯覚

1
smac89

ネストされた関数の宣言は、おそらく1に対して許可されています。

ネストされた関数の定義は、おそらく1のような問題のため許可されません。

私の限られた理解から:)

0
NeutronStar