はじめに
C++はbetter Cとよばれる、C++でもクラス及びテンプレートを使わない手法で使われることがある。また、テンプレートを全面禁止して使われることもある。さらにはクラスにおいて、多重継承が禁止されたりすることもある。これらはC++の黒魔術を封じ込めるためになされる。C++は大変危険なのだ。
C++で黒魔術を書くためには、このページに書いてあることは当然全て理解して使えるようになる必要があるが、それだけでは足りない。このページではC++がいかに複雑奇怪な言語であるかを、C言語を習得した人に向けて示すために要点を押さえて解説してある。C言語すら怪しい方は今一度C言語の復習をして頂きたい。
C++の黒魔術は、
- テンプレート
- 多重継承
- あらゆる演算子の定義
- ラムダ式
によって構成されている。これらを駆使することで(後でそのプログラムをメンテする人、或いは後輩が)死ぬ。
ほんとうは、
- 自由な例外機構
- 明確なデストラクタの呼び出されるタイミング
- 必要な速度に応じてチューニング可能
- C言語の膨大な資産を流用可能
などなどとても便利な特徴があってお勧めな言語なんですけどね。
Cとおなじ文法達
if/else-if/switch-case/while/do-while/for/gotoはそのまま残っている。おそらくgotoの有用な使い方はC++の様々な機能によってなくなったと思われる。
typedefやマクロも同じである。次の例は変数の交換をするマクロである。
#define SWAP(a,b) (a ^= b,b = a ^ b,a ^= b)
次の例はC<n>クラス中でtypeという型の名前をC<n - 1>::typeによって定義する例である。typenameとは続く識別子が型の名前であることを明示するためのキーワードである。
typedef typename C<n - 1>::type type;
ポインタ
Cと同様に&(変数名)でポインタを取得し、宣言時には型の名前に*を付けることでポインタを宣言できる。但し、後に述べる参照にポインタはない。
関数ポインタなどもCと同様で、次のように、
int型を引数に取る、int型を引数に取りint型を返す関数へのポインタを返す関数へのポインタを引数に取り、int型を引数に取りint型を返す関数へのポインタを返す関数へのポインタを3つ持つ配列へのポインタ を表すには、
int(*(*(*p)[3])(int))(int(*(*)(int))(int));
C++ではメモリをヒープに確保するための文法が用意されている。Cではライブラリstdlib.hによってmallocで行われていたものである。当然、C++でもmallocが使えるが、直接メモリ確保ができるnewが便利である。
int* p = new int(3);
これはint型のメモリを確保し、3で初期化したのち、ポインタpにそのアドレスを代入している。使い終わったら、
delete p;
として開放する。
placement new
new演算子は、既存のメモリに上書きして生成することもできる。次の例では、1024個のchar型をもつ配列を宣言し、そのポインタをpとし、pをつかってnew, p + sizeof(int)をつかってnewしたものである。このnewはnewする対象がクラスの場合、コンストラクタを呼び出し、領域にコピーする。
通常のnewとはことなり、予めメモリが確保されていると分かるので高速に動作する特徴がある。
#include <iostream>
#include <new>
using namespace std;
int main(void) {
char memory[1024];
char* p = memory;
int* x = new(p) int;
p += sizeof(int);
int* y = new(p) int;
*x = 10;
*y = 20;
cout << *x << endl;
cout << *y << endl;
cin.get();
return 0;
}
配列
配列は、連続したメモリを確保するために使われる。サイズは変更できないという性質上、連続するデータを表すためではなくメモリ確保のために使われると考えた方がいい。
int a[4]; // int型×4
MyClass my_class[12]; // MyClass型×12
これらはローカル変数ならばスタックに確保される。スコープを抜けると自動的に開放され、デストラクタが呼び出される。(後述)
配列をヒープに確保することもできる。その場合はnew演算子を使う。
int* p = new int[4];
p[0] = 1;
p[1] = 2;
p[2] = 3;
p[3] = 4;
delete[] p; // deleteだけではだめ。delete[]とする
初期化リストを使うとまとめて初期化できる。まず従来のCの記述方法では、
int a[4] = { 1, 2, 3, 4 };
と書いていた。新しいC++では、
int a[4] { 1, 2, 3, 4 };
と書いても良い。new演算子を使った場合は次のように書ける。
int* p = new int[4] { 1, 2, 3, 4 };
クラスCが次のように定義されているとする。
class C {
public:
C(int x, int y) {
std::cout << x << "," << y << endl;
}
};
これを初期化するには、次のようにする。
C a[2] = { { 1, 2 }, { 3, 4 } };
C b[2] { { 1, 2 }, { 3, 4 } };
C*p = new C[2] { { 1, 2 }, { 3, 4 } };
C++11では{ リスト }に型std::initializer_list<T>が与えられた。すなわち、
std::initializer_list<int> a = { 1, 2 };
このように代入できてしまう。コンストラクタはstd::initializer_list<T>型で受け取って、任意の個数の引数を取ることができる。これはイテレータで実現できて、
std::initializer_list<int> list = { 1, 2 };
for(std::initializer_list<int>::iterator it = list.begin(); it != list.end(); it++) {
cout << *it << endl;
}
とする。
型推論
C++11では右辺値の型が明らかな場合、autoを指定して推論してくれる。
auto i = 0; // int型として認識される
上の複雑なイテレータstd::initializer_list<int>::iteratorも右辺値から明らかなので、
auto list = { 1, 2 };
for(auto it = list.begin(); it != list.end(); it++) {
cout << *it << endl;
}
と書ける。
拡張for文
イテレータを実装したクラスはその全ての要素に渡って操作できるので、上のfor(auto it = list.begin(); it != list.end(); it++) のような書き方を常にしなければならない。begin, end, ++なんて決り文句の塊である。他の言語のforeachのようなものをC++11で導入した。次のように書く。
auto list = { 1, 2 };
for(int value : list) {
cout << value << endl;
}
const
constは値を変更できなくする縛りをプログラマに課す。一体何が嬉しいのだろうか? それは変更されるべきでないことをコンピュータに教えてあげ、人間がミスをしたら警告してくれるようにできる点だ!
void func(const int* const* const p) {
int a;
int* b;
p = &b; // 変数pの変更禁止
*p = &a; // 変数pの実体の変更の禁止
**p = a; // 変数pの実体の実体の変更禁止
}
参照
C++の大きな機能の一つとして参照が挙げられる。
int x = 3;
int& r = x; // rはxの参照
std::cout << r << std::endl;
また、参照を使って参照を定義できる。
int x;
int& r = x;
int& rr = r;
これはポインタを使うと次のように書け、実際コンパイル結果は同じになった。(コンパイラによって実装が異なるかもしれないが、VC++では同じだった。すなわち、参照もポインタも同様にアドレスをメモリに保存する。)
int x = 3;
int* p = x; // pはxのポインタ
std::cout << *p << std::endl;
参照はその実装がどうあれ、C++の使用上としては変数の別の名前に過ぎないため、一応データ領域を持たないことになっている。(何度も繰り返すが、VC++の出力するアセンブリを見る限りは全く同じコードを生成する)従って以下のように、要素のサイズが分からないで配列を宣言するとエラーとなる(のが現状の言語仕様である)。
int& refs[3]; // エラー
もちろん、配列の参照は可能である。
int (&ref)[3];
話戻って
(std::cout << "Hello, world.") << std::endl;
を見てみよう。これは実は自分自身の参照を返している。こうすることで自分自身を連続的に評価できる。
どういう意味か? 例えば+演算子を考えてみよう。A1 + A2 + A3を実行したとき、
(A1.operator+(A2)).operator+(A3)
と実行される。さて、(A1.operator+(A2))はA1, A2, A3と同じ型であるべきだが、C/C++のポインタでは、ポインタ自体が値であるから、「.」ではなく「->」でなければならない。従って、もしもポインタを返したら*(A1 + A2) + A3としなければならないことになる。これは不便。そこで、変数と同じように振る舞うために参照を返すことで解決することにした。
結局、参照とはポインタと同じ概念であり、ポインタに「1. 参照先を変更できない 2. 初期化時に参照先を確定する 3. 1ゆえにその変数は普通の変数として振る舞う」という制約を設けたものである。基本的に参照を使い、ポインタが必要な場面(すなわち参照ではなく、アドレスが必要な場面)ではポインタを使うのがC++の賢い使い方である。
参照先の保証はない
参照もポインタと同様、参照先がなくなる可能性がある。
class C {
public:
C() {
std::cout << "C()" << std::endl;
}
void func() {
std::cout << "C::func()" << std::endl;
}
~C() {
std::cout << "~C()" << std::endl;
}
};
struct S {
S(C& r) : r(r) { }
C& r;
};
struct S2 {
S2(C* r) : r(r) { }
C* r;
};
int main() {
S* s;
S2* s2;
{
C c1, c2;
s = new S(c1);
s2 = new S2(&c2);
} // ここでc1, c2のデストラクタが呼び出される
s->r.func(); // 保証されない
s2->r->func(); // 保証されない
std::cin.get();
return 0;
}
const参照
const参照は、
const int& r = a;
のようなものをいう。これは変更不可にした参照であり、
int x;
const int& r = x; // rは変更不可
r = 10; // エラー
x = 10; // ok
const参照を用いてconstでない参照は作れない。
int x;
const int& r = x; // rは変更不可
int& rr = r; // エラー
constでない参照は、初期化時に他の「変数」を指定しなければならない。計算結果だったり、定数だったりと一時オブジェクトを指し示すことはできない。この、宣言された「変数」を左辺値、定数や計算結果を右辺値という。
C*& p3 = &(C()); // エラー; constでないリファレンスへは右辺値を代入できない
const C*& p2 = &(C()); // エラー; constでないリファレンスへは右辺値を代入できない
C* const& p = &(C()); // ok
いずれもリファレンスの定義だが、コンパイルが通るのはpだけで、これがconst参照に当たる。const参照は、定義する式が右辺値であるとき、一時変数に初期化子への値を保存して、その一時変数への参照を用いることになっている。これはconst参照だけ可能である。
右辺値の他の例として定数がある。
int const& r = 1;
この場合、一時変数に1を入れて、その変数への参照を返す。そして、この一時変数の寿命は参照変数rと同じくすることになる。一時変数は至る所で出現する。例えば、以下の例ではコンストラクタC()が呼び出され、これは一時変数に格納される。しかし、文が終わればそれは使われないので、開放される。
class C {
public:
C() {
std::cout << "C()" << std::endl;
}
~C() {
std::cout << "~C()" << std::endl;
}
};
int main() {
C();
std::cin.get();
return 0;
}
実行結果:
C()
~C()
ところが、この一時変数の値をリファレンスに入れてあげると、
int main() {
C const& r = C();
std::cin.get();
return 0;
}
実行結果:
C()
参考 :
http://www.hiramine.com/programming/c_cpp/operator.html
右辺値参照
C++11の新しい機能である。書き方は簡単で、
int&& r = 10; // 右辺値を参照する参照変数
int& l = 10; // エラー
とかく。先ほどのconst int&でも右辺値がとれた。実は結局同じことなのだが、constでなくなったという点が異なる。
結局この右辺値参照が導入されたことで、
f(T& larg);
f(T&& rarg);
と、右辺値か左辺値化によってオーバーロードを選択することができるようになったと考えて良いだろう。右辺値参照を使った例は、オブジェクトの移動に関するところで出てくる。クラスのところで述べる。
参照とポインタ
結局、参照のことを長々述べたが、参照はポインタの機能を制限したものであるととらえて問題ない。constだって普通の変数を書き換えることにおいて制限したものである。クラスに於けるprivateなメンバは外部からの読み込みにおいて制限したものである。このようにC++には何らかの制限を設ける構文がたくさんある。もちろん、Cで書いていたときのように制限がない書き方もできるが、この制限を課すことでより正確にしたいことをプログラム上で表現できる。出力されるコードもコンパイラの最適化によって変わるかもしれないが、それ以上にプログラムソース上で識別できるのは利点である。
ポインタはとにかく自由度が高すぎる。そこで、最初は何らかのデータを指し示すとき、参照を使い、その後問題が出てきたらポインタを使ってみてはいかがだろう? そのうち、アドレスが必要な場合とデータを指し示すだけで十分な場合と区別が付いてくると思う。
構造体
実はC++ではCとはちがってクラスと同じ扱いを受ける。但し、デフォルトでpublicが指定されるという違いがある。
共用体
共用体は内部のデータを共有する。Cでもあってほとんど変わらない。ただし、構造体と似た扱いを受ける。すなわち、メンバ関数を持つことができる。但し継承はできない。クラス・構造体と同様でunion U ~と宣言しなくても、U ~で使える。
union U {
int a;
int b;
};
無名構造体も入れられる。
union U {
int a;
int b;
struct {
short x;
short y;
};
};
無名共用体もできる。この場合、同じスコープ上に変数が散らばることになる。
int main() {
union {
int a;
int b;
};
a = 10;
std::cout << b << std::endl;
std::cin.get();
return 0;
}
無名共用体をグローバルスコープで使う場合は、staticを付けて宣言する必要がある。
static union {
int a;
int b;
}
列挙型
enum E { A, B, C, D };
おなじみのenumだが、C++ではクラスと同様に
E e;
とenum Eとしなくても良くなった。
Cと同じ点では、A, B, C, Dがグローバルスコープ、すなわち
#define
A 0
#define
B 1
と同じような扱いになっているという点。これはCとの互換のためであろう。ただし、C++のクラスのように::演算子でアクセスもできる。
E e = E::B;
enum E1 { A, B, C, D };
enum E2 { A, B, C, D };
E1 e1 = E2::B;
はしっかりコンパイルエラーを吐いてくれるぞ!
そして、C++11ではenumのグローバルスコープにする悪しき風習をなくすことができて、
enum class E { A, B, C, D };
とできる。
また、enumやenum classは整数型を変更できて、
enum E : unsigned char { A, B, C, D };
などとできる。
bool型
Cでは真・偽を表すのに1/0を使っていて、比較演算子はその結果として1/0を用いていた。しかし、C++では代わりにtrue/falseを使うことができる。これらは数値ではなくboolという型が付けられた。もちろん、1bitの大きさを持つわけではないので注意。
nullptr
C++ではNULLを0とみなして、Cでは(void*)0と見なしていた。もちろん処理系によって異なる場合もある。nullptrはそうしたものを統一して、様々な型に入れても問題が起こらないように導入されたnullptrなのだ。どこのデータも指していないことを示せる。
try-catch
ifとかwhileとかよくある奴はそのままである。C++の目玉の1つである例外処理は悪魔のようなエラー処理から解放されることになる。
次の例では、例外がtry文の中の関数f()の中で発生し、それをmainの中で処理している例である。どれだけ深くネストされようが、catchされるまで例外は遡り、結局キャッチできなければ強制終了される。(terminate関数が呼び出される。)
class e1 : public std::exception {
};
class e2 : public std::exception {
};
void f() {
throw e1();
}
int main() {
try {
f();
}
catch(e1& e) {
std::cout << "エラー1" << std::endl;
}
catch(e2& e) {
std::cout << "エラー2" << std::endl;
}
std::cin.get();
return 0;
}
C言語のように戻り値でエラーを返すことはなくなり、すっきりした。例外機構は上のようにstd::exceptionクラス(またはその派生クラス)を継承して独自の例外クラスを作り、その例外クラスをインスタンス化してthrowする。それを参照で受け取ってcatchする。
全ての例外を補足するには
catch(...) {
// その他の例外処理
}
とするが、余り使うべきではない。なぜならば、分からない例外は上位に任せるべきだからである。
関数tryブロック
※ 例外を本来はintで投げたりしてはいけないが、今回は文法の例として簡単に分かるようにintを使っている。
void func(int x)
try {
throw x + 1;
}
catch(int n) {
cout << x << endl;
cout << n << endl;
}
のように関数全体をtryブロックとすることができる。
コンストラクタに適用すると、初期化子の例外を補足できる。
class M {
public:
M() {
throw 0;
}
};
class C {
public:
M m;
C() try : m() {
throw 1;
}
catch(int n) {
throw 2; // 初期化子で発生した例外をキャッチして更に例外を投げる
}
};
int main() {
try {
C c;
}
catch(int n) {
cout << n << endl; // 2
}
return 0;
}
std::exception
標準ヘッダファイルexceptに標準エラークラスstd::exceptionが定義されている。これを継承して基本的な例外が標準で定義されていて、#include <stdexcept>することで使用できる。これらには次のようなものがある。<>内は定義されているヘッダファイルである。
- exception <exception>
- bad_exception <exception>
- bad_alloc <new>
- bad_cast <typeinfo>
- bad_typeid <typeinfo>
- ios_base::failure <ios>
- logic_error <stdexcept>
- domain_error
- invalid_argument
- length_error
- out_of_range
- runtime_error <stdexcept>
- overflow_error
- range_error
- underflow_error
基本的には、これら派生クラスを継承して独自例外を作るが、独自クラスの例外を作る場合はstd::exceptionを継承してクラスの例外を作り、そのクラスの例外を継承してここの例外を作るのが望ましい。サンプル例をロボットファクトリーのブログ記事 C++Tips2から引用してきた :
#include <stdio.h>
#include <stdexcept>
class MyClassException : public std::exception {
public:
MyClassException(const char* message) : message(message) { }
virtual const char* what() const {
return message;
};
private:
const char* message;
};
class MyClass {
public:
MyClass() {
throw MyClassException("失敗1");
}
};
class MyClass2Exception : public std::exception {
public:
MyClass2Exception(const char* message) : message(message) { }
virtual const char* what() const {
return message;
};
private:
const char* message;
};
class MyClass2 {
public:
MyClass2() {
try {
MyClass mc; // 下位のクラス
}
catch(const MyClassException& e) { // 下位の失敗をフォローして
throw MyClass2Exception(e.what()); // 自分の失敗として上位に報告する
}
}
};
int main(void) {
try {
MyClass2 mc2; // MyClass2Exceptionが起こることしか期待しない
}
catch(const MyClass2Exception& e) { // 1つ下の例外だけ気にすればいい
printf(e.what());
}
getchar();
return 0;
}
namespace
C++ではブロックに名前を付けることができる。(名前)::(識別子)と::演算子を用いてアクセスする。
namespace N {
int x;
void f() {
}
}
int main() {
N::x = 10; // xだけだとエラー
N::f();
std::cin.get();
return 0;
}
入れ子にもできて、
namespace N {
namespace M {
int x;
}
void f() {
}
}
int main() {
N::M::x = 10;
N::f();
std::cin.get();
return 0;
}
実はクラスや構造体、共用体にもそのブロックには名前が付き場合によっては::演算子が必要なことがある。詳細はクラスのところで触れよう。
namespaceでくくらないと、それはグローバル名前空間に属することになる。グローバル上にあるものを直接指したい場合は、::を付ける。例えば、Nの中からグローバルの変数を指したい場合は次のようにする。
int x;
namespace N {
int x;
namespace M {
int x;
}
void f() {
x; // N::xのこと
M::x; // N::M::xのこと
::x; // グローバルなxのこと
}
}
名前空間を別の名前に置換できてしまう。
namespace NM = ::N::M;
C++では、全ての翻訳単位が同じグローバルな名前空間に属しているとしている。従って、複数のファイルのファイルスコープに宣言した場合(要するにグローバル変数)、どこからでもアクセスされてしまう。ただしstaticを付けた場合は内部リンケージを持つためこの限りでない。
namespaceの名前を省略することもできる。その場合、その空の名前空間はグローバルな名前空間と区別されて、翻訳単位内で使用可能である。他の翻訳単位からは参照されないため、staticと同じ扱いとなる。C++ではstaticよりもこちらの方が推奨されている。
namespace {
int x; // (1)
}
int x; // (2)
x; // これは(1)と(2)で曖昧となるためエラー
関数
C++はCと互換性を持たせるために、C言語の構文を引き継ぎなら拡張されてきた。そのため、C言語のように関数を直接書くことができる。
void func() {
// なんちゃら
}
オーバーロード
void f(int a) {
// なんちゃら
}
void f(double a) {
// なんちゃら
}
と同じ名前で関数が定義できる。そして、
f(10);
f(10.5);
などと呼び出すとき、その引数の型に応じて適切な関数を呼び出してくれる機能がある。これをオーバーロードと言い、関数名と引数の型をセットでシグネチャという。
テンプレートを使って
template<typename T>
void f(T a) {
// なんちゃら
}
と書きたいが、doubleだけ除外したい。そういったときは
template<typename T>
void f(T a) {
// なんちゃら
}
void f(double a) = delete;
とすれば呼び出せなくなる。このように関数に対してdelete指定ができる。当然、
f(10);
f(10.5); // コンパイルエラー
となる。
デフォルト値
仮引数に=をかくことでデフォルト値を決められる。デフォルト値のある引数は引数を省略することができる。
void f(int a = 10) {
// なんちゃら
}
この場合は、f()と呼び出してもf(10)と呼び出してもaは10として評価される。もちろん、この値以外にも使える。仮引数の変数で初期化することも可能である。
無名引数
引き数名を省略することができる。この際、型だけ与える。無名引数もシグネチャの対象となるので、オーバーロードの際に評価される。例えば、次のようにテンプレートの値によって処理を振り分けることができる。
template<int n>
class C { };
void f(C<0>) {
std::cout << "C<0>" << std::endl;
}
void f(C<1>) {
std::cout << "C<1>" << std::endl;
}
int main() {
f(C<0>());
f(C<1>());
}
ラムダ式
簡単に言うと関数の中でも定義できる関数である。ただしそれよりももう少し高機能である。
引数に関数を渡したりするとき、関数ポインタを渡すのが今までの方法であった。要するに関数ポインタに似た機能だが、新たなラムダ式というものが導入され、これは関数ポインタを使った関数呼び出しとは異なりスコープを共有できてしまう。
まずは普通のラムダ式の実行を見よう。これは関数ポインタと変わらない。
int main() {
auto l = [](int x) -> int { return x * 2; };
int(*p)(int);
p = l;
std::cout << l(10) << std::endl; // 20
std::cout << (*p)(5) << std::endl; // 10
std::cin.get();
return 0;
}
[](引数) -> 戻り値の型 { 処理内容 };
と構成されている。これは普通の関数であり、上記の通り関数ポインタに代入までできてしまう。このように->を使って戻り値を指定することができる。これは戻り値の後置修飾と呼ばれており、普通の関数の定義でも使える。ラムダ式の型はautoにして使う。
しかし、スコープの変数を参照できて、
int main() {
int n = 0;
auto l = [&](int x) -> int { return x * 2 + n; };
n = 1;
std::cout << l(10) << std::endl; // 21
n = 2;
std::cout << l(10) << std::endl; // 22
std::cin.get();
return 0;
}
これは関数ポインタでは実現できない。特定の変数だけ拝借することもできる。また、上記の例では変数nなどを参照として受け取るが、定義時に値をコピーして受け取ることもできる。
int main() {
int n = 0;
auto l = [=](int x) -> int { return x * 2 + n; };
n = 1;
std::cout << l(10) << std::endl; // 20
n = 2;
std::cout << l(10) << std::endl; // 20
std::cin.get();
return 0;
}
そのほか、[]の中には、どの変数をコピーするか、参照するか、明確に書くこともできる。例えば変数xをキャプチャしたい場合は&xと指定する。次の例では関数fにラムダ式lを渡して、内部で実行している。
#include <iostream>
using namespace std;
template<typename lambda>
void f(lambda l) {
int x = 10;
l();
cout << "In f() : " << x << endl;
}
int main(void) {
int x = 100;
auto l = [&x](void) { x += 1; };
f(l);
cout << "In main() : " << x << endl;
cin.get();
return 0;
}
結果 :
In f() : 10
In main() : 101
ラムダ式がキャプチャしているのは宣言された時点での変数、すなわちmain関数のint xであり、それ以外の変数には興味を示さない。関数に渡され、fのなかでもint xがあるがこれは全く関係がない。そして、fのなかでラムダ式が実行されても、キャプチャしたmain関数内の変数xは保持され、1加算される。その結果、In main() : 101が表示された。
ラムダ式の戻り値は型推論され、型を省略することができる。
auto l = [=](int x) { return x * 2 + n; }; // 戻り値の型を省略可能
C++11ではラムダ式でない関数の戻り値を型推論することができないため、ラムダ式を返す、ラムダ式出ない関数を定義できない。つまり次のようなことはC++14以降でないとできない。
auto func(void) {
return []() { /* 処理 */ };
}
(なお、時期VC++ではこの戻り値の型推論の機能が実装される予定だそうだ。2014/3/5時点でもVisual C++ Compiler November 2013 CTP〔無料である〕を導入することでこれらの機能を試すことができる。)
ラムダ式の戻り値は型推論されるため、ラムダ式がラムダ式を返すことは容易である。
auto l = []() { return []() { /* 処理 */ }; }
ラムダ式を関数に渡すには、
template<class T>
void f(T l) { }
とテンプレートで任意引数を取るか、
void f(std::function<int(int)> f) {
std::cout << f(10);
}
とstd::functionを使うかして行う。なお、これはfunctionalをインクルードする必要がある。テンプレートの場合はラムダ式であるかそうでないか判別できないため、どんな型でも渡せてしまう。関数に渡す時点でエラーチェックができないのだ。ただし、実行した先ではエラーが起きるため問題ないと言えば問題ないかもしれない。
template<class T>
void f(T t) {
t(10);
}
int main() {
int n = 2;
auto l = [&n](int x) -> int { return x * n; };
auto l2 = [](int x, int y) -> void { std::cout << x * y; };
n = 3;
f(l);
f(l2); // 展開時にエラー
std::cin.get();
return 0;
}
std::functionを使うと、そのオーバーヘッドが大きい。従って、呼び出し先で何回も実行するようなラムダ式の場合は遅くなる可能性がある。実際、以下のようにベンチマークを試みた。
#include <iostream>
#include <functional>
#include <time.h>
static const int N = 100000000;
template<class T>
void f1(T t) {
for(int i = 0; i < N; i++) {
t(i);
}
}
void f2(std::function<int(int)> f) {
for(int i = 0; i < N; i++) {
f(i);
}
}
int main() {
int n = 2;
clock_t c;
auto l = [&n](int x) -> int { return x * n; };
n = 3;
c = clock();
f1(l);
std::cout << "template : " << (clock() - c) << std::endl;
c = clock();
f2(l);
std::cout << "function : " << (clock() - c) << std::endl;
std::cin.get();
return 0;
}
結果は、
template : 490
function : 2900
であった。
クラス
C++のクラスは大変複雑なので
クラスを参照して欲しい。
テンプレート
C++の目玉であるテンプレートも大変複雑なので
テンプレートを参照して欲しい。
最終更新:2014年03月05日 15:29