C ++の関数内に関数を含めることはできますか?


225

私は次のようなものを意味します:

int main() 
{
  void a() 
  {
      // code
  }
  a();

  return 0;
}

1
なぜこれをやろうとしているのですか?あなたの目的を説明すると、誰かがあなたの目標を達成するための正しい方法を教えてくれるかもしれません。
Thomas Owens

3
gccはネストされた関数を非標準の拡張としてサポートします。ただし、gccを使用している場合でも、使用しないことをお勧めします。C ++モードでは、とにかく利用できません。
Sven Marnach、2010

27
@トーマス:の範囲を減らすのは良いことだから?関数内の関数は、他の言語では通常の機能です。
Johan Kotlinski、2010

64
彼は入れ子関数について話している。クラス内の次のクラスにできるように、彼は関数内に関数をネストしたいと考えています。実際、可能であれば私もそうする状況にあった。これを可能にする言語(F#など)があり、特定のコンテキスト以外では役に立たない多数のヘルパー関数を含むライブラリを汚染することなく、コードをより明確に、読みやすく、保守しやすくすることができます。;)
Mephane

16
@Thomas-ネストされた関数は、現在のスコープを囲んでいるスコープ内では一般的に使用されない関数で満たすことなく、複雑な関数/アルゴリズムを解読するための優れたメカニズムになります。PascalとAdaは(IMO)それらをサポートしています。Scalaや他の多くの古い/新しい尊敬されている言語と同じです。他の機能と同様に、これらも悪用される可能性がありますが、これは開発者の機能です。IMO、彼らは有害であるよりもはるかに有益でした。
luis.espinal 2010

回答:


271

最新のC ++-はい、ラムダで!

現在のバージョンのc ++(C ++ 11、C ++ 14、およびC ++ 17)では、ラムダの形式で関数内に関数を含めることができます。

int main() {
    // This declares a lambda, which can be called just like a function
    auto print_message = [](std::string message) 
    { 
        std::cout << message << "\n"; 
    };

    // Prints "Hello!" 10 times
    for(int i = 0; i < 10; i++) {
        print_message("Hello!"); 
    }
}

ラムダは、**参照によるキャプチャ*を通じてローカル変数を変更することもできます。参照によるキャプチャでは、ラムダはラムダのスコープで宣言されたすべてのローカル変数にアクセスできます。通常は変更および変更できます。

int main() {
    int i = 0;
    // Captures i by reference; increments it by one
    auto addOne = [&] () {
        i++; 
    };

    while(i < 10) {
        addOne(); //Add 1 to i
        std::cout << i << "\n";
    }
}

C ++ 98およびC ++ 03-直接ではなく、ローカルクラス内の静的関数

C ++はそれを直接サポートしていません。

とは言っても、ローカルクラスを持つことができ、それらは関数(- staticまたは以外static)を持つことができるので、これは少し厄介ですが、ある程度拡張することができます。

int main() // it's int, dammit!
{
  struct X { // struct's as good as class
    static void a()
    {
    }
  };

  X::a();

  return 0;
}

しかし、私は実際に質問します。:)C ++はローカル関数をサポートしていないので、誰もが知っている(さて、あなたがそうしたようになりました)。しかし、それらはその汚物に使用されていません。私はこのコードにかなりの時間をかけて、ローカル関数を許可するためだけに存在することを確認しました。良くない。


3
Mainは、戻り値の型に慣れるつもりであれば、2つの引数も受け取ります。:)(または、それはオプションですが、最近のリターンではありませんか?私は追いつくことができません。)
レオデビッドソン

3
これはただ悪いことです-それは良い、きれいなコードのあらゆる慣習を破ります。これが良い考えである単一のインスタンスを考えることはできません。
Thomas Owens

19
@Thomas Owens:コールバック関数が必要で、それで他の名前空間を汚染したくない場合は、良いことです。
Leo Davidson

9
:標準は、メイン用の2つの許容フォームがあると言う:@Leo int main()int main(int argc, char* argv[])
ジョンDiblingは

8
標準は言うしint main()int main(int argc, char* argv[])サポートする必要があります、他の人がサポートされる可能性がありますが、それらはすべてintを返します。
JoeG

260

すべての意図と目的のために、C ++はラムダを介してこれをサポートします1

int main() {
    auto f = []() { return 42; };
    std::cout << "f() = " << f() << std::endl;
}

ここでは、fローカル関数のように動作するラムダオブジェクトですmain。キャプチャを指定して、関数がローカルオブジェクトにアクセスできるようにすることができます。

舞台裏にf関数オブジェクト(つまり、を提供するタイプのオブジェクトoperator())があります。関数オブジェクトタイプは、ラムダに基づいてコンパイラによって作成されます。


C ++ 11以降1


5
ああ、それは素晴らしいです!思わなかった。これは私の考えよりもはるかに優れ+1ています。
sbi

1
@sbi:私は実際に過去にこれをシミュレートするためにローカルの構造体を使用しました(そうです、私は自分自身を恥じています)。しかし、ローカル構造体はクロージャを作成しない、つまりローカル構造体のローカル変数にアクセスできないという事実により、有用性は制限されます。コンストラクタを介して明示的に渡し、保存する必要があります。
Konrad Rudolph

1
@Konrad:それらに関するもう1つの問題は、C ++ 98ではローカルタイプをテンプレートパラメータとして使用してはならないことです。C ++ 1xはその制限を解除したと思います。(または、C ++ 03でしたか?)
sbi

3
@luis:私はフレッドに同意する必要があります。ラムダに単純にない意味を付けます(C ++でも、私が使用した他の言語でも、レコードにはPythonとAdaが含まれていません)。さらに、C ++にはローカル関数であるピリオドがないため、C ++ではその区別は意味がありません。ラムダしかありません。関数のようなもののスコープを関数に制限したい場合、唯一の選択肢はラムダか、他の回答で言及されているローカル構造体です。後者は、あまりに複雑すぎて、実際に関心を持つことはできないと思います。
Konrad Rudolph

2
@AustinWBryanいいえ、C ++のラムダはファンクタの構文糖であり、オーバーヘッドはありません。このウェブサイトのどこかにもっと詳しい質問があります。
Konrad Rudolph

42

ローカルクラスについては既に説明しましたが、operator()オーバーロードと匿名クラスを使用して、ローカルクラスとしてローカルクラスを表示する方法を次に示します。

int main() {
    struct {
        unsigned int operator() (unsigned int val) const {
            return val<=1 ? 1 : val*(*this)(val-1);
        }
    } fac;

    std::cout << fac(5) << '\n';
}

私はこれを使用することについて助言しません、それはただおかしいトリックです(することができますが、imhoはすべきではありません)。


2014年の更新:

しばらく前にC ++ 11が登場したため、JavaScriptを少し連想させる構文のローカル関数を使用できるようになりました。

auto fac = [] (unsigned int val) {
    return val*42;
};

1
でなければなりませんoperator () (unsigned int val)。括弧が欠けています。
Joe D

1
実際、これは、このファンクタをstl関数またはアルゴリズム(やなどstd::sort())に渡す必要がある場合に実行するのに完全に合理的な方法std::for_each()です。
ディマ

1
@Dima:残念ながら、C ++ 03では、ローカルで定義された型はテンプレート引数として使用できません。C ++ 0xはこれを修正しますが、ラムダのはるかに優れたソリューションも提供するため、それを行うことはできません。
Ben Voigt 2010

おっと、あなたは正しいです。私の悪い。しかし、それでも、これは単なる面白いトリックではありません。それが許されればそれは有用なものだっただろう。:)
Dima

3
再帰がサポートされています。ただし、auto変数の宣言には使用できません。Stroustrupが例を示しfunction<void(char*b, char*e)> rev=[](char*b, char*e) { if( 1<e-b ) { swap( *b, *--e); rev(++b,e); } };ます。開始ポインタと終了ポインタが指定された文字列を逆にします。
代名詞、

17

番号。

あなたは何をしようとしているのですか?

回避策:

int main(void)
{
  struct foo
  {
    void operator()() { int a = 1; }
  };

  foo b;
  b(); // call the operator()

}

2
クラスのインスタンス化アプローチにはメモリ割り当てが付属しているため、静的アプローチが支配的であることに注意してください。
ManuelSchneid3r

14

C ++ 11以降では、適切なラムダを使用できます。詳細については、他の回答を参照してください。


古い答え:並べ替えることはできますが、だましてダミークラスを使用する必要があります。

void moo()
{
    class dummy
    {
    public:
         static void a() { printf("I'm in a!\n"); }
    };

    dummy::a();
    dummy::a();
}

代わりにオブジェクトを作成する(それと同じくらい多くのノイズIMOを追加する)ことを除いて、できるかわかりません。名前空間でできる賢いことがなければ、私はそれを考えることはできませんし、すでにある以上に言語を悪用することはおそらく良い考えではありません。:)
Leo Davidson、

Getting-rid-of-dummy ::は他の回答の1つにあります。
セバスチャンマッハ

8

他の人が述べたように、gccのgnu言語拡張を使用することにより、ネストされた関数を使用できます。あなた(またはあなたのプロジェクト)がgccツールチェーンを使用している場合、コードはgccコンパイラーがターゲットとするさまざまなアーキテクチャー間でほとんど移植可能です。

ただし、別のツールチェーンを使用してコードをコンパイルする必要がある可能性があるという要件がある場合は、そのような拡張は避けます。


ネストされた関数を使用するときも注意して踏みます。これらは、複雑であるがまとまりのあるコードブロックの構造を管理するための美しいソリューションです(その一部は外部/一般的な使用を目的としていません)。名前空間汚染の制御にも非常に役立ちます(自然に複雑な/冗長クラスの長いクラス。)

しかし、何でもそうであるように、彼らは虐待を受ける可能性があります。

C / C ++がそのような機能を標準としてサポートしていないのは残念です。ほとんどのPascalバリアントとAdaが行います(ほとんどすべてのAlgolベースの言語が行います)。JavaScriptも同様です。Scalaのような最新の言語でも同じです。Erlang、Lisp、Pythonなどの由緒ある言語でも同じです。

そして、C / C ++と同様に、残念ながらJava(私が私の生活のほとんどを稼いでいます)はそうではありません。

ネストされた関数の代わりにクラスとクラスのメソッドの使用を提案するポスターをいくつか目にしているので、ここでJavaについて触れます。そして、それはJavaの典型的な回避策でもあります。

短い答え:いいえ。

そうすることは、クラス階層に人為的で不必要な複雑さをもたらす傾向があります。すべてが等しい場合、理想は、実際のドメインをできるだけ単純に表すクラス階層(およびその包括的名前空間とスコープ)を持つことです。

ネストされた関数は、関数内の複雑な「プライベート」の処理に役立ちます。それらの機能がないため、その「プライベート」な複雑さをクラスモデルに伝搬させないようにする必要があります。

ソフトウェア(および任意のエンジニアリング分野)では、モデリングはトレードオフの問題です。したがって、実際には、これらのルール(またはガイドライン)には正当化される例外があります。ただし、注意して続行してください。


8

C ++ではローカル関数を使用できません。ただし、C ++ 11にはラムダがあります。ラムダは基本的に関数のように機能する変数です。

ラムダにはタイプがありますstd::function実際にはそうはありませんが、ほとんどの場合はそうです)。このタイプを使用するには、以下を行う必要があり#include <functional>ます。std::functionはテンプレートで、構文を使用して、戻り値の型と引数の型をテンプレート引数として受け取りますstd::function<ReturnType(ArgumentTypes)。例えば、std::function<int(std::string, float)>ラムダ戻っているintと二つの引数、1服用std::stringして1をfloat。最も一般的なのはでstd::function<void()>、何も返さず、引数も取りません。

ラムダが宣言されると、構文を使用して通常の関数と同じように呼び出されますlambda(arguments)

ラムダを定義するには、構文を使用します[captures](arguments){code}(他の方法もありますが、ここでは触れません)。argumentsラムダが取る引数でありcode、ラムダが呼び出されたときに実行されるコードです。通常あなたは置く[=][&]、またはキャプチャとして。[=]値によって値が定義されているスコープ内のすべての変数をキャプチャすることを意味します。つまり、ラムダが宣言されたときの値を保持します。[&]スコープ内のすべての変数を参照によってキャプチャすることを意味します。つまり、変数には常に現在の値が含まれますが、それらがメモリから消去されると、プログラムがクラッシュします。ここではいくつかの例を示します。

#include <functional>
#include <iostream>

int main(){
    int x = 1;

    std::function<void()> lambda1 = [=](){
        std::cout << x << std::endl;
    };
    std::function<void()> lambda2 = [&](){
        std::cout << x << std::endl;
    };

    x = 2;
    lambda1();    //Prints 1 since that was the value of x when it was captured and x was captured by value with [=]
    lambda2();    //Prints 2 since that's the current value of x and x was captured by value with [&]

    std::function<void()> lambda3 = [](){}, lambda4 = [](){};    //I prefer to initialize these since calling an uninitialized lambda is undefined behavior.
                                                                 //[](){} is the empty lambda.

    {
        int y = 3;    //y will be deleted from the memory at the end of this scope
        lambda3 = [=](){
            std::cout << y << endl;
        };
        lambda4 = [&](){
            std::cout << y << endl;
        };
    }

    lambda3();    //Prints 3, since that's the value y had when it was captured

    lambda4();    //Causes the program to crash, since y was captured by reference and y doesn't exist anymore.
                  //This is a bit like if you had a pointer to y which now points nowhere because y has been deleted from the memory.
                  //This is why you should be careful when capturing by reference.

    return 0;
}

名前を指定して、特定の変数をキャプチャすることもできます。それらの名前を指定するだけで値によってそれらをキャプチャし、&beforeでそれらの名前を指定すると参照によってそれらをキャプチャします。たとえば、[=, &foo]foo、参照によってキャプチャされる変数を除くすべての変数を値によってキャプチャし、値によってキャプチャされる[&, foo]変数を除くすべての変数を参照fooによってキャプチャします。あなたはまた、例えば、特定の変数、キャプチャすることができます[&foo]キャプチャしますfoo参照することによって、および他の変数をキャプチャしませんが。を使用して、変数をまったくキャプチャしないこともできます[]。キャプチャしなかったラムダ内の変数を使用しようとすると、コンパイルされません。次に例を示します。

#include <functional>

int main(){
    int x = 4, y = 5;

    std::function<void(int)> myLambda = [y](int z){
        int xSquare = x * x;    //Compiler error because x wasn't captured
        int ySquare = y * y;    //OK because y was captured
        int zSquare = z * z;    //OK because z is an argument of the lambda
    };

    return 0;
}

ラムダ内の値によってキャプチャされた変数の値を変更することはできません(値によってキャプチャされた変数にはconst、ラムダ内の型があります)。これを行うには、変数を参照でキャプチャする必要があります。次に例を示します。

#include <functional>

int main(){
    int x = 3, y = 5;
    std::function<void()> myLambda = [x, &y](){
        x = 2;    //Compiler error because x is captured by value and so it's of type const int inside the lambda
        y = 2;    //OK because y is captured by reference
    };
    x = 2;    //This is of course OK because we're not inside the lambda
    return 0;
}

また、初期化されていないラムダの呼び出しは未定義の動作であり、通常はプログラムがクラッシュします。たとえば、これを絶対に行わないでください。

std::function<void()> lambda;
lambda();    //Undefined behavior because lambda is uninitialized

ラムダを使用して質問で実行したいコードを次に示します。

#include <functional>    //Don't forget this, otherwise you won't be able to use the std::function type

int main(){
    std::function<void()> a = [](){
        // code
    }
    a();
    return 0;
}

ラムダのより高度な例を次に示します。

#include <functional>    //For std::function
#include <iostream>      //For std::cout

int main(){
    int x = 4;
    std::function<float(int)> divideByX = [x](int y){
        return (float)y / (float)x;    //x is a captured variable, y is an argument
    }
    std::cout << divideByX(3) << std::endl;    //Prints 0.75
    return 0;
}

7

いいえ、許可されていません。CもC ++もデフォルトではこの機能をサポートしていませんが、TonyKは(コメントの中で)Cでこの動作を可能にするGNU Cコンパイラの拡張機能があることを指摘しています。


2
特別な拡張機能として、GNU Cコンパイラでサポートされています。しかし、C ++ではなくC専用です。
TonyK、2010

ああ。Cコンパイラに特別な拡張機能はありません。しかし、それは知っておくと役に立ちます。そのtitbitを私の答えに追加します。
Thomas Owens、2010

ネストされた関数をサポートするためにgcc拡張機能を使用しました(ただし、C ++ではなくCで)。ネストされた関数は、(PascalやAdaの場合のように)複雑でありながらまとまりのある構造を管理するための(PascalやAdaのように)気の利いたものであり、一般的に使用することを意図していません。gccツールチェーンを使用している限り、ほとんどすべてのターゲットアーキテクチャに移植できることが保証されます。しかし、結果のコードをgcc以外のコンパイラでコンパイルする必要があるという変更がある場合は、そのような拡張を避け、ansi / posixのマントラにできるだけ近づけることが最善です。
luis.espinal 2010

7

このすべてのトリックは、ローカル関数のように見えますが(多かれ少なかれ)、そのようには機能しません。ローカル関数では、そのスーパー関数のローカル変数を使用できます。セミグローバルのようなものです。これらのトリックのどれもそれを行うことができません。最も近いのはc ++ 0xのラムダトリックですが、そのクロージャは使用時間ではなく定義時間にバインドされています。


これが最良の答えだと思います。関数内で関数を宣言することは可能ですが(これは常に使用します)、他の多くの言語で定義されているローカル関数ではありません。それでも可能性を知っておくのは良いことです。
Alexis Wilke


4

可能な限りクリーンだと考えるC ++ 03の解決策をここに投稿しましょう。*

#define DECLARE_LAMBDA(NAME, RETURN_TYPE, FUNCTION) \
    struct { RETURN_TYPE operator () FUNCTION } NAME;

...

int main(){
  DECLARE_LAMBDA(demoLambda, void, (){ cout<<"I'm a lambda!"<<endl; });
  demoLambda();

  DECLARE_LAMBDA(plus, int, (int i, int j){
    return i+j;
  });
  cout << "plus(1,2)=" << plus(1,2) << endl;
  return 0;
}

(*)C ++の世界では、マクロを使用することは決してクリーンとは見なされません。


アレクシス、それは完全にきれいではないと言うのは正しいです。副作用なしにプログラマーが何をするつもりであったかをよく表すので、それはまだきれいに近いです。プログラミングの芸術は、人間が読めるように、小説のように読める表現を書くことだと思います。
Barney

2

しかし、main()内で関数を宣言できます。

int main()
{
    void a();
}

構文は正しいですが、「最も厄介な解析」につながる場合があります。

#include <iostream>


struct U
{
    U() : val(0) {}
    U(int val) : val(val) {}

    int val;
};

struct V
{
    V(U a, U b)
    {
        std::cout << "V(" << a.val << ", " << b.val << ");\n";
    }
    ~V()
    {
        std::cout << "~V();\n";
    }
};

int main()
{
    int five = 5;
    V v(U(five), U());
}

=>プログラム出力はありません。

(コンパイル後のClang警告のみ)。

C ++の最も厄介な構文解析

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.