6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[C++] コンストラクタの書き方

Posted at

そもそもコンストラクタは必要か?

集積体初期化を使えるなら、あえてコンストラクタを書かない選択肢もあります。

struct Foo final
{
  int         m_val_1 {} ;
  double      m_val_2 {} ;
  std::string m_val_3 {} ;
};
const Foo obj{ 1, 1.5, "a" };

冗長な記述が不要となるのが、メリット。

ただし、デメリットもあります。

  • 使用するのに、条件が有る
    • 全てのメンバ変数が public である事、など
  • 引数の数が足りなくてもビルドが通る
const Foo obj{ 1, 1.5 }; // 数が合わないが、ビルドは通る。

基本的な書き方

struct Foo final
{
public:
  explicit Foo( const int val )
    : m_val{ val }
  {}
private:
  int m_val {} ;
};

explicit は、常に付ける様にした方が良いかと思います。

  • 引数が1つの時に、不意に変換コンストラクタとして機能するのを防ぐため。
    • 引数が1つの時のみ付けるだと、注意を払うべき事象が無駄に増えてしまう。

なおに、メンバ変数と仮引数が同名でも構いません。

struct Foo final
{
public:
  explicit Foo( const int val )
    : val{ val } // 左側(波括弧の外)は関数のスコープ外なので、メンバ変数を指す。
                 // 右側(波括弧の中)は関数のスコープ内なので、関数の引数を指す。
  {}
private:
  int val {} ;
};

move でコピーコストを減らす

コピーコストが重いメンバ変数を保つ場合、
右辺値参照を取るコンストラクタを用意する事でコピーコストが減らせます。

struct Foo final
{
public:
  explicit Foo( const std::vector<int>& val )
    : m_val{ val }
  {}

  explicit Foo( std::vector<int>&& val )
    : m_val{ std::move( val ) }
  {}
private:
  std::vector<int> m_val;
};
const std::vector<int> ary = GetAry() ;
Foo obj_1{ ary      } ; // vector はコピーされる。
Foo obj_2{ GetAry() } ; // vector はムーブされる。

ただし、メンバ変数1つに付き2倍の数のコンストラクタが必要になるので、メンテが大変になります。

ムーブのコストが軽いのなら、引数を値渡しにした方が良いでしょう。

struct Foo final
{
public:
  explicit Foo( std::vector<int> val )
    : m_val{ std::move( val ) }
  {}
private:
  std::vector<int> m_val;
};
const std::vector<int> ary = GetAry() ;
Foo obj_1{ ary      } ; // vector はコピーされた後、ムーブされる。
Foo obj_2{ GetAry() } ; // vector はムーブされた後、ムーブされる。

const参照を持つ場合

const参照なメンバ変数を持つ場合、簡単には以下の様にする。

struct RefKeeper
{
  explicit RefKeeper( const double& val )
    :m_ref{ val }
  {}

  const double& m_ref;
};

が、これだと、コンストラクタで右辺値も取れてしまいます。

RefKeeper obj{ 7.53 };
obj.m_ref; // ダングリング(i.e.寿命切れ)

なので、右辺値を取るオーバーロードを用意し、
delete することで、これを防げます。

struct RefKeeper
{
  explicit RefKeeper( const double& val )
    :m_ref{ val }
  {}
  explicit RefKeeper( const double&& val ) = delete ; // 右辺値禁止!

  const double& m_ref;
};

条件チェックを行う

コンストラクタで事前条件のチェックを行いたい時、
例外を投げるのは、色々な面で扱いづらいです。

  • 投げる例外を決める必要がある, チェック漏れされやすい, 値をconstにしづらい、など。

この場合、コンストラクタで private にし、チェック付きの static 関数でラップする事で、
optional にして返す事が出来る様になります。

#include <optional>

// どんな時でも、ポジティブバリュー
struct PositiveValue
{
public:
  static std::optional<PositiveValue> Create( const int val ) noexcept {
    if ( val <= 0 ) { return std::nullopt ;}
    return PositiveValue{ val };
  }
private:
  explicit PositiveValue( const int val )
    : m_val{ val }
  {}
private:
  int m_val {};
};
const auto obj_1 = PositiveValue::Create( 5 ) ;
const auto obj_2 = PositiveValue::Create( 0 ) ; // nullになる。

メンバ変数の複雑な初期化

以下の様な、コンストラクタで平均と分散を計算するクラスを考える。

  • 分散の計算には、平均が必要な事に留意。
struct Foo final
{
public:
  explicit Foo( const std::vector<int>& ary )
    : m_average { ... }, m_variance{ ... } // コンストラクタで、引数 ary の平均,分散を計算する。
  {}
private:
  double m_average {}; // 平均
  double m_variance{}; // 分散
};

以下の様に、メンバの初期化で他のメンバを使う事も出来ますが、
メンバ変数の宣言順を変えると未初期化の変数を使ってしまう恐れがあります。

struct Foo final
{
public:
  explicit Foo( const std::vector<int>& ary )
    : m_average { CalcAverage( ary ) }
    , m_variance{ CalcVariance( ary, m_average ) } // メンバ変数の宣言順に注意!
  {}
private:
  double m_average {}; // 平均
  double m_variance{}; // 分散
};

以下の様に内部クラスを別途用意すると、複雑な計算を伴う初期化が行いやすくなります。

// 宣言
struct Foo final
{
public:
  explicit Foo( const std::vector<int>& ary ) ;
public:
  struct Data final
  {
    double average {}; // 平均
    double variance{}; // 分散
  } ;
private:
  Data m{};
};

// 実装
Foo::Data Init( const std::vector<int>& ary )
{
  const auto average  = CalcAverage( ary ) ;
  const auto variance = CalcVariance( ary, average ) ;
  return { average, variance };
}

Foo::Foo( const std::vector<int>& ary )
  : m{ Init( ary ) }
{}

継承が有る場合に集積体初期化を使う(C++14)

C++17 で改善された が、何らかのインターフェースを継承していると、
集積体初期化は利用出来なくなってしまいます。

struct Foo : public IF
{
  int         m_val_1 {} ;
  double      m_val_2 {} ;
  std::string m_val_3 {} ;
};
// Foo obj_1{ 1, 1.5, "foo" }; // ビルドエラー。
Foo obj_2{ {}, 1, 1.5, "foo" }; // C++17なら可,C++14ではビルドエラー。

内部クラス等でラップすれば、C++14でも集積体構築が利用出来ます。

struct Foo : public IF
{
public:
  struct Data
  {
    int         val_1 {} ;
    double      val_2 {} ;
    std::string val_3 {} ;
  };
  explicit Foo( const Data& m ) : m{ m } {}
private:
  Data m{};
};
Foo obj_1{ Foo::Data{ 1, 1.5, "foo" } }; // C++14でも可
Foo obj_2{          { 1, 1.5, "foo" } }; // 型名は省略可
6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?