野良C++erの雑記帳 このページをアンテナに追加 RSSフィード Twitter

2013-12-11 いやぁ,艦これって面白いですね.

std::make_shared から private コンストラクタを呼び出す

std::make_shared で private コンストラクタを呼ぶには,関数内クラスと継承を利用すれば良い.

class hoge {
  hoge();  // private ctor

 public:
  static std::shared_ptr<hoge> create() {
    // return std::make_shared<hoge>();  // こう書きたいが,コンパイルエラー
    // 代わりに関数内クラスを利用する
    struct impl : hoge {
      impl() : hoge() {}
    };
    auto p = std::make_shared<impl>();
    return std::move(p);
  }
};

継承ではなく包含を使うことも可能だし,関数内クラスではなくクラス内クラスを使うことも出来る.

class hoge {
  hoge();  // private ctor

  struct create_helper {
    hoge x;
    // クラス内クラスだとテンプレートにできるのでテンプレートで書いてみる
    template<class... Args>
    explicit create_helper(Args&&... args)
        : x(std::forward<Args>(args)...) {
    }
  };

 public:
  static std::shared_ptr<hoge> create() {
    auto p = std::make_shared<create_helper>();
    return std::shared_ptr<hoge>(std::move(p), &p->x);
  }
};

要するに, private コンストラクタが見えているところに補助クラスを定義し,そいつを make_shared した後,補助クラス内部にある目当てのオブジェクトへの shared_ptr を作ればよい.


以下,補足説明.


そもそも std::make_shared は,「オブジェクトを new して std::shared_ptr に格納する」という処理を,安全かつ効率的に行う関数テンプレートである.

struct hoge {
  // ...
};

void f(std::shared_ptr<hoge> p1, std::shared_ptr<hoge> p2);

int main() {
  auto p = std::make_shared<hoge>();  // これは
  // std::shared_ptr<hoge> p(new hoge());  // これと同じだが,より効率的

  f(std::shared_ptr<hoge>(new hoge()), std::shared_ptr<hoge>(new hoge()));  // メモリリークの可能性あり
  f(std::make_shared<hoge>(), std::make_shared<hoge>());  // 例外安全
}

しかし,この make_shared は,呼び出したいコンストラクタが private で定義されていた場合,たとえクラス内からであろうと,直接 make_shared 経由でオブジェクトを構築させることはできない.

// shared_ptr に格納されることが前提のクラス
class hoge {
  hoge();  // private ctor

 public:
  static std::shared_ptr<hoge> create() {
    return std::shared_ptr<hoge>(new hoge());  // make_shared<hoge>() だとコンパイルエラー
  }
};

これは,たとえ make_shared がクラス内で使われていようとも, make_shared 関数の中でコンストラクタが呼ばれる文脈は,構築したいクラス(先程の例では hoge)とは無関係のものであるためだ.


ならば friend 関数宣言を使えば良い,と思うかもしれないが,

  • コンストラクタ呼び出しが std::make_shared 直下で行われているとは限らない(make_shared の内部で実装用関数を呼んでいるかもしれない
  • std::make_shared のシグネチャが規格通りとは限らない(enable_if などを使っている場合にはシグネチャは一致しなくなる

といった理由で, friend 関数は相応しくないし,仮に friend 関数にすることができても,その場合 クラス外部からも make_shared できてしまうため,モジュール化を壊してしまう.


そのため,今回 紹介したような,補助クラスを用いた小技が必要になってくるのである.

2013-06-20 Exceptional C++11 の出版が望まれる

「 Copy して Swap 」 対 「 Copy して Move 代入」

例外安全なコピー代入演算子を定義しようとしたとき, C++11 では

  • Copy して Swap する方法
  • Copy して Move 代入する方法

の二通りが存在するので,それぞれのメリットとデメリットを比べてみた.


Copy して Swap

昨日の記事で説明した方法.

struct Hoge {
  std::vector<int> x, y;

  Hoge() = default;
  Hoge(std::vector<int> x_, std::vector<int> y_)
    : x(std::move(x_)), y(std::move(y_)) {}

  Hoge(Hoge const&) = default;
  Hoge(Hoge &&) = default;

  Hoge& operator=(Hoge rhs /*pass by val*/) noexcept {
    this->swap(rhs);
    return *this;
  }

  void swap(Hoge& other) noexcept {
    using std::swap;
    swap(x, other.x);
    swap(y, other.y);
  }
  friend void swap(Hoge& l, Hoge& r) noexcept {
    l.swap(r);
  } 
};

メリット:

  • 代入演算子を一つ定義すれば済む
  • C++98/03 でも全く同じように書ける
  • メンバ変数が Move 代入をサポートしていない場合でも, Swap さえあれば問題ない
  • Copy/Move コンストラクタと Copy/Move 代入演算子の挙動が自然に一致する

デメリット:

  • 手動で Swap を定義しなければいけない(メンバ変数の追加に弱い)
  • 単純な Move 代入と比べて処理が多くなる傾向がある(最適化を考慮しない場合)

Copy して Move 代入

過去の記事で提案した方法.

struct Hoge {
  std::vector<int> x, y;

  Hoge() = default;
  Hoge(std::vector<int> x_, std::vector<int> y_)
    : x(std::move(x_)), y(std::move(y_)) {}

  Hoge(Hoge const&) = default;
  Hoge(Hoge &&) = default;

  Hoge& operator=(Hoge&&) = default;  // デフォルト定義された Move 代入演算子
  Hoge& operator=(Hoge const& rhs) {
    *this = Hoge(rhs);  // コピーコンストラクタを呼び,その結果を Move 代入
    return *this;
    // return *this = Hoge(rhs); と,一行で書くこともできる
  }
};

メリット:

  • 自動生成された代入演算子を使うため,メンバ変数の追加に強い
  • 自前の Swap を用意する必要がない(汎用の std::swap で十分に効率的)
  • Copy して Swap した場合と比較し,無駄な処理が少ない

デメリット:

  • Move 代入演算子の定義を忘れると無限ループ
  • Move 代入を = default; で定義できない場合も しばしば存在し,そのような場合,
  • 状況に応じて例外指定を行う必要がある
  • Move 代入演算子が例外を投げうる場合には全く意味が無い

使い分け基準の提案

Copy して Move 代入するケース

  • メンバ変数が多い,またはメンバ変数構成が変更される可能性が高い場合
  • ただし,コピー操作が例外を投げない場合,またはメンバ変数が一つしか存在しない場合には,
    Copy/Move 関連のコンストラクタ/代入演算子は明示的に書かず,自動生成されたものを使う方が良い

Copy して Swap するケース

  • Copy/Move コンストラクタを = default; で定義できない場合(典型的にはデストラクタをユーザ定義する場合*1
  • Swap が例外を投げないことは分かってるが, Copy が例外を投げうるか否かが よく分からない場合
  • メンバ変数Swap には対応しているが Move には対応していない場合(C++98/03時代のクラスを使う場合)
  • C++11 ではなく C++98/03 を使う場合,または Move 代入に対する = default; 指定に対応していないコンパイラ*2を使う場合

ともあれ,例外を投げうるコピーおよびムーブならびに代入操作は滅ぶべきであると考える次第である.

*1:デストラクタが絡む場合の他には, Move 後のオブジェクトに対しても不変条件が満たされることを保証したい場合にも,一般に Move 操作はユーザ定義する必要がある

*2GCC だと 4.4 以前

2013-06-18 祝・ Clang-3.3 リリース!

代入演算子のエレガントな定義方法と,その不満点

ユーザ定義されたクラスに対して 代入演算子を定義する場合,

コンパイラの生成するデフォルトの代入演算子では,例外安全の強い保証を満たせない場合がある.

struct Hoge {
  std::vector<int> x, y;

  Hoge() = default;
  Hoge(std::vector<int> x_, std::vector<int> y_)
    : x(std::move(x_)), y(std::move(y_)) {}

};

int main() {
  Hoge a = {{1, 2, 3}, {4, 5}};
  Hoge b;

  try {
    b = a;  // 例外安全の強い保証を満たせない
    // なぜなら,この操作は
    // b.x = a.x;
    // b.y = a.y;
    // と同じであり, b.y = a.y; で例外が投げられた場合
    // b.x は代入されたが b.y は代入されないまま残るからである
  } catch(...) {
    // ここで b は中途半端な状態になっているかもしれない
  }
}

無論, C++11 を使う場合には,例外を投げうる代入演算子は書くべきではないのだが,

人の書いたコードを使う場合など,例外はいくらでもあるし,

事情により C++03 を使わざるを得ない場合には, move が使えない以上,どうしようもない.


このような場合に例外安全な代入演算子を定義する方法として,かの Exceptional C++ では,

「copy and swap」技法という手法が提案されていた.

これは,例外を投げない swap を実装し,それを使って代入演算子を定義する方法である.

struct Hoge {
  std::vector<int> x, y;

  Hoge() = default;
  Hoge(std::vector<int> x_, std::vector<int> y_)
    : x(std::move(x_)), y(std::move(y_)) {}

  Hoge(Hoge const&) = default;
  Hoge(Hoge &&) = default;

  Hoge& operator=(Hoge const& rhs) {
    Hoge(rhs).swap(*this);  // copy and swap
    return *this;
  }
  Hoge& operator=(Hoge&&) = default;  // これは例外安全なので問題ない
                                      // ただし move 代入が例外を投げうるケースは極希に存在するため,
                                      // そのような場合には Hoge(std::move(rhs)).swap(*this); で実装する必要あり

  void swap(Hoge& other) noexcept {
    using std::swap;
    swap(x, other.x);
    swap(y, other.y);
  }
  friend void swap(Hoge& l, Hoge& r) noexcept {
    l.swap(r);
  } 
};

これは極めて優秀な方法であるが,問題がないわけではなく,

C++11 の場合,うっかり move 代入演算子の定義を忘れてしまうといったケース*1が考えられる.


その問題に対処する方法として,代入演算子引数を値渡しにする,というものがある.

struct Hoge {
  std::vector<int> x, y;

  Hoge() = default;
  Hoge(std::vector<int> x_, std::vector<int> y_)
    : x(std::move(x_)), y(std::move(y_)) {}

  Hoge(Hoge const&) = default;
  Hoge(Hoge &&) = default;

  Hoge& operator=(Hoge rhs /*pass by val, copy/move is done here*/) noexcept {
    this->swap(rhs);
    return *this;
  }
  // Hoge& operator=(Hoge&&) = default;  // もはや必要ない

  void swap(Hoge& other) noexcept {
    using std::swap;
    swap(x, other.x);
    swap(y, other.y);
  }
  friend void swap(Hoge& l, Hoge& r) noexcept {
    l.swap(r);
  } 
};

上記のコードは C++11 スタイルで書いたが,この書き方は C++03 でも通用する*2ため,

この書き方は,例外安全性のみならず,移植性にも優れている.


また,オリジナルのコードでは operator= が noexcept 指定されていない(関数内部で例外が投げられうる)のに対し,

値渡しを使ったコードでは, operator= が noexcept 修飾されている(関数内部で例外は投げられない).

これは,実際のコピー代入処理で例外が起きる可能性が存在しなくなった,という意味ではなく,

例外の投げられうる処理(コピーコンストラクタ呼び出し)が関数引数部分に追い出された,ということであり,

noexcept 演算子等を使ってコピー代入時に例外が投げられるか否かを判別した場合には,正しく判断される.


この挙動は,実装者にとって,極めて都合のいいものである.

特にテンプレートを扱った場合には,代入演算子に対する noexcept 指定を行うのは かなり面倒なので*3

それを避けられるだけでも,この手法を使う価値は十二分にあると言っていいだろう.


総じて,この「値渡しを使った copy and swap 」は,オリジナルの const 参照を使った方法より優れており,

例外安全などの理由で代入演算子を自前で用意しなければいけない場合には,こちらを使う方が良いと考えられる.




* * * * *

(以降,マニア向けの内容)

* * * * *


さて,値渡しを使った copy and swap は,極めて便利なのだが,難点も存在する.

それは,値渡しというよりは copy and swap 技法全般に当てはまることであり,

swap を自前で定義しなければいけない,という,言ってしまえば当たり前のものだ.


しかし,当たり前だということは,それが些細な問題であるということを意味しない.

swap を自前で定義することの問題点は,

  • メンバ変数が増えた場合に,その都度 swap メンバ関数を書き換えて対応する必要がある
  • メンバ関数 swap が定義されたクラスを継承した場合に,自前の swap を定義し忘れる可能性がある

という二点なのだが,いずれも厄介な問題なのだ.


これらの問題点への対応を忘れた場合,オブジェクトが部分的に更新されないまま残ることになり,

そうなった場合,往々にして「オブジェクトが たまに奇妙な振る舞いをするが,原因はよく分からない」という状況に陥る.

自動生成された代入演算子では このような事態は起きにくいことを考えると,なかなかに難しい問題である*4


これらの問題を解決するために, C++1y (not C++14) では

コンパイラによって自動で定義される swap 演算子の導入が検討されている.

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3553.pdf

この提案が採用されるかどうかは不透明だが, swap は copy and swap 技法以外にも よく使われる為,

個人的には,互換性の許す限り(そこがネックだが),是非とも導入されて欲しいと考えている.


また,それとは全く別に,うろ覚えで この技法を使った場合に起き得る問題点も存在する

具体的には,代入演算子の実装に メンバ関数版の swap ではなく 非メンバ関数版の swap を使った場合,

自前の swap の定義をうっかり忘れてしまうと,無限ループとなる可能性が出てきてしまうのだ.

struct Hoge {
  std::vector<int> x, y;

  Hoge() = default;
  Hoge(std::vector<int> x_, std::vector<int> y_)
    : x(std::move(x_)), y(std::move(y_)) {}

  Hoge(Hoge const&) = default;
  Hoge(Hoge &&) = default;

  Hoge& operator=(Hoge rhs) noexcept {
    using namespace std;
    swap(rhs, *this);  // 汎用の std::swap を呼ぶ. その場合,内部で Hoge::operator= が呼ばれて無限ループ
    return *this;
  }

  // swap の定義を忘れた
};

このような事態を避けるため,この手法を使う場合には,必ずメンバ関数swap を呼ぶべきである.


ただし,この手法以外の文脈では,一貫して 非メンバ関数swap を使うほうがよい.

組み込み型とクラスで同じ書き方が可能である,というだけではなく,

継承を使って派生クラス側で swap の定義を忘れた場合でも,非メンバ関数を使えば,危険は減るからだ.

struct Base {
  std::vector<int> member;

  void swap(Base& x) {
    member.swap(x.member);
  }
  friend void swap(Base& x, Base& y) {
    x.swap(y);
  }
};
struct Derived {
  int additional_member;
  // swap は定義されない
};

int main() {
  Derived x, y;
  // x.swap(y);  // Base::swap が呼ばれる. additional_member は交換されない
  using namespace std;
  swap(x, y);  // std::swap が呼ばれる. 全メンバが交換される
}

この問題点は,現状の C++ では「可能な限り非メンバ関数を使う」ことでしか対処できないが,

新たに言語機能として

「あるメンバ関数に対して,『このメンバ関数は継承されない』*5という旨を宣言できる」

機能が追加されれば,改善される可能性がある.

// 注: このコードは「こう書けたらいいなら」というものであり, C++ コードではない
struct Base {
  std::vector<int> member;

  void swap(Base& x) uninherited {  // 『このメンバは継承されない』. 注: C++ では こうは書けない
    member.swap(x.member);
  }
  friend void swap(Base& x, Base& y) {
    x.swap(y);
  }
};
struct Derived {
  int additional_member;
  // swap は定義されない
};

int main() {
  Derived x, y;
  x.swap(y);  // コンパイルエラー, swap は継承されない
}

こういった機能も, C++1y では是非とも導入されて欲しいものだ.

*1コンパイルエラーにならないまま効率が悪くなるのでタチが悪い

*2:ただし noexcept 指定は消す必要がある

*3noexcept(std::is_nothrow_copy_assignable<T>::value) とか書く必要がある

*4:余談ではあるが,この問題点を解決する方法として, C++11 では copyしてmove することでコピー代入演算子を定義する方法もある. ただ,その場合, noexcept 指定は自前で行わなければならず,しばしば面倒である.

*5:…と書くと曖昧なので,仮に規格に入った場合には『このメンバ関数は派生クラスで暗黙のうちに隠される』となると思われる

2013-06-04 スバル=サンのアトマワシ・ジツ!

C++11 時代のクラス設計に関する提案

先日,ついに C++11 の主要な機能を一通り実装した GCC-4.8.1 がリリースされた

もう一方の主要な C++ コンパイラである Clang++ でも C++11 の機能は既に全て実装されており,

来る 6/05 に最新版の Clang-3.3 がリリースされ, C++11 対応が完了する見通しだ.*1


このような状況においては, C++11 への乗り換えを検討し始めているプロジェクトも多いことだろう.


さて, C++11 では, C++98/03 との互換性を保ちつつ,クラス設計に大きな影響を齎す変化が採用された.

すなわち, Move Semantics である.

この登場により, C++11 で「良い」とされるクラス設計は, C++98/03 時代とは若干 異なったものとなる.


そこで,この記事では,筆者が C++11 において「良い」と考えているクラス設計を提案してみたい.


なお,この記事で行われている主張は,あくまで筆者 個人の考えである.

状況によっては相応しくない場合もあるし,汎用的に使える より良いクラス設計方針が存在する可能性を否定するものではない.

現に, C++11 の標準ライブラリでは,互換性の都合から,これらの原則は必ずしも満たされていない.

しかし, C++11 を使って安全にコードを書く場合には,今 考えられる中では最良に近いと自負している.


* * * * *


さて,その提案であるが,端的に言えば

「クラスの特殊メンバ関数swap および暗黙の型変換は,例外を投げないようにしよう」

となる.


特殊メンバ関数とは,明示的に定義しなかった場合に自動で生成されるメンバ関数のことであり,

の6つである.


これらと暗黙の型変換は,オブジェクトに対して操作を行う時,何かと呼び出される関数*2であり,

これらの関数が例外を投げうる(つまり,失敗する可能性がある)場合には,コードのパスが複雑になる恐れがある.

struct Hoge {
  Hoge();  // デフォルトコンストラクタ
  Hoge(Hoge const&);  // コピーコンストラクタ
  Hoge& operator=(Hoge const&);  // コピー代入演算子
  Hoge(Hoge &&);  // ムーブコンストラクタ
  Hoge& operator=(Hoge &&);  // ムーブ代入演算子
  ~Hoge();  // デストラクタ

  void swap(Hoge& other);  // swap (メンバ関数版)
  friend void swap(Hoge& one, Hoge& another);  // swap (非メンバ関数版)

  Hoge(int);  // 暗黙の型変換コンストラクタ
  operator double() const;  // 暗黙の型変換演算子

  // ...

};

void f(Hoge);

int main() {
  Hoge x, y;  // デフォルトコンストラクタが呼ばれる
  f(x);  // コピーコンストラクタが呼ばれる
  x = y; // コピー代入演算子
  f( Hoge() );  // (デフォルトコンストラクタと)ムーブコンストラクタ
  x = 0;  // 暗黙の型変換とムーブ代入演算子
  std::vector<Hoge> vec = { 1, 2, 3 };  // 暗黙の型変換
  std::sort( vec.begin(), vec.end(),  // sort 内部で swap
    [] (Hoge const& x, Hoge const& y) {
      double x_ = x, y_ = y;  // 暗黙の型変換
      return x_ < y_;
    }
  );
  // デストラクタはそこらじゅうで呼ばれてる
}

これら全てで例外を意識するのは無理がある*3し,

例外を投げうる操作というのは得てして実行速度が そこまで高速ではない*4ので,

そういう操作がコードからは見えにくい部分で行われるのは あまり良くないんじゃないか,という筆者の主張だ.


個別に解説していこう.


まずデストラクタ. 例外を投げるデストラクタは害悪である.

これは当たり前だし,探せば解説は いくらでもあるので,特に解説しない.

少なくとも,例外を投げうるデストラクタが存在した時点で,そのクラスは

標準ライブラリではマトモに使えなくなるし,そんな代物を俺々ライブラリで扱える訳がない.

というか,殆どの場合,デストラクタは暗黙の例外指定を持つため,

デストラクタで例外が投げられた場合には,デストラクタの外に伝搬せず,

std::terminate が呼ばれて プログラムは即座に終了してしまう.

そんなこんなで,例外を投げるデストラクタはあってはならないものなのだ.


次, swap

これは Exceptional C++ を読めば分かる通り,例外を投げない swap がないと,

例外安全を満たしたコードを書くのが困難になる.

とはいえ, C++11 では, swap を使っていた技法の一部は Move Semantics に置き換えられるし,

そもそも swap 自体が Move Semantics から自動生成されるようになったので,ここでは特に解説しない.

興味が有る方は Exceptional C++ を読むといいだろう.


その次,ムーブコンストラクタとムーブ代入演算子

これは本当に頻繁に呼ばれるものなので,これらが例外を投げるとコーディングは かなり困難になる.

特に, Move 周りが例外を投げうる場合,複数のオブジェクトを Move しようとした場合に

例外安全の強い保証を満たせなくなってしまう,という問題点があり,

特に std::vector のようなコンテナの場合には,複数のオブジェクトを移動する際に Move が使えず,*5

代わりにコピーが行われることで,かなり効率が悪くなってしまうケースがある.

これは,内部の処理で Move が行われるか否かが,外から見て区別できないため,

非効率的な状態に気付けない,という意味で,非常に厄介な挙動である.

また, Move が例外を投げうる場合, swap も例外を投げうるということになり,

色々な操作に対して,例外安全の強い保証を満たすことが困難になる,という問題もある.


更に次,暗黙の型変換.

これは型変換コンストラクタと型変換演算子の二種類があるが,どちらも例外を投げるべきではない.

C++ というのは本当に自然に暗黙の型変換が行われる言語なので,

それらの処理の負荷が大きい場合,気付かないうちにプログラムのコストを上げてしまう.

template<class T>
struct my_vector {
  std::vector<T> data;

  // src の中身を変換する型変換コンストラクタ
  // サイズによっては当然 重い処理になる
  template<class U>
  my_vector( my_vector<U> const& src )
    : data( src.data.begin(), src.data.end() ) {}

};

void f(my_vector<std::string> const&);

int main() {
  my_vector<char const*> vec;
  vec.data = 〜;
  f(vec);  // 暗黙の型変換が行われ,要素ごとにメモリ確保が行われる
           // vec の型が my_vector<std::string> だった場合と比べて効率悪い
}

もちろん,

void f(std::string);
f("hoge");  // 動的メモリ確保を行う(例外を投げうる)暗黙の型変換だが,読みやすさの点では優れている
// f(std::string{"hoge"});  // こう書くのは いかにもダサい

のような便利なケースもあるので,一概に否定はできないが,

それでも,自分で設計する時は,こういう暗黙の型変換も避けるのが良いだろう.


次,デフォルトコンストラクタ

これは特に強い理由はない.

強いて言えば,デフォルトコンストラクタは,スコープの関係などで

「とりあえず変数を定義しておく」場合に よく使われるので,その処理の負荷が大きいと困る,程度だ.

// とりあえず変数作って
std::function<void()> f;
{
  // あとで代入
  auto p = std::make_shared<Hoge>();
  f = [p] { p->do_something(); };
}
// 使う
f();

上記のような例も,ラムダ式を使って

auto f = []() -> std::function<void()> {
  auto p = std::make_shared<Hoge>();
  return [p] { p.do_something(); };
};
f();

と書けば良いし, C++14 においては std::optional の存在も有るので,特に神経質になることはない.

が,デフォルトコンストラクタに対して例外を投げないようにするのは割と楽なので,

どうせなら守るのも手では有ると思う. 少なくとも損はしない.


最後に,コピーコンストラクタとコピー代入演算子. この記事の本題である.


std::vector や std::string , std::function のように, C++ には

コピー時とムーブ時で処理の効率が大きく違うクラスが多いが,これは問題が有ると筆者は考える.


というのも,コピーという操作は, C++ ではごく自然に行われる操作であり,

ちょっと std::move を忘れれば即座にコピーが行われ,

しかもコピーが行われたことはコードを注意して読み直さない限り気付けない からだ.


以下のコードを見てほしい.

void f( std::vector<int> const& x );
void g( std::vector<int> x );

int main() {
  std::vector<int> vec( 1000000 ); // でっかい vector
  f( vec );  // const 参照渡し,効率的
  g( vec );  // 値渡し,効率悪い
}

fg関数呼び出しは,呼び出す側からは全く同じに見えるが,

実際の処理は(もちろん関数の中身にもよるが)かなり効率が違ってくる.

このような場合に「うっかりコピーしないよう」気を付けるのは,かなりストレスの元になるし,

せっかく C++ を使っているのだから,そういうのはコンパイル時に検出したいのだ.


もし仮に, std::vector が今回の提案に従い,コピー時に例外を投げないようにしていた場合,

そもそもコピーが定義されなくなる*6ので,

void f( std::vector<int> x );

int main() {
  std::vector<int> vec( 1000000 ); // でっかい vector
  f( vec );  // 値渡し,効率悪い
}

のようなケースはコンパイルエラーにすることが可能になる.

もしコピーを行いたい場合には,明示的にコピーを行う関数を用意すればいい.

void f( std::vector<int> x );

int main() {
  std::vector<int> vec( 1000000 ); // でっかい vector
  f( duplicate(vec) );  // 明示的にコピーする
}

値渡しを行う場合は, duplicate か move を使い分けることになる.

void f( std::vector<int> x );

int main() {
  std::vector<int> vec( 1000000 ); // でっかい vector
  f( duplicate(vec) );  // 明示的にコピーする
  f( move(vec) );  // 明示的にムーブする
}

これなら間違いも減るだろうし,

先ほどの「Move 周りが例外を投げうる場合, Move の代わりにコピーが行われて効率が悪くなる」ケースも,

コピーの際に効率が劣化するような場合はコンパイルエラーにできるので,色々と都合がいい.


というわけで,このルール,興味ある人は守ってみたら如何だろうか.

明示的なコピーが言語側でサポートされてないので,微妙に扱いにくい部分はあるが,

慣れてくると間違いが減らせて非常に便利なのは間違いない. 特に暗黙変換とコピー.


ちなみに,例外を投げうるようなコピーや暗黙変換を禁止した場合,

std::string や std::function が自由にコピーできなくなって地味に面倒なのだが,

これらのクラスは immutable なクラスとして再設計すれば,コピー時に例外を投げなくなる.

auto s = string{"hoge"};  // メモリ確保を伴うので暗黙変換は禁止
auto t = s;  // OK, 浅いコピーを行う
s[0] = 'a'; // NG, immutable なので変更できない

auto f = function<void()>( []{} );
auto g = f;  // OK, g と f は内部の関数オブジェクトを共有する
int count = 0;
auto h = function<void()>( [count] () mutable { ++count; } );  // NG, mutable な関数オブジェクトは格納できない

ただ,暗黙変換とか絡むと, string はともかく function は少々面倒なので,ちょっとどうしたものか.

*1:ただし,日本時間では日付が変わっている可能性は普通にある.

*2:「関数」より「機能」の方が相応しい表現かもしれない. なお,どちらも英語では function である.

*3:もちろん RAII を使えば大抵のケースでは問題ないが, RAII に頼れないケースも意外と多く,その場合には見落としは致命的になる.

*4:例えば動的メモリ確保は例外を投げうる操作の典型例である. とはいえ動的メモリ確保自体は そこまで効率の悪い処理ではない(むしろ,その後,動的に確保した領域に書き込む操作が問題になる)が,スタック上の変数操作に比べたら遅いのは間違いない.

*5:参考: http://d.hatena.ne.jp/gintenlabo/20110117/1295281933

*6:std::vector のコピーをメモリ確保なしに行うのは無理である

2013-06-03 ――戦いは終わった. ありがとう無職生活,こんにちは上司.

Jubatus の開発に加わりました

6月から無職を辞めて派遣社員になりました.


勤務する会社(派遣先)は,武蔵野にある NTT ソフトウェアイノベーションセンタで,

機械学習分散処理フレームワーク Jubatus の開発チームの一員として働かせて頂きます.


やる仕事は,当面は C++ の知識を活かしたバグ対応やコード改善となります.

機械学習の方は,大学で少しかじった程度で,正直 素人なのですが,

いずれはそちらもマスターして,新機能の開発や, blog での解説など,行なっていければと思います.


というわけで,機械学習関連でオススメの書籍とかありましたら,是非お教え下さい.

ちなみに当方は7月半ばまで金欠であり,また 6月17日 は誕生日となっていますが,

単に事実を書いただけであり,他意はまったくありませんのでごあんしんください.*1

*1:もちろん,これは あくまでも半分くらい冗談であって,Amazonのウィッシュリストを無遠慮にも貼り付けるが如き奥ゆかしさの欠ける行為とは違い,青少年のなんかも実際 守られるためもんだいありません. わかったか.