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

2020年1月19日

組み込みソフトウェア開発でもようやく C++11 が使えるようになってきました。ARM 開発環境ベンダーの二大巨頭である Keil と IAR は、ともに C++11 コンパイラの提供を開始しています。

従来の C++ がそうだったように、機能がありすぎるせいで、すべてを使い切ろうとするとかえって悪いプログラムになるという点では C++11 も同じです。

そこで、組み込み屋の視点で C++11 の「使える」機能を選別してみたいと思います。

守りの機能

かつて C++ が登場したとき、情報感度の高い職場では、「ベター C」としての使い方が推奨されることもありました。つまり、無条件に良いものだけは採り入れるけれども、基本的には C と思って使おう、というスタンスです。

C++11 にも、「ベター C++」としての使い方があるはずです。そのような、いきなり採用してもショックが小さい機能を、「守り」の機能と題してご紹介していきます。

enum class

C++ では、列挙子の名前をつけるのは一苦労でした。次のように、他の列挙型との名前の衝突があると、ビルドが通らないからです。

enum AsicState
{
  eInitialized,
  eOperational,
};

enum FpgaState
{
  eInitialized,
  eOperational,
};

仕方なく、次のように機械的な命名規則を設けて重複を避けていた人も少なくないはずです。

enum AsicState
{
  eAsicStateInitialized,
  eAsicStateOperational,
};

enum FpgaState
{
  eFpgaStateInitialized,
  eFpgaStateOperational,
};

でも本当は、素直で短い列挙子を使いたいですよね? C++11 では scoped enum という概念が登場しました。enum の代わりに enum class というキーワードを用いることで、列挙子のスコープがその列挙型に限定されるというものです。つまり、次のようなコードが書けます。

enum class AsicState
{
  Initialized,
  Operational,
};

enum class FpgaState
{
  Initialized,
  Operational,
};

スコープが限定されているので、もはや e のようなプレフィックスも不要ですね。使うときは、次のように使います。

AsicState state = AsicState::Initialized;

これを知ってしまうと、昔の enum には戻れません。

nullptr

C の時代からおなじみの NULL ですが、これはキーワードではなく単なるラベルです。C では NULL は次のように定義されています。

#define NULL ((void *)0)

C++ では型チェックが厳しくなり、この定義だと普通にポインタ変数に代入できないという弊害があったため、次のようになりました。

#define NULL 0

すでに NULL の存在意義が怪しくなっています。しかも、結局これはこれで、引数にすると int に推論されてしまうという欠点があったりします。

このように NULL のあり方は迷走を続けていました。そもそも、何かインクルードしないと使えないというのが NULL の最も残念なところです。

この迷走に終止符を打つのが nullptr です。これはれっきとしたキーワードで、かつての NULL を完全に置き換えるものです。使えるところでは、使っていきましょう。

int *p = nullptr;

キーワードなので色も変わりますよ!

constexpr

今までは、「代入できない値」と「コンパイル時にはすでに決定している値」がともに同じ const キーワードで一括りにされていました。たとえば、次のコードの const に注目してください。

int clamp(int n0, int lower, int upper)
{
  const int n1 = max(n0, lower);
  const int n2 = min(n1, upper);
  return n2;
}

引数の値は関数が呼ばれたときに決定するわけですから、この const 変数の値をコンパイル時に決定できないことは明らかです。

一方、次の const はどうでしょう。

class Queue
{
private:
  static const int N = 12;
  int m_values[N];
};

この const 変数はどう見てもコンパイル時に決定可能です。実際、C++ ではこのような int を特別扱いする謎の仕様があり、この N はコンパイル時に展開されます。だから配列の添え字にもできるわけです。ただ、なぜか float や double でこの表現は許されませんでした。

新しいキーワードである constexpr は、コンパイル時に決定できる値を明確に区別するものです。これならば double でも使えます。

class Queue
{
private:
  static constexpr int N = 12;
  static constexpr double PI = 3.14;
};

コンパイル時に展開されるので高速、というような説明がなされることがありますが、それは理由としては些末なものです。重要なのは、コンパイル時定数であるという明確なサインを、ソースを読む人に送ることです。constexpr と書いてビルドが通るならば、弊害はまずありません。積極的に使っていきましょう。

override

ある仮想関数の定義を見つけたとき、それが新規のものなのか、親クラスを引き継ぐものなのかを区別するには、関数名を見るしかありませんでした。

そしてそのことは、次のような悲劇をもたらすことがありました。

class A
{
public:
  virtual void registerData();
};

class B : public A
{
public:
  virtual void resisterData();
};

B は A の関数を引き継いでいるつもりですが、よく見るとスペルを間違えています。しかしコンパイラは怒りません。この関数は B で新規に定義されたものとみなされるからです。この状況で次のようなコードを実行しても――

B b;
A *p = &b;

p->registerData();  // B::registerData を呼んでいるつもり

当然 A::registerData が呼ばれます。この種のバグは簡単に入り込むわりに、発見がしづらいものです。

override キーワードは、それが親クラスからの引き継ぎであるという意思表示です。

class B : public A
{
public:
  virtual void resisterData() override;
};

このコードはスペル間違いをしているので、「何も引き継いでいない」ということでコンパイルが通りません。このようにミスを発見してくれることが override のメリットです。もちろん、ソースが読みやすくなるというメリットもあります。

これが義務だとなおいいのですが、過去の C++ との互換性がなくなるため、そうはなっていません。ですからこのキーワードは、プログラマーが積極的に使っていかなければ意味をなさないのです。

新しい初期化構文

新しい初期化構文は厳密に話すと長くなりますが、本当に覚えるべきは一つです。C++ には、クラスのメンバ変数としての配列を初期化する手段がないという致命的な欠陥がありました。

C++11 ではこれが可能になりました。

class ADC
{
private:
  const int m_channels[4];

public:
  ADC() : m_channels{ 1, 3, 5, 7 }
  {
  }
};

この点だけでも、部分的に用いる価値はあります。