【C++】コンストラクタ初期化の作法。初期化リストを使うべき理由と現場の知恵

【C++】コンストラクタ初期化の作法。初期化リストを使うべき理由と現場の知恵 C++

皆さん、こんにちは。リーダーです。

C++でクラス設計をする際、必ずと言っていいほど向き合うことになるのが「コンストラクタでの初期化」です。
実はここ、ベテランのエンジニアでも「なんとなく」で書いてしまっているケースを時々見かけます。

私も若手の頃、コンストラクタの「{}(中身)」の中でメンバー変数に値を代入して、「これで初期化完了!」と思い込んでいた時期がありました。

しかし、組み込み開発の現場でパフォーマンスがシビアな製品を扱った際、先輩から「それは初期化ではなく再代入だよ」と諭され、自分のコードが非効率だったことに気づかされた苦い思い出があります。

今日は、皆さんがそんな遠回りをしなくて済むように、C++におけるコンストラクタ初期化の「正しい流儀」について、実体験を交えながらお話ししてみたいと思います。

【執筆者の簡易プロフィール】
リーダー 執筆者:リーダー
  • 当ブログの統括者兼ライター
  • 45歳男性、既婚、2児の父
  • 組み込みエンジニア
  • 新卒で大手電機メーカーに入社し、開発部門に配属、現在も勤務
  • 主な使用言語はC言語とC++
  • 趣味は毎晩の晩酌

「メンバ初期化リスト」を活用するのがC++の基本

C++には、コンストラクタの本体が実行される前にメンバー変数を初期化するための「メンバ初期化リスト」という仕組みがあります。

書き方は、コンストラクタの引数リストの後に「:(コロン)」を付け、変数名(値)の形式で記述します。

#include <iostream>
#include <string>

class Sensor {
private:
std::string name;
int id;

public:
// これが「メンバ初期化リスト」による初期化です
Sensor(std::string s_name, int s_id) : name(s_name), id(s_id) {
// コンストラクタ本体(ここではもう初期化が終わっています)
std::cout << name << "を生成しました。" << std::endl;
}
};

このコードでは、name(s_name) と id(s_id) の部分が初期化リストです。
コンストラクタの {} の中に入る前に、変数が直接その値で作成されるイメージですね。

なぜわざわざこんな書き方をするのか。
それは、この後に説明する「代入との違い」が非常に重要だからなんです。

なぜ「中身で代入」ではいけないのか?

コンストラクタの {} の中で name = s_name; と書くのと何が違うのか、疑問に思う方も多いでしょう。
結論から言うと、「初期化」と「代入」は別物だからです。

コンストラクタ本体で代入を行う場合、実は以下のような2段階の処理が行われています。

  1. メンバー変数が「デフォルト値」で一度作成される(デフォルトコンストラクタの呼び出し)
  2. その後、コンストラクタ本体で新しい値を「上書き(代入)」する

これでは、一度作ったものをすぐに捨てて書き直しているようなもので、少しもったいないですよね。
特に std::string や複雑なクラスオブジェクトの場合、この無駄なステップがパフォーマンスに影響を及ぼすことがあります。

「初期化リスト」を使えば、最初から指定した値で変数を作成するため、無駄が一切ありません。
処理効率を重んじるエンジニアであれば、常にこちらを選択したいところですね。

初期化リストを使わなければならないケース

「効率がいいだけなら、中身で代入してもいいじゃないか」と思われるかもしれませんが、実は初期化リストを使わないとコンパイルエラーになるケースが存在します。

主に以下の3つです。

  • constメンバー変数:一度作成したら変更できないため、作成時(初期化リスト)に値を決める必要があります。
  • 参照(&)型のメンバー変数:作成と同時に参照先を固定しなければなりません。
  • デフォルトコンストラクタを持たないクラス:「とりあえず作成」ができないため、明示的に初期化する必要があります。
class Device {
private:
const int model_number; // constなので後から代入不可
int& system_ref;        // 参照なので作成時に紐付けが必要

public:
// これらは必ず初期化リストで書く必要があります
Device(int num, int& ref) : model_number(num), system_ref(ref) {
}
};

こうした制約があるからこそ、「基本は初期化リストを使う」という習慣を身につけておくことが、安全なコードを書くためのエンジニアとしての心構えだと私は思っています。

初期化の「順序」に潜む罠

ここで、私が過去にやってしまった一番の失敗談をお話ししますね。

メンバ初期化リストに書く順番を、メンバー変数の「宣言順」と変えてしまったんです。
実はC++において、メンバー変数が初期化される順番は、「クラス内での宣言順」であって、初期化リストに書いた順番ではありません。

class BadExample {
private:
int a;
int b;

public:
// リストではbを先に書いているけれど、実際はaが先に初期化される!
BadExample(int val) : b(val), a(b + 1) {
// このとき、aにはまだ初期化されていないbの値が使われ、バグになる
}
};

このコードでは、a を b + 1 で初期化しようとしていますが、宣言順では a が先なので、まだゴミデータが入っている b を使って計算してしまいます。

コンパイラが警告を出してくれることもありますが、これに気づかず出荷直前のテストで動作が不安定になり、冷や汗をかいたことがあります……。

それ以来、私は「宣言順と初期化リストの順序を必ず一致させる」というのを、自分の中の鉄則にしています。

モダンなC++なら「メンバ変数の初期値」も活用しよう

C++11以降では、クラスの宣言の中で直接初期値を書くこともできるようになりました。

class ModernDevice {
private:
int status = 0; // ここでデフォルト値を指定できる
std::string type = "Generic";

public:
ModernDevice() {} // statusは0, typeは"Generic"で初期化される
ModernDevice(int s) : status(s) {} // statusはsで上書き、typeは"Generic"
};

コンストラクタが複数ある場合、共通の初期化をここで行っておくと、書き忘れを防げて非常に安心です。

「初期化リスト」と「宣言時初期化」をバランスよく使い分けるのが、現代的なエンジニアの流儀と言えるでしょう。

まとめ:PCと仲間に優しい初期化を

C++のコンストラクタ初期化について、いくつかのポイントを見てきました。

基本は「メンバ初期化リスト」を使う。(無駄がなくて効率的)
constや参照型など、リストでしか初期化できないものがある。
初期化順序は「宣言順」であることを絶対に忘れない。(失敗談より)
C++11以降なら宣言時の初期化も併用して、書き漏らしを防ぐ。

たかが初期化、されど初期化です。
こうした基礎を一つひとつ丁寧に積み重ねることで、バグが少なく、他の人が読んでも安心できるコードが生まれます。

皆さんも、今日からコンストラクタを書くときは、コロン(:)の後の世界を少しだけ意識してみてくださいね。
その丁寧さが、きっと将来の自分やチームを助けることになるはずです。

それでは、また次の記事でお会いしましょう。リーダーでした。

コメント

タイトルとURLをコピーしました