Article >

C++0x 改め C++11 はじめの一歩   2011 / 8/ 14

 2011年8月12日、これまで C++0x と呼ばれていた C++ の次期規格が C++11 (正式名称: ISO/IEC 14882:2011) と改まり、国際標準として承認されました。C++11 では 従来の C++ (2003年に策定されたC++03 ) とほぼ 100% の互換性を保ちつつ、多くの新機能や標準ライブラリが追加され、不便だった部分が改善されました。
 現在のところ C++11 の新機能を「すべて」使える処理系は存在しませんが、今回の規格承認を受け対応がますます加速することは間違いありません。Visual C++ 2010 は、すでに C++11 の主要機能のうち「ラムダ式」「auto」「rvalue references」「static_assert」「nullptr」「decltype」の 6 つの新機能に対応しています。また、完全な実装ではありませんが <unordered_map> <random> <tuple> <regex> などの新しいヘッダが追加されています。
 この記事では、その中から簡単で実用性の高い 4 つの目玉機能「ラムダ式」「auto」「rvalue references」「nullptr」 を紹介したいと思います。



[1] ラムダ式

 ラムダ式は従来の関数オブジェクトの代替となるものです。

(問題)文字列 "C++11 Lambda Expressions" に含まれるアルファベットの大文字の個数を数えるプログラムを書きなさい。

方法 1 | ループを使う

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
# include <iostream>
# include <string>
  
int main()
{
    const std::string str = "C++11 Lambda Expressions";
 
    size_t n = 0;
  
    for(size_t i=0; i<str.length(); ++i)
    {
        if('A'<=str[i] && str[i]<='Z')
        {
            ++n;
        }
    }
 
    std::cout << n << std::endl;    // n == 3
}


方法 2 | アルゴリズムに関数を渡す

 C++ ではアルゴリズムを書くことで表現を明快にし、保守性を高めることができます。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
# include <iostream>
# include
 <string>
# include
 <algorithm>
 
bool 
IsUpper(char c){ return 'A'<=c && c<='Z'; }
 
int 
main()
{
    
const std::string str = "C++11 Lambda Expressions";
 
    size_t n = std::count_if(str.begin(),str.end(),IsUpper);
 
    std::cout << n << std::endl;    
// n == 3
}


方法 3 | アルゴリズムに関数オブジェクトを渡す

 方法 2 は、見た目はシンプルですが、関数をポインタで渡すため処理がインライン化されづらいという欠点があります。
 次のように関数オブジェクトを使うと、アルゴリズムにはポインタではなくオブジェクトが渡されるので、インライン化された速いコードが生成されます。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
# include <iostream>
# include <string>
# include <algorithm>
 
class IsUpper
{
public:
    bool operator()(char c) const
    {
        return 'A'<=c && c<='Z';
    }
};
 
int main()
{
    const std::string str = "C++11x Lambda Expressions";
 
    size_t n = std::count_if(str.begin(),str.end(),IsUpper());
 
    std::cout << n << std::endl;    // n == 3
}


方法 4 | アルゴリズムにラムダ式を渡す

 関数オブジェクトをつくる方法は、効率的な一方どうしてもコード量が大きくなってしまいます。
 またアルゴリズム自体には関数を使う場所と関数を定義する場所が離れてしまう欠点があります。
 それを解決できるのが C++11 のラムダ式です。詳しい説明は後回しにして、実際にどのように使われるかを見てみましょう。

01
02
03
04
05
06
07
08
09
10
11
12
# include <iostream>
# include
 <string>
# include
 <algorithm>

int
 main()
{
    
const std::string str = "C++11 Lambda Expressions";

    size_t n = std::count_if(str.begin(),str.end(),[](
char c){ return 'A'<=c && c<='Z';});

    std::cout << n << std::endl;    
// n == 3
}

 ラムダ式は [ ], ( ), { } の 3 つのブロックで構成されます。
 [ ] はキャプチャリストといって、関数の中でアクセスする必要がある、スコープ内にある外部の変数を記述します。
 ( ) には関数の引数を記述します。
 { } には関数の本体を記述します。
 型推論の機能 「decltype」がはたらくことで戻り値の型は省略できますが、明示的に書く場合は [ ]( )->type{ } のようにします。

 ラムダ式は ( ) 内の引数をとり、{ } 内の処理を実行する、無名の関数オブジェクトとして振る舞います。
 関数オブジェクトを使う方法と同等の実行効率で、可読性に優れたコードが書けます。

 いくつか例を見て、ラムダ式に慣れてみましょう。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# include <iostream>
# include
 <vector>
# include
 <algorithm>
# include
 <functional>
 
// std::function<戻り値の型,(引数の型,..)>
void 
Func( std::function<void(void)> function, int count )
{
    
for(int i =0; i<count; ++i)
    {
        function();
    }
}
 
int
 main()
{
    std::vector<
int> v(10);
 
    std::generate(v.begin(),v.end(),[](){
return rand();});  // 要素に乱数を代入
 
    std::for_each(v.begin(),v.end(),[](
int i){ std::cout << i << std::endl;});  // 要素を表示
 
    
int x = 10000;
 
    std::cout << std::count_if(v.begin(),v.end(),[x](
int i){ return i < x;}) << "個" << std::endl; // x より小さい要素の個数を数える
 
    
int sum = 0;
 
    std::for_each(v.begin(),v.end(),[&sum](
int i){ sum += i;}); // キャプチャした変数を変更する場合は & 修飾した参照キャプチャ
 
    std::cout << 
"平均: " << static_cast<double>(sum) / v.size() << std::endl; // 要素の平均を表示
 
    Func([](){std::cout << 
"Hello!" << std::endl;},5);  // オブジェクトなので、別の関数に渡すことができる
 
    
auto bye = [](){std::cout << "Good bye." << std::endl;};    // auto (後述)を使って変数のように扱う
 
    Func(bye,3);
}

 アルゴリズムとラムダ式を組み合わせることで、明示的なループを減らし、安全で速いプログラムを簡潔に書けるようになりました。



[2] rvalue references

 rvalue references は、無名の一時オブジェクトを引数にとる代入演算子やコンストラクタを作るための演算子です。

(問題)初期化時にサイズを指定できる IntArray クラスを設計し、以下のように使った。
    CreateZeroArray(size) は引数 size のサイズを持つゼロ初期化された IntArray を返す関数。
    このプログラムを実行すると、何回 new が呼ばれるか。また、出力結果も合わせて考えてみよ。

コード
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# include <iostream>
 
class IntArray
{
public:
 
    IntArray(): p(0),m_size(0)
    {
        std::cout << "デフォルトコンストラクタ" << std::endl;
    }
 
    IntArray(size_t size)
    {
        std::cout << "コンストラクタ(new)" << std::endl;
 
        p = new int[size];
 
        m_size = size;
    }
 
    IntArray(const IntArray& other)
    {
        std::cout << "コピーコンストラクタ(new)" << std::endl;
 
        p = new int[other.m_size];
 
        m_size = other.m_size;
 
        for(size_t i=0; i<m_size; ++i)
        {
            p[i] = other[i];
        }
    }
 
    ~IntArray()
    {
        if(!p)
        {
            std::cout << "なにもしない";
        }
 
        std::cout << "デストラクタ(delete)" << std::endl;
 
        delete[] p; // p == NULL なら、なにもしない。
    }
 
    int& operator[](size_t pos)
    {
        return p[pos];
    }
 
    const int& operator[](size_t pos) const
    {
        return p[pos];
    }
 
    const IntArray& operator =( const IntArray& other )
    {
        std::cout << "コピー代入演算子(delete)(new)" << std::endl;
 
        if( this == &other )
        {
            return *this ;
        }
 
        delete[] p;
 
        p = new int[other.m_size];
 
        m_size = other.m_size;
 
        for(size_t i=0; i<m_size; ++i)
        {
            p[i] = other[i];
        }
 
        return *this;
    }
 
private:
 
    int* p;
 
    size_t m_size;
};
 
IntArray CreateZeroArray(size_t size)
{
    IntArray tmp(size);
 
    for(size_t i=0; i<size; ++i)
    {
        tmp[i] = 0;
    }
 
    return tmp;
}
 
int main()
{
    IntArray a(100);
 
    a = CreateZeroArray(300);
}

main だけに注目すれば、IntArray オブジェクトは 2 つしか作っていないので、new が呼ばれる回数も 2 回のように思えます。
では実行結果を見てみましょう。' ;' 以降は補足のコメントです。

debug ビルドなどでコードが最適化されない場合は
1
2
3
4
5
6
7
コンストラクタ(new)          ; a(100)            // a.p = new int[100]
コンストラクタ(new)          ; tmp(300)      // tmp.p = new int[300]
コピーコンストラクタ(new)     ; 無名(tmp)       // 無名.p = new int[300], COPY( from tmp to 無名 )
デストラクタ(delete)          ; ~tmp()            // delete[] tmp.p
コピー代入演算子(delete)(new)       ; a = 無名        // delete[] a.m_p,  a.m_p = new int[300], COPY( from 無名 to a )
デストラクタ(delete)          ; ~無名()         // delete[] 無名.p
デストラクタ(delete)          ; ~a();         // delete[] a.p

のように 4 回。

一般的なコンパイラの最適化を有効にすれば
1
2
3
4
5
コンストラクタ(new)          ; a(100)        // a.p = new int[100]
コンストラクタ(new)          ; 無名(300)   // 無名.p = new int[300]
コピー代入演算子(delete)(new)       ; a=無名      // delete[] a.p,  a.p = new int[300], COPY( from 無名 to a )
デストラクタ(delete)          ; ~無名()     // delete[] 無名.p
デストラクタ(delete)          ; ~a()      // delete[] a.p

のように 3 回という結果が得られるはずです。
ここでは後者の最適化された結果について考えていきます。

CreateZeroArray の戻り値は「無名の一時オブジェクト」です。
なぜなら、main スコープでは名前を持たず、代入が終わると同時に破棄されるからです。

代入演算 a=無名 において、a はもともと持っていた配列を削除したあと、新しい配列を new し、無名オブジェクトから配列をコピーします。
不要になった無名オブジェクトは、代入が終わると同時にデストラクト、 delete[] されます。

でも、コピーをするためだけに、無名の一時オブジェクトを作成、破棄するのはもったいないと思いませんか?

そこで、無名の一時オブジェクトを代入するときは、「コピー」ではなく「そのオブジェクトが所有するデータを奪う」ようにしようと考えたのが 「rvalue references(右辺値参照)」です。

rvalue とは、一時的に生成される無名のオブジェクトのことです。上の例では CreateZeroArray の戻り値が rvalue にあたります。
rvalue のオブジェクトはすぐ破棄されるので、そのオブジェクトからデータを奪っても、のちのち困ることはありません。

したがって

; a=無名 // delete[] a.p, a.p = new int[300], COPY( from 無名 to a )



; a=無名 // delete[] a.p, a.p = 無名.p, 無名.p = NULL

とすれば、効率が良くなると考えられます。この処理のことを「コピー」と対照させて「ムーブ」と呼びます。

従来の operator =( const Something& r ) もしくは operator =( Something r ) では、引数が rvalue であるかどうかを判断できません。
そこで C++11 では、 rvalue による呼び出しをそれ以外の呼び出しと区別するために、rvalue への参照を示す演算子 && が追加されました。

operator =( const Something& r ) と operator =( Something&& r ) をそれぞれ用意すれば、
名前つきのオブジェクトを引数にとる場合は前者が、rvalue を引数にとる場合は後者が呼ばれるようになります。

先ほどのプログラムを rvalue references に対応させましょう。
コード
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# include <iostream>
 
class IntArray
{
public:
 
    IntArray(): p(0),m_size(0)
    {
        std::cout << "デフォルトコンストラクタ" << std::endl;
    }
 
    IntArray(size_t size)
    {
        std::cout << "コンストラクタ(new)" << std::endl;
 
        p = new int[size];
 
        m_size = size;
    }
 
    IntArray(const IntArray& other)
    {
        std::cout << "コピーコンストラクタ(new)" << std::endl;
 
        p = new int[other.m_size];
 
        m_size = other.m_size;
 
        for(size_t i=0; i<m_size; ++i)
        {
            p[i] = other[i];
        }
    }
 
    /*--------------------------------
    //    ムーブコンストラクタ
    --------------------------------*/
    IntArray( IntArray&& other)
    {
        std::cout << "ムーブコンストラクタ" << std::endl;
 
        p = other.p;
 
        m_size = other.m_size;
 
        other.p = NULL;
 
        other.m_size = 0;
    }
 
    ~IntArray()
    {
        if(!p)
        {
            std::cout << "なにもしない";
        }
 
        std::cout << "デストラクタ(delete)" << std::endl;
 
        delete[] p; // p == NULL なら、なにもしない。
    }
 
    int& operator[](size_t pos)
    {
        return p[pos];
    }
 
    const int& operator[](size_t pos) const
    {
        return p[pos];
    }
 
    const IntArray& operator =( const IntArray& other )
    {
        std::cout << "コピー代入演算子(delete)(new)" << std::endl;
 
        if( this == &other )
        {
            return *this ;
        }
 
        delete[] p;
 
        p = new int[other.m_size];
 
        m_size = other.m_size;
 
        for(size_t i=0; i<m_size; ++i)
        {
            p[i] = other[i];
        }
 
        return *this;
    }
 
    /*--------------------------------
    //    ムーブ代入演算子
    --------------------------------*/
    const IntArray& operator =( IntArray&& other )
    {
        if( this == &other )
        {
            return *this ;
        }
 
        std::cout << "ムーブ代入演算子(delete)" << std::endl;
 
        delete[] p;
 
        p = other.p;    /* ポインタを奪う */
 
        m_size = other.m_size;
       
        other.p = NULL; /* 奪ったあとは NULL にする */
 
        other.m_size = 0;
 
        return *this;
    }
 
private:
 
    int* p;
 
    size_t m_size;
};
 
IntArray CreateZeroArray(size_t size)
{
    IntArray tmp(size);
 
    for(size_t i=0; i<size; ++i)
    {
        tmp[i] = 0;
    }
 
    return tmp;
}
 
int main()
{
    IntArray a(100);
 
    a = CreateZeroArray(300);
}

このプログラムを実行した結果は以下のとおりです。
コード
1
2
3
4
5
コンストラクタ(new)          ; a(100)        // a.p = new int[100]
コンストラクタ(new)          ; 無名(300)   // 無名.p = new int[300]
ムーブ代入演算子(delete)        ; a=無名      // delete[] a.p,  a.p = 無名.p, 無名.p = NULL
なにもしないデストラクタ(delete)    ;~ 無名()     // delete NULL
デストラクタ(delete)          ; ~a()      // delete[] a.p

 new の呼び出しを最小限の 2 回に抑えることができました。

 このように、rvalue references は、サイズが大きくコピーにコストがかかるオブジェクトの操作を効率化するのに役立ちます。



[3] auto

 auto はコンパイラに型を推論させる機能です。

(問題)次のプログラムで、★の直後の行の intunsigned に変更することになった。全体で何箇所の修正が必要か。

コード
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# include <iostream>
# include <string>
# include <map>
# include <vector>
 
int main()
{
    // ★
    std::vector<int> v(5,10);   // 5 個の 10 の配列
 
    std::map<std::string,std::vector<int> > m;
 
    m["ABC"] = v;
 
    m["DEF"].push_back(777);
   
    m["GHI"] = v;  
   
    for(std::map<std::string,std::vector<int> >::iterator p = m.begin();p != m.end(); ++p)
    {
        std::vector<int> &r = p->second;
 
        for(std::vector<int>::iterator q = r.begin(); q != r.end(); ++q)
        {
            std::cout << *q << std::endl;
        }
    }
}

正解は 5 箇所。すべての <int> を <unsigned> に変更する必要があります。
"Don't repeat yourself" に反する、変更に弱いコードです。

そこで、 C++11 の新機能 auto の出番です。
変数の型に auto キーワードを使うと、コンパイラが型を推論し、適切な型に置き換えてくれます。
例えば
コード
1
2
3
int i = 10;
 
double f = 3.1415;

としていたところを、
コード
1
2
3
auto i = 10;    // i は int
 
auto f = 3.1415;    // f は double

と書けるようになります。
組み込み型に対して auto を使うのは少し変な感じがしますね。
実際は STL やテンプレートでの複雑な型指定を簡略化するために使われます。

はじめのプログラムを auto を使って書き直しましょう。
コード
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# include <iostream>
# include <string>
# include <map>
# include <vector>
 
int main()
{
    std::vector<int> v(5,10);   // 5 個の 10 の配列
 
    std::map<std::string,std::vector<int> > m;
 
    m["ABC"] = v;
 
    m["DEF"].push_back(777);
   
    m["GHI"] = v;  
   
    for(auto p = m.begin();p != m.end(); ++p)
    {
        auto r = p->second;
 
        for(auto q = r.begin(); q != r.end(); ++q)
        {
            std::cout << *q << std::endl;
        }
    }
}

 見通しの良いコードになり、<int> → <unsigned> への変更も、全体で2箇所しか必要としなくなります。

 auto を使うことで、煩雑な型名の記述から解放され、管理しやすいコードを書けるようになりました。



[4] nullptr

 nullptr はヌルポインタを示すキーワードです。

(問題)次のプログラムの実行結果を答えなさい。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# include <iostream>
 
void 
Func( int n, char* str )
{
    
for(int i=0; i<n; ++i)
    {
        
if(str != NULL)
        {
            std::cout << str << std::endl;
        }
        
else
        {
            std::cout << "(null)" << std::endl;
        }
    }
}
 
void Func( int x, int y )
{
    std::cout << x + y << std::endl;
}
  
int main()
{
    Func(3,NULL);
}

 注意深い方なら、Func のオーバーロードに気付いたでしょう。
 正解の出力は

 3

 です。

 このプログラムで、char* 型のヌルポインタを意識して書いたと思われる NULL は、C++ では

01
#define NULL 0

 と、単純に 0 を define したものであり、ポインタ型とは決まっていません。
 Func(3,NULL); と書くと、コンパイラはまず NULL (=0) を int 型の数値とみなし、引数の型が一致する関数を探します。
 この例では Func(int,int) が適合するので、Func(int,int) が呼ばれます。

 もし引数の型が一致する関数が見つからない場合は、値をキャストすることで呼び出し可能になる関数を探します。
 例えば 0 → (char*)(0) のキャストが許されるので、Func(int,char*) が適合します。
 もしこのとき Func(int,double) というオーバーロードがあったらどうなるでしょう。
 コンパイラはどちらの関数を呼べばいいのかわからず、コンパイルエラーになります。

 ポインタ型を意図したのに、引数に数値型をとる関数が呼ばれてしまうのを回避するためには、
 次のように明示的にポインタ型であることを示せばよいのですが、記述が面倒です。

01
Func(3,static_cast<char*>(NULL));

 これを改善するのが C++11 で新しく加わったキーワード nullptr です。
 nullptr は数値型にキャストすることができない、ヌルポインタの値を表します。

 先ほどのプログラムを nullptr キーワードを使って書き直しましょう。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# include <iostream>
 
void 
Func( int n, char* str )
{
    
for(int i=0; i<n; ++i)
    {
        
if(str != nullptr)
        {
            std::cout << str << std::endl;
        }
        
else
        {
            std::cout << "(null)" << std::endl;
        }
    }
}
 
void Func( int x, int y )
{
    std::cout << x + y << std::endl;
}
 
int main()
{
    Func(3,nullptr);
}

 同名の関数が 2 つあるので、コンパイラはオーバーロードを解決するために、引数の型が一致する関数を探します。
 nullptr は (char*)(nullptr) とキャスト可能ですが、 数値へのキャストは許されないので、Func(int,int) は適合しません。
 したがって、意図通り Func(int,char*) が呼ばれます。

 出力結果

 (null)
 (null)
 (null)

 nullptr キーワードによって、ポインタ型を引数にとるオーバーロードを正しく解決できるようになりました。



 今回紹介した「ラムダ式」「auto」「rvalue references」「nullptr」の 4 つの機能は C++11 の新機能のほんの一部に過ぎません。
 C++11 をもっと深く知りたい方のために、C++11 についてさらに学べるおすすめのサイトを 4 つ紹介します。

 本の虫
    http://cpplover.blogspot.com/

 Faith and Brave - C++で遊ぼう
    http://d.hatena.ne.jp/faith_and_brave/

 Wikipedia C++11
    http://ja.wikipedia.org/wiki/C%2B%2B11

 msdn (ラムダ式の文法について)
    http://msdn.microsoft.com/ja-jp/library/dd293603(v=VS.100).aspx


2011/8/14 @Reputeless
2012/1/31 一部加筆修正