Modern C++ Programming

こんばんは.誰に投げるか決めてなかったために2日連続です,N@Nです.つらいです.また,C++の話です.

アブストラクト

一般に用いられるC++コンパイラgcc及びClang)は当然のことながら最新のC++にどんどんと対応していく.

最新のC++コンパイラC++11,C++14対応に関しては以下を参照すると良いだろう.

コンパイラの実装状況 - cpprefjp - C++ Library Reference

C++1y/C++14 Support in GCC - GNU Project - Free Software Foundation (FSF)

Clang - C++1z, C++14, C++11 and C++98 Status

これを見る限りgccについてもClangについてもC++11の対応は完了したと言って良いだろう.但し,C++14対応はgccの方がClangに後れを取っている,と言った感じだ.

C++11によってC++は大幅に進化した.コンパイラの対応状況から言ってもC++11の機能は可能な限り使えるべきだろう.

C++11の全ての機能については書籍であるC++ポケットリファレンスや,Webサイトでもリファレンスがいくらでも転がっているため,ここでは特に主要な機能に焦点を当てたいと思う.

また,各機能においても詳細な議論をすると,それだけで一記事が埋まってしまうので詳細は他の記事を参照されたい.

ちなみに2014年12月19日現在コンパイラの最新バージョンは

  • gcc 4.9.2 (2014/10/30 Released)
  • Clang 3.5 (2014/09/04 Released)

だ.ところでMSVCは2015でかなりの対応を見せるとのことだったが果たしてどうなるのであろうか.

C++11

前述の通り,C++11の機能は最早完全に実装されたと言っても良いほどだ.

C++11の中でも特に用いることが多いであろう機能を以下に記そう.

型推論

いわゆる型推論C++11より導入された(C++03以前にもautoというキーワードはあったが最早過去のものだ).

auto a = 0; // a => int
auto b = 4.0; // b => double
auto c; // ill-formed

また,gcc 4.9.0-において-std=gnu++1yオプションを指定することにより,関数の引数にも型推論を使用することができる.

ヌルポインタ

これまでC++におけるNULLとは単なるint型の0であるマクロに過ぎなかった.

#define NULL 0

これがC++03までのNULLだ.

しかし,C++11で導入されたnullptrはnullptr_t型の実装となった.これによってオーバーロード時の不具合を避けることができる.

ところで/usr/local/include/c++/4.9.0/i686-pc-linux-gnu/bits/c++config.hを見てみよう.

#if __cplusplus >= 201103L
  typedef decltype(nullptr) nullptr_t;
#endif

とある.また,/usr/local/include/c++/4.9.0/ext/type_traits.hには

#if __cplusplus >= 201103L
  inline bool
  __is_null_pointer(std::nullptr_t)
  { return true; }
#endif

とある.

配列

実装に関しては昨日の記事でなんとか読もうとしたものがあるので,大した参考にならないとは思うが参照する方は参照されると良い.

C++03までの配列と違い,C++11のstd::arrayはstd::vectorなどのコンテナと同様のイテレータを持つ配列だ.

range based for

いわゆるforeachだ.std::vectorなどのコンテナに対し,煩わしいコードを書かずに済む.ループカウンタのような本来不要な変数をなくすことにもなる.

std::vector<int> vec = {0, 1, 2};
for(auto const& v : vec) {
  std::cout << v << " ";
}

// => 0 1 2

スマートポインタ

C++03までは,動的なメモリの確保にnewdeleteを用いていた.うっかりdeleteし忘れることによってメモリリークを起こした方も多いのではないか.

また,newをしていないポインタに対するdeleteや,deleteしたポインタに対するdeleteは未定義であった.

スマートポインタはそれを解決する.

shared_ptr<type>unique_ptr<type>についてのおおまかな違いは

  • shared_ptr<type>はメモリの所有権を共有できるが,unique_ptr<type>は唯一である
  • shared_ptr<type>はコピーが可能であるが,unique_ptr<type>は所有権の移動に限られる.

他,deleterの指定ができるなどの機能がある.

std::shared_ptr<int> sptr1;
std::shared_ptr<int> sptr2;
sptr2 = sptr1; // OK

std::unique_ptr<int> uptr1;
std::unique_ptr<int> uptr2;
uptr2 = uptr1; // ill-formed

コンパイル時定数

constexpr宣言された変数はコンパイル時変数となる.

constは単なる定数であるオブジェクトであり,初期化子が定数式でなければ定数式にはならなかった.

しかし,constexprは宣言した変数を明示的にコンパイル時定数としてくれる.可能な限り定数にはconstexpr宣言をするべきだ.

// C++03
#define INF 10e5 // bad
const int INF = 10e5; // good

// C++11
constexpr int INF = 10e5;

ちなみにだが,gcc 4.8.2(恐らくはそれ以前もだ)において

struct X { int n; };
int main() {
    const X x = {10};
    int a[x.n] = {1};
}

はx.nがコンパイル時定数でないためill-formedだった(当然constexpr宣言すれば問題はない).しかし,同様のコードをgcc 4.9.0-で実行するとconstであってもコンパイルは通っていた.

この辺り仕様変更があったのだろうが,詳細はネットの海にあるだろう.

ところで,C++11のconstexprには大幅な制限があった.詳細はボレロ村上氏の資料が詳しいので参照されたい.

中3女子でもわかる constexpr

ラムダ式

ラムダについての詳細な議論は計算機科学の分野なのでここでは一部言及するに止めよう.

ラムダ式はいわゆる無名関数というやつだ.ラムダ計算において,

f(x) = y // 有名(f)関数
λx.y    // 無名関数

となる.

C++においてラムダ式は関数オブジェクトであり,それをローカルに定義することができる.

与えられたint型の引数の2乗を返すプログラムをラムダ式を用いたものとそうでないもので記述してみよう.

constexpr int square(int n) {
  return n * n;
}

int n = 3;
std::cout << square(n) << std::endl; // => 9
auto square = [](int n){ return n * n; };
std::cout << square(3);

短く記述できる関数オブジェクトであり,また,使用する場所と定義する場所が近くなるのでコードの可読性も上昇する.ラムダ式の記述は極めて容易で

[] // キャプチャ
() // 引数
{} // ステートメント
() // 関数呼び出し式(定義だけなら不要)
;

となる.また,引数は省略可能である.そのため例えばラムダ式を用いたHello, World!は,

[]{ std::cout << "Hello, World!" << std::endl; }();

と記述できる.

初期化子リスト

ユーザー定義クラスで一度に初期化することができる.例えば,

std::vector<int> v = {1, 2, 3, 4};

だ.

右辺値参照

右辺値参照はサイズの大きなオブジェクトのコピーに役立つ.右辺値とは無名の一時オブジェクトだ.

右辺値参照は&&を用いて参照する.

// C++03
type a;
type& aRef = a;

// C++11
type a;
type&& aRef = a;

enum class

enum classは強い型付けとスコープを持ったenumだ.

暗黙の型変換や名前の衝突を避ける事に役立つ.

enum class name : type {
  enumrator1,
  enumrator2 = value,
  enumrator3, ... ,
}

C++14

C++14はコンパイラによって実装状況にばらつきがある.

ここで,その機能の一部を提示しよう.

戻り値の型推論

return文から戻り値の型を推論するようになった.但し,複数のreturn文がある場合,型が一致している必要がある.

auto f(auto n) { return n * n; }
std::cout << f(3) << " " << f(2.2);

// => 9 4.84

ジェネリックラムダ式

C++14において,ラムダ式の引数の型を明確にする必要がなくなった.

auto add = [](auto x, auto y) { return x + y; };
std::cout << add(3, 2.2);

// => 5.2

コンパイル時定数の制限緩和

  • C++11ではループ構文の使用不可
  • 条件分岐は三項演算子のみ
  • constexpr修飾された関数内ではローカル変数の宣言が不可

などの制限があったがこれらが撤廃された.但し,Clangしか対応していないのが現状である.

C++14におけるconstexprはより柔軟な表現を手に入れた.

C++14についてもC++11のもの同様ボレロ村上氏の資料が詳しいので参照されたい.

C++14 時代の constexpr プログラミング作法 - ボレロ村上 - ENiyGmaA Code

2進数リテラル

2進数だ.但し浮動小数点数は使えないので注意が必要である.

int x = 0b100; // 4
int y = 0B0011; // 3

終わりに

C++C++11で大幅な進化を遂げ,C++14ではそれを更に強化する形となった.

C++11,C++14ではC++03では煩雑にならざるを得なかったコードも簡潔に書くことができることが多くなった.

C++11,C++14の機能を用いないレガシーなコードは可読性も低く,実装の手間も大きい.積極的にC++11,C++14の機能を用いるべきである.

明日……と言いたいが,日付を超えてしまったので,今日の分をクック君にお願いします.