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

2014-06-17 そういえば誕生日でした。

if(cond) と if(cond == true) は同じ意味ではない

if(cond)コンパイルは通るが、 if(cond == true) と書くとコンパイルが通らない例がある。

struct bool_ {
  bool value;
  constexpr explicit operator bool() const noexcept {
    return value;
  }
};

int main() {
  bool_ x{true};
  if (x == true) {
    // do something
  }
}

http://melpon.org/wandbox/permlink/jCXG8WMDTBLhvWI2


C++03 でも operator== のオーバーロードで似た問題が起きるケースが有ったが、 C++11 で explicit operator bool が登場し、広く使われるようになったことで、この問題に遭遇する機会が増えている。

魔黒などを使う場合には注意が必要だ。

2014-06-14 twitter の発言を blog で整理すれば皆ハッピーになれるじゃん?

データメンバを見ると循環参照してないのに shared_ptr のせいで循環参照になるケース


実例.

#include <memory>
#include <iostream>

struct hoge {
  hoge() {
    std::cout << "hoge::hoge()\n";
  }
  ~hoge() {
    std::cout << "hoge::~hoge()\n";
  }

  std::shared_ptr<int> p;
};

int main() {
  auto p = std::make_shared<hoge>();
  p->p.reset(new int(), [p](int*){ (void)p; });
}

http://melpon.org/wandbox/permlink/QoDsygLC97QtZYsz

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);
  }
};

[7/5, 2014 追記] クラス内クラスを使う場合は,クラス内クラスを定義する位置に気をつける必要がある.

class hoge {
  hoge();  // private ctor
  struct create_helper;

 public:
  static std::shared_ptr<hoge> create();
};

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

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

注意点として,包含を使った場合,現行の仕様・実装では std::enable_shared_from_this が上手く働かないため,

shared_from_this を使うのであれば継承を使う必要がある.

#include <memory>

class hoge : public std::enable_shared_from_this<hoge> {
  hoge(){}  // private ctor
  struct create_helper;

 public:
  static std::shared_ptr<hoge> create();
};

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

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

int main() {
  auto p = hoge::create();
  p->shared_from_this();  // error!
}

http://melpon.org/wandbox/permlink/Rt7WAdfEOsN8jR1w


とまぁ,細かい注意点はいろいろあるが,要するに, 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:…と書くと曖昧なので,仮に規格に入った場合には『このメンバ関数は派生クラスで暗黙のうちに隠される』となると思われる