C++入門/制御の発展と名前空間

C++ には変数の存在場所という概念がある.違う場所にある変数を別の場所から参照することができない,という風に機能する.この「場所」はスコープや名前空間と呼ばれ,{ } により指定することができる.以下に例を示す.

#include <iostream>

int main(){
    int a = 1;

    {
        int c = 1 + a;
        std::cout << c << '\n';
    }

    std::cout << a << '\n';
    std::cout << c << '\n'; // 不可
    return 0;
}

この例では,a は一番外側の名前空間({ })に定義されているが,c は一つ内側の { } の中に定義されている.内側に定義された { } の中から,外側に定義されたものを参照することは可能である.これにより,8行目の外側の変数 a を使った c の初期化が問題なくコンパイラに解釈される.一方で,外側の { } の中で,内側の { } で定義された c を参照しようとする 13 行目の文は,コンパイルに失敗する.このコードはこの失敗する文を含んでいることから,13行目を消さない限りコンパイルができない.このように,名前空間には上位下位といった階層関係が存在する.

以下の記載におけるdが宣言されている{ } の部分は意味をなさない.a の定義されている6行目から15行目までが実行される部分であり,最上位階層であるが,これと同じ階層にあるこの空間の変数はプログラムから参照することができないからである.実際これはコンパイルに失敗する.

#include <iostream>

{
    int d = 0;
}

int main(){
    int a = 1;

    {
        int c = 1 + a;
        std::cout << c << '\n';
    }

    std::cout << a << '\n';
    return 0;
}

この空間を利用可能とする方法は2つある.一つが,名前をつけることである.人は同じ名前の人間がいたとしても,例えば,鈴木ジャッキーと山田ジャッキー(この組み合わせの方はいないだろうと思うが,いたら申し訳ない)という二名がいたとして,家の中ではどちらもジャッキーと呼ばれているとする.だが,家の外では,鈴木ジャッキーと山田ジャッキーと読んで,両名を区別する必要があるだろう.これと同じ方法がC++にも用意されている.姓に相当するものは,namepace という識別子で定義する.フルネーム(姓名)で変数を参照するときは,姓::名前,のように書く.この姓に当たる部分を,名前空間と呼ぶ.具体例を次に示す.

#include <iostream>

namespace A
{
    int d = 0;
}

int main(){
    int d = 1;
    std::cout << A::d << '\n';
    std::cout << d << '\n';
    return 0;
}

実行結果を以下に示す.同じ名前のdという変数が名前空間を指定することで区別できていることがわかる.

hiroki@hiroki-home:~/git/cpp$ g++ c.cpp hiroki@hiroki-home:~/git/cpp$ ./a.out 9122 1271

C++には処理範囲の定義にも { } を使う.すでに制御のwhile や if で例を説明したが,namespace と同じように,最上位階層に処理範囲を定義して名前をつけることができる.つけた名前を使って,どの段階からもその処理範囲へ制御を移動することができる.これにより,順番に文を処理していくという原則であるC++の基本制御だけでなく,より柔軟な制御が可能となる.以下に具体的なコードと実行結果を示す.

#include <iostream>

namespace A
{
    int d = 1;
}

int F()
{
    int k = 1;
    A::d = A::d + 1;
    return 0;
}

int main(){
    int d = 1;
    F();
    std::cout << A::d << ' ' << d << '\n';
    F();
    std::cout << A::d << ' ' << d << '\n';
    return 0;
}
hiroki@hiroki-home:~/git/cpp$ g++ c.cpp hiroki@hiroki-home:~/git/cpp$ ./a.out 2 1 3 1

d という変数が2箇所に定義されている.また,F の範囲とAの範囲の2つがある.() をつけることで,単なる変数の宣言だけでなく,処理をここに転送することを意味する.ただ,() をつけた場合は F::k のようにその中で定義された変数を外部から参照することはできない.namespace と記載した場合は,逆に外部から参照できる代わりに,処理を受けることはできない.また,() により宣言された範囲は,制御が回ってくる場所で F(); のように記述すれば,制御を F の範囲へ転送することができる.最後の return 0 と記載された部分に達すると,制御は元の F() の文の直後の文へ移行する.Fの方は仕様上は名前空間と呼ばず,関数と呼ぶ.名前空間は制御のような順序が定義さないことから名前空間と異なるため区別しているのだろう.どちらの範囲にも使える言葉として「スコープ」がよく使われる.日本語で言えば範囲である.

この制御の転送はF();を記載するたびに何度も行うことができる.この例では,F(); が二回記載されているため,A::d の値が 11 行目の文により2増えるはずである.16行目に定義された d は A::d とは異なるから,F() を処理した影響は受けないはずで,結果からそのとおりになっていることを確認できる.

ここで,main() というものについて説明をする.この main の範囲は,プログラム起動時に最初に実行されるとC++の仕様で定められている.他にいくら関数(処理範囲)が書かれていても,コンパイラはmainを探し出して,そこを始めに実行するコードを生成することとなっている.

std::cout というのも,namespace std がどこかに宣言されているため,この記法が使える,などと想像しておいてもらいたい.C++ が提供する基幹機能は,他のプログラム作成者が定義した変数と混同されては困るため,stdという名前空間に定義されていて,基本的にはフルネームでstd::cout と指定するという決まりになっている.

名前空間は階層的に定義することが可能である.例えば,以下のようにコードを作成すれば,F の中で Yamada:: とわざわざ記載する必要はない.もちろん,鈴木家のなかで鈴木ジャッキーとフルネームで呼ぶことも不可能ではないのと同じで,書いてもいい.

#include <iostream>

namespace Yamada
{
    int d = 1;
    int F()
    {
        int k = 1;
        Yamada::d = Yamada::d + k;
        d = d + k; // Yamada の下に定義された空間なので,Yamada::を省略可能
        return 0;
    }
}


int main(){
    int d = 1;
    Yamada::F();
    std::cout << Yamada::d << ' ' << d << '\n';
    Yamada::F();
    std::cout << Yamada::d << ' ' << d << '\n';
    return 0;
}

用語を以下にまとめる.

一応述べておくが,名前空間や関数の名前はいくつの文字を使って構成してもいい.上記例では Yamada にしてみている. A::などと書くのが面倒だと感じる方は,名前空間の詳細を先に確認すると良いだろう.