C++11 に期待すること(後編)

2020年1月19日

前回の続きです。今回はちょっと難易度が上がります。

攻めの機能

ここでは C++11 の新機能のうち、見た目のショックは大きいものの、プログラミングの品質をさらなる高みへいざなう機能を、「攻め」の機能と題してご紹介していきます。

std::array

標準ライブラリに、動的メモリを使わない配列が搭載されました。消費サイズは生配列と同じ。まさに組み込みのためにあるようなクラスです。たとえば次の配列は――

int g_values[4];

std::array に置き換えると次のようになります。

std::array<int, 4> g_values;

std::array が威力を発揮するのは、関数の引数にするときです。確かに生配列も、無理やり関数の引数にすることはできます。このように。

int calcSum(int values[], int n)
{
  int sum = 0;
  for (int i = 0; i < n; ++i) { sum += values[i]; }
  return sum;
}

void test()
{
  const int values[] = { 1, 2, 3, 4 };
  int sum = calcSum(values, 4);
  /* 以下略 */
}

しかしこのコードで、生配列の欠点が2つ露呈しています。

  • 関数をまたぐと要素数が失われる。プログラマーが気を利かせてやらないと、バグの原因になる
  • 初期化子リストを直接関数に放り込むことはできないので、何らかの変数でいったん受けてやらないといけない

std::array は、この2つをいっぺんに解決します。

#include <array>

template <int N>
int calcSum(const std::array<int, N> &values)
{
  int sum = 0;
  for (int i = 0; i < N; ++i) { sum += values[i]; }
  return sum;
}

void test()
{
  int sum = calcSum<4>({ 1, 2, 3, 4 });
  /* 以下略 */
}

慣れないと見た目に戸惑うかもしれませんが、このスタイルは確実にバグを減らします。変数が減るというのは、バグの住みかが減るということと同義だからです。

range-based for

他の言語では foreach として知られている構文です。

for ループを使う場面というのは、大抵が配列を扱うときです。次の for ループもその一例です。

#include <array>

template <int N>
int calcSum(const std::array<int, N> &values)
{
  int sum = 0;
  for (int i = 0; i <= N; ++i) { sum += values[i]; }
  return sum;
}

一般に1つの for ループには、バグが好む場所が3つあります。初期化文・継続条件・増分処理です。このコードにも、バグがありますね1。この手のバグ、埋めてしまったことありませんか?

range-based for は、配列を中心にループを回す構文を特別に用意して、ループ変数をもなくしてしまおうというものです。先ほどのループに適用すると、こうなります。

#include <array>

template <int N>
int calcSum(const std::array<int, N> &values)
{
  int sum = 0;
  for (int value : values) { sum += value; }
  return sum;
}

これに慣れると、従来の for ループが妙に古びたものに見えてくるから不思議です。

std::function

std::function は、従来の関数ポインタを完全に置き換えるものです。これ単体ではメリットが見えづらいですが、次に説明するラムダ式との組み合わせで鬼のような効果を発揮します。

たとえば次のような関数ポインタは――

void (*func1)();
float (*func2)(float);
bool (*func3)(int, int);

std::function に置き換えると次のようになります。

#include <functional>

std::function<void()> func1;
std::function<float(float)> func2;
std::function<bool(int, int)> func3;

これらの変数は、関数ポインタとラムダ式のどちらも受け入れ可能という点に強みがあります。C++11 では関数ポインタの使用をやめて、代わりに std::function を使っていくことをお勧めします。

ラムダ式

ラムダは強烈です。まずもって、見た目がヤバいです。その反面、着実に理解すれば、非常に強力な武器になるのも事実です。使うかどうかはさておき、覚える価値はあります。なぜなら現在主流となる言語、たとえば C#、Swift、Kotlin、Python などでは、もうとっくに使いこなせて当然の機能だからです。

ラムダ式は、いわば「即席関数」です。従来の C++ の世界で関数といえば、グローバル領域に並べられることだけが唯一のあり方でした。これをもっと軽やかに、必要に応じてさっと関数を生み出して、用が済めば捨てる。このような関数のあり方を実現するのがラムダ式です。

実際のコードで説明します。理論上最短のラムダ式は、「引数なしの何もしない関数」を表す、次のコードです。

[]() {}

角括弧、丸括弧、波括弧です。まったくもって意味不明だと思いますが、まずこの基本形を覚えてください。ラムダ式のない従来の C++ では、何もしない関数を与えるだけでも、次のようなコードを書かなければなりませんでした。

extern void addCallback(std::function<void()> callback);

void doNothing() { }

int main()
{
  addCallback(&doNothing);
  return 0;
}

ラムダ式は関数であると同時に値でもあるので、代入したり、引数にしたりできます。たとえば上のコードを、次のように書き換えることができます。本当にこれでコンパイルが通ります。

extern void addCallback(std::function<void()> callback);

int main()
{
  addCallback([]() {});
  return 0;
}

関数に名前を付けたり、どこか別の場所に定義を置いたりする手間が要らなくなっていることがわかりますか? ここまでは何もしないラムダ式の例ですが、何かするラムダ式は次のように書きます。

[]() { printf("Hello world!\n"); }

引数ありの関数も書けます。次のコードは、min 関数をラムダ式で表現したものです。

[](int a, int b) { return (a < b) ? a : b; }

ここまでなら、「普通に関数でいいじゃん」と思われたかもしれません。しかし、ラムダ式でなければできない秘技があります。それはローカル変数を絡めた関数を引き渡すことです。これを「キャプチャ」といいます。ようやく角括弧の出番です。

角括弧の中にローカル変数を並べれば、それらをラムダ式の処理に干渉させることができます。キャプチャが特に本領を発揮するのは、クラスのメンバ関数を引き渡すときです。this をキャプチャすれば、メンバ関数をあたかもグローバルな関数であるかのように引き渡すことができるのです。このように。

extern void addCallback(std::function<void()> callback);

class Test
{
public:
  void doSomething() { /* 略 */ }

  Test()
  {
    addCallback([this]() { this->doSomething(); });
  }
};

メンバ関数とコールバックの相性の悪さに悩んだことがある人なら、このパワーが理解できるのではないでしょうか。ただしこれを実現するには、受け入れ先が関数ポインタではなく std::function であることが条件になります。セットで覚えましょう。

無視した機能

ここまでに挙げた機能は、まだまだ氷山の一角です。ですが、C++11 らしいプログラムを書くにはここまでで十分だと思います。

たとえば次に挙げる機能は、魅力的ではありますが、組み込み用途には不向きという理由で紹介しませんでした。

  • auto
  • 右辺値参照
  • スマートポインタ

脚注

  1. 比較演算子は < でなければなりません