2013年4月14日日曜日

Google Code Jam 2013 Qualification Round

結果

D-Large が Timeout した以外は全部提出しましたが、C-Large が両方落ちて結局 100pt 4217 位でした。落ちる前は 3 桁 rank だったので終了後スコア見たときには D-Large 解いた人そんないるの?と思ったりしてしまいました。C-Large は 15 桁くらいまで bruteforce したリストと照らし合わせた上だったので落ちると思っていなかったのです。が、まぁ、最終結果で確認しろってことですね(後述)。

前振り

今回も C++11 を使って書く、をコンセプトにしています。開始してから gcc version 4.8.1 20130328 (prerelease) (GCC) をダウンロードしてきて使っています。ついでにとりあえず普段は使わない 64 bit exe にしています。共通コードは以下のようなコードです。Boost も使用。Google Code Jam の規定用に URL とバージョンを明示しています。IR というのは range-base for loop 用のものです(Integer Range のイメージ)。for(auto i : IR(1, 6)) とかやると 1,2,3,4,5 でループされます。

// gcc version 4.8.1 20130328 (prerelease) (GCC) at http://www.drangon.org/mingw/
// with -std=c++11

#include <iostream>
#include <sstream>
#include <iomanip>

#include <iterator>

#include <algorithm>
#include <numeric>
#include <utility>
#include <limits>

#include <string>

#include <vector>
#include <deque>
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <stack>

#include <tuple>
#include <initializer_list>

#include <cmath>

// Boost library can be retrieved from http://www.boost.org/
// 1.52 is used

#pragma GCC diagnostic ignored "-Wconversion"
#include <boost/range/irange.hpp>
#include <boost/range/iterator_range.hpp>
#pragma GCC diagnostic warning "-Wconversion"

typedef unsigned long long ULL;
typedef long long LL;
typedef unsigned long UL;
typedef unsigned int UI;
typedef unsigned short US;
typedef unsigned char UC;

#define RNG(v) (v).begin(), (v).end()
#define REP(v, e) for(UI v = 0U; v < e; ++v)
#define REP_(v, s, e) for(UI v = s; v < e; ++v)
#define REPV(v, e) for(v = 0; v < e; ++v)
#define REPV_(v, s, e) for(v = s; v < e; ++v)

using namespace std;

template<class Integer>
inline boost::iterator_range< boost::range_detail::integer_iterator<Integer> >
IR(Integer first, Integer  last)
{ return boost::irange(first, last); }

Problem A. Tic-Tac-Toe-Tomek

縦横斜めで各コマの数を数えて判定。Small input の時は↓のように書きましたが、

int main(void)
{
 ios_base::sync_with_stdio(false);

 UI cases; cin >> cases; cin.ignore(numeric_limits<streamsize>::max(), '\n');
 REP(casenum, cases) {
  string result;
  int num = 0;
  int counter[4+4+2][3] = { 0 };
  REP(i, 4) {
   string s;
   getline(cin, s);
   REP(j, 4) {
    if(s[j] == '.') continue;
    int t = 0;
    ++num;
    switch(s[j]) {
    case 'X': t = 0; break;
    case 'O': t = 1; break;
    case 'T': t = 2; break;
    default: assert(false);
    }
    ++counter[  i][t];
    ++counter[4+j][t];
    if(  i == j) ++counter[8][t];
    if(3-i == j) ++counter[9][t];
   }
  }
  REP(i, 10) {
   if(counter[i][0] + counter[i][2] == 4) result = "X won";
   if(counter[i][1] + counter[i][2] == 4) result = "O won";
  }
  if(!result.size()) {
   if(num == 16) result = "Draw";
   else result = "Game has not completed";
  }
  cin.ignore(numeric_limits<streamsize>::max(), '\n');
  cout << "Case #" << casenum+1 << ": " << result << endl;
 }

 return 0;
}

「C++11で書いてないじゃないですかー」ということから一部修正して large input を submit

map<char, int> table = { {'X', 0 }, {'O', 1 }, {'T', 2 } };
// 略
    int t = table[s[j]]; // swtich のところを置き換え

書くだけならこうも書けますが

    int t = map<char, int>{ {'X', 0 }, {'O', 1 }, {'T', 2 } }[s[j]];

毎回 map 作られるのはどうかと思ったのでやめました。

Problem B. Lawnmower

高い方のマスから縦横の高さを確定していく、というコードで1回書いて、別に縦横両方ともその高さで刈る訳じゃないじゃんということで没。実際に提出したコードは↓。縦横で最大の高さをとって(これ以上低く刈ることはできない)、各マスに対して縦横の低い方を取った結果が指定に一致するか、で判定しています。これ C++03 でも通っちゃうんじゃ?と思いましたが、right angle bracket さん(template 指定としての >> の連続)がいるので大丈夫ですね。

int main(void)
{
 ios_base::sync_with_stdio(false);

 UI cases; cin >> cases;
 REP(casenum, cases) {
  UI N, M; cin >> N >> M;
  vector<vector<int>> board(N, vector<int>(M, 0));
  vector<int> h(N, 1), v(M, 1);
  REP(i, N) {
   REP(j, M) {
    cin >> board[i][j];
    h[i] = max(h[i], board[i][j]);
    v[j] = max(v[j], board[i][j]);
   }
  }
  std::string result = "YES";
  REP(i, N) {
   REP(j, M) {
    if(board[i][j] != min(h[i], v[j])) { result = "NO"; break; }
   }
   if(result == "NO") break;
  }
  cout << "Case #" << casenum+1 << ": " << result << endl;
 }

 return 0;
}

Problem C. Fair and Square

問題のアレです。繰り上がりが発生しないケースで限定しています。abcba の二乗だと、

876543210
a22ab2ca+b22ab+2bc2a2+2b2+c22ab+2bc2ca+b22aba2

abccba の二乗だと、

109876543210
a22ab2ca+b22bc+2ca2ab+2bc+c22a2+2b2+2c22ab+2bc+c22bc+2ca2ca+b22aba2

と、偶数桁と奇数桁で挙動が違いますが中央桁の 2a2+2b2+2c2 あるいは 2a2+2b2+c2 が一番効いてきます。結局ほとんど 0 と 1 場合によっては 2 もありうるという感じになります。これをパターン分けして数え上げて最初にリストを作ってしまう、というコードが↓です。

vector<string> fair = { "1", "4", "9", "121", "484" };

struct numeric_less
{
 bool operator()(const string &s1, const string &s2) const {
  return s1.size() < s2.size() ||
   (s1.size() == s2.size() && s1 < s2);
 }
};

void mygenerate(int len, const vector<pair<int,int>> &indices)
{
 bool valid = true;
 vector<int> v(len);
 for(auto &val : indices) {
  v[val.first * 2] += val.second * val.second;
  if(v[val.first * 2] > 9) { valid = false; break; }
 }
 REP(i, indices.size()) {
  for(auto j : IR<UI>(i+1, indices.size())) {
   v[indices[i].first+indices[j].first] += 2 * indices[i].second * indices[j].second;
   if(v[indices[i].first+indices[j].first] > 9) { valid = false; break; }
  }
  if(!valid) break;
 }
 if(valid) {
  string s;
  for(auto &val : v) {
   s.push_back(val+'0');
  }
  fair.push_back(s);
 }
}

void init_fair(size_t max_len)
{
 const UI N = (max_len + 1) / 2 + 1;
 REP(i_, N) {
  int i = i_ + 1; // 1 -> (max_len + 1) / 2
  if(i < 3) continue;
  if(i & 1) { // odd
   // 1xx0xx1 x can have 1 at most 6 positions
   //   1.0.1
   //   1.1.0.1.1
   //   1.1.1.0.1.1.1
   //   1.1.1.1.0.1.1.1.1
   // 1xx1xx1 x can have 1 at most 6 positions
   //   1.1.1
   //   1.1.1.1.1
   //   1.1.1.1.1.1.1
   //   1.1.1.1.1.1.1.1.1
   // 1xx2xx1 x can have 1 at most 2 positions
   //   1.2.1
   //   1.1.2.1.1
   // 2.2
   // 2.1.2

   const int X = (i - 1) / 2;
   // 2.2
   mygenerate(2*i-1, { {0,2}, {2*X,2} });
   // 1.0.1
   mygenerate(2*i-1, { {0,1}, {X,0}, {2*X,1} });
   // 1.1.1
   mygenerate(2*i-1, { {0,1}, {X,1}, {2*X,1} });
   // 1.2.1
   mygenerate(2*i-1, { {0,1}, {X,2}, {2*X,1} });
   // 2.1.2
   mygenerate(2*i-1, { {0,2}, {X,1}, {2*X,2} });

   for(auto ii : IR(1, X)) {
   // 1.1.0.1.1
    mygenerate(2*i-1, { {0,1}, {ii,1}, {X,0}, {2*X-ii,1}, {2*X,1} });
   // 1.1.1.1.1
    mygenerate(2*i-1, { {0,1}, {ii,1}, {X,1}, {2*X-ii,1}, {2*X,1} });
   // 1.1.2.1.1
    mygenerate(2*i-1, { {0,1}, {ii,1}, {X,2}, {2*X-ii,1}, {2*X,1} });
   }

   for(auto ii : IR(1, X)) {
    for(auto jj : IR(ii+1, X)) {
   // 1.1.1.0.1.1.1
    mygenerate(2*i-1, { {0,1}, {ii,1}, {jj,1}, {X,0}, {2*X-jj,1}, {2*X-ii,1}, {2*X,1} });
   // 1.1.1.1.1.1.1
    mygenerate(2*i-1, { {0,1}, {ii,1}, {jj,1}, {X,1}, {2*X-jj,1}, {2*X-ii,1}, {2*X,1} });
    }
   }

   for(auto ii : IR(1, X)) {
    for(auto jj : IR(ii+1, X)) {
     for(auto kk : IR(jj+1, X)) {
   // 1.1.1.1.0.1.1.1.1
    mygenerate(2*i-1, { {0,1}, {ii,1}, {jj,1}, {kk,1}, {X,0}, {2*X-kk,1}, {2*X-jj,1}, {2*X-ii,1}, {2*X,1} });
   // 1.1.1.1.1.1.1.1.1
    mygenerate(2*i-1, { {0,1}, {ii,1}, {jj,1}, {kk,1}, {X,1}, {2*X-kk,1}, {2*X-jj,1}, {2*X-ii,1}, {2*X,1} });
     }
    }
   }

  } else { // even
   const int X = i  / 2;

   // 1xxxx1 x can have 1 at most 6 positions
   //   1..1
   //   1.1..1.1
   //   1.1.1..1.1.1
   //   1.1.1.1..1.1.1.1
   // 2.2

   // 1..1
   mygenerate(2*i-1, { {0,1}, {2*X-1,1} });
   // 2.2
   mygenerate(2*i-1, { {0,2}, {2*X-1,2} });

   for(auto ii : IR(1, X)) {
   // 1.1..1.1
    mygenerate(2*i-1, { {0,1}, {ii,1}, {2*X-ii-1,1}, {2*X-1,1} });
   }

   for(auto ii : IR(1, X)) {
    for(auto jj : IR(ii+1, X)) {
   // 1.1.1..1.1.1
    mygenerate(2*i-1, { {0,1}, {ii,1}, {jj,1}, {2*X-jj-1,1}, {2*X-ii-1,1}, {2*X-1,1} });
    }
   }

   for(auto ii : IR(1, X)) {
    for(auto jj : IR(ii+1, X)) {
     for(auto kk : IR(jj+1, X)) {
   // 1.1.1.1..1.1.1.1
    mygenerate(2*i-1, { {0,1}, {ii,1}, {jj,1}, {kk,1}, {2*X-kk-1,1}, {2*X-jj-1,1}, {2*X-ii-1,1}, {2*X-1,1} });
     }
    }
   }

  }
 }
 sort(RNG(fair), numeric_less());
#if 0
 for(auto & val : fair) {
  cout << val << endl;
 }
#endif
}

UI solve(const pair<string, string> &iv)
{
 auto it1 = lower_bound(RNG(fair), iv.first, numeric_less());
 auto it2 = lower_bound(RNG(fair), iv.second, numeric_less());
 UI result = it2 - it1;
 if(it2 != fair.end() && *it2 == iv.second) ++result;
 return result;
}

int main(void)
{
 ios_base::sync_with_stdio(false);

 UI cases; cin >> cases;
 size_t max_len = 0;
 vector<pair<string,string>> iv(cases);
 for(auto & val : iv) {
  cin >> val.first >> val.second;
  max_len = max({max_len, val.first.size(), val.second.size()});
 }
 init_fair(max_len);
 for(auto & val : iv) {
  cout << "Case #" << &val - iv.data() + 1 << ": " << solve(val) << endl;
 }

 return 0;
}

で、どこでミスったかというと

 sort(RNG(fair), numeric_less());
#if 0
 for(auto & val : fair) {
  cout << val << endl;
 }
#endif

が、こうなってました。

#if 0
 sort(RNG(fair), numeric_less());
 for(auto & val : fair) {
  cout << val << endl;
 }
#endif

ああああああああ。まぁ Qualification Round なので通ったからいいんですが。不用意な修正には気をつけろってことですね。

Problem D. Treasure

lexicographically smallest とか言われているのでこりゃ探索するしかないなってんで、どの chest を開いたかによって鍵の数も決まるので探索済みだったら枝刈りする形のコードが↓。これでも small input は通ります。あ、#include <boost/dynamic_bitset.hpp> の追加が必要です。

deque<int> solve(const vector<int> &required, const vector<vector<int>> &got, set<dynamic_bitset<>> &visited, dynamic_bitset<>& open, vector<int> & keys)
{
 if(visited.count(open)) return {};
 if(open.count() == required.size()) return { -1 };
//cerr << "OPENED_COUNT: " << open.count() << endl;
 REP(i, required.size()) {
  if(!open[i] && keys[required[i]] > 0) {
//cerr << "OPEN: " << i << endl;
   open.set(i);
   --keys[required[i]];
   REP(j, got[i].size()) {
    ++keys[got[i][j]];
   }
   auto t = solve(required, got, visited, open, keys);
   if(t.size()) {
    if(t.back() == -1) {
     t.back() = i+1;
    } else t.push_front(i+1);
    return t;
   }
   REP(j, got[i].size()) {
    --keys[got[i][j]];
   }
   ++keys[required[i]];
   open.reset(i);
  }
 }
 visited.insert(open);
 return {};
}

int main(void)
{
 ios_base::sync_with_stdio(false);

 UI cases; cin >> cases;
 REP(casenum, cases) {
  UI K, N; cin >> K >> N;
  vector<int> keys(200);
  REP(i, K) { UI t; cin >> t; ++keys[t-1]; }
  vector<int> required(N);
  vector<vector<int>> got;
  REP(i, N) {
   cin >> required[i]; --required[i];
   UI t; cin >> t;
   vector<int> got_(t);
   for(auto &val : got_) {
    cin >> val; --val;
   }
   got.push_back(got_);
  }

  dynamic_bitset<> open(N);
  set<dynamic_bitset<>> visited;
  cout << "Case #" << casenum+1 << ":";
  auto result = solve(required, got, visited, open, keys);
  if(result.size()) {
   for(auto &val: result) {
    cout << ' ' << val;
   }
  } else {
   cout << " IMPOSSIBLE";
  }
  cout << endl;
 }

 return 0;
}

IMPOSSIBLE に対する時間がかかりすぎるので、鍵の数が足りない場合と相互(3つ以上含む)に持ち合っている場合を除外「しよう」としたのが↓コードです(※通りません)。@tanakh さん曰く

ということで、連結してないケース=相互に持ち合ってるケースのイメージなので方針としては間違って無かったようです。手書きメモ上ではグラフみたいなもの書いてたのに(時間もあるのに)適当実装してしまったのが駄目なところですね。

deque<int> solve(const vector<int> &required, const vector<vector<int>> &got, set<dynamic_bitset<>> &visited, dynamic_bitset<>& open, vector<int> & keys)
{
 if(visited.count(open)) return {};
 if(open.count() == required.size()) return { -1 };
//cerr << "OPENED_COUNT: " << open.count() << endl;
 REP(i, required.size()) {
  if(!open[i] && keys[required[i]] > 0) {
//cerr << "OPEN: " << i << endl;
   open.set(i);
   --keys[required[i]];
   REP(j, got[i].size()) {
    ++keys[got[i][j]];
   }
   auto t = solve(required, got, visited, open, keys);
   if(t.size()) {
    if(t.back() == -1) {
     t.back() = i+1;
    } else t.push_front(i+1);
    return t;
   }
   REP(j, got[i].size()) {
    --keys[got[i][j]];
   }
   ++keys[required[i]];
   open.reset(i);
  }
 }
 visited.insert(open);
 return {};
}

bool sanity_check(const vector<int> &required, const vector<vector<int>> &got, const vector<int> &keys_orig)
{
 vector<int> req(200);
 REP(i, required.size()) {
  ++req[required[i]];
 }
 bool flag = false;
 REP(i, required.size()) {
  vector<int> keys(keys_orig);
  REP(j, required.size()) {
   if(i != j) {
    REP(k, got[j].size()) {
     ++keys[got[j][k]];
    }
   }
  }
  bool flag_one = true;
  REP(j, keys.size()) {
   if(req[j] > keys[j]) {
    flag_one = false;
    break;
   }
  }
  if(flag_one) {
   flag = true;
   break;
  }
 }
 return flag;
}

bool sanity_check2(const vector<int> &required, const vector<vector<int>> &got, const vector<int> &keys)
{
 stack<int> q;
 dynamic_bitset<> pushed(200);
 dynamic_bitset<> open(required.size());
 REP(i, keys.size()) { if(keys[i] && !pushed[i]) { pushed.set(i); q.push(i); } }
 while(!q.empty()) {
  int n = q.top(); q.pop();
  REP(i, required.size()) {
   if(required[i] == n && !open[i]) {
    open.set(i);
    for(auto j : got[i]) {
     if(!pushed[j]) { pushed.set(j); q.push(j); }
    }
   }
  }
 }
 return open.count() == required.size();
}

int main(void)
{
 ios_base::sync_with_stdio(false);

 UI cases; cin >> cases;
 REP(casenum, cases) {
  UI K, N; cin >> K >> N;
  vector<int> keys(200);
  REP(i, K) { UI t; cin >> t; ++keys[t-1]; }
  vector<int> required(N);
  vector<vector<int>> got;
  REP(i, N) {
   cin >> required[i]; --required[i];
   UI t; cin >> t;
   vector<int> got_(t);
   for(auto &val : got_) {
    cin >> val; --val;
   }
   got.push_back(got_);
  }

  dynamic_bitset<> open(N);
  set<dynamic_bitset<>> visited;
  cout << "Case #" << casenum+1 << ":";
  if(sanity_check(required, got, keys) && sanity_check2(required, got, keys)) {
   auto result = solve(required, got, visited, open, keys);
   if(result.size()) {
    for(auto &val: result) {
     cout << ' ' << val;
    }
   } else {
    cout << " IMPOSSIBLE";
   }
  } else {
   cout << " IMPOSSIBLE";
  }
  cout << endl;
 }

 return 0;
}

総評

C-Large はせっかく合ってたのに感はありますが通過できればいいんです、うん。とにかく楽しかったです。Round 1A も頑張るぞ。

2012年12月15日土曜日

Boost.Context on Cygwin

1.51.0 において Boost.Context がリリース入りしました。Boost.Context はコンテキスト切り替えのためのライブラリです。コンテキストの最も一般的な訳語は「文脈」ですが、この場合は実行中のアドレス、CPU のレジスタなど、実行時の状態情報とでもいうべきものです。(プリエンプティブ)マルチタスクは OS がこのコンテキスト情報を切り替えることで成立していますが、これを自前で切り替えられるようにするのが Boost.Context です。あるいは、setjmp/longjmp だと setjmp した時点に戻ることしかできませんが、longjmp した時に同時に setjmp が実行されその場所にまた戻ることが出来ると言っても良いかもしれません(スタックの取り扱いが違いますが)。 これが出来て何が嬉しいかというと、C# の yield みたいなことが実現できるわけですが、それはともかく。CPU のレジスタとか書いていることでお分かりかもしれませんが、Boost.Context は C/C++ の範囲では実現できません。ということでアセンブリ言語で実装されています。Windows だと MASM が要求されるのが面倒だったので gas に移植(というほどのこともないですが)し、Cygwin でビルドできるところまで到達したのですが、example の jump.cpp をコンパイル、動作させてみると、コンテキストを切り替えた先の文字列出力で詰まってしまいました。

//          Copyright Oliver Kowalke 2009.
// Distributed under the Boost Software License, Version 1.0.
//    (See accompanying file LICENSE_1_0.txt or copy at
//          http://www.boost.org/LICENSE_1_0.txt)

#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>

#include <boost/assert.hpp>
#include <boost/context/all.hpp>

namespace ctx = boost::context;

ctx::fcontext_t fcm;
ctx::fcontext_t * fc1 = 0;
ctx::fcontext_t * fc2 = 0;

void f1( intptr_t)
{
        std::cout << "f1: entered" << std::endl;
        std::cout << "f1: call jump_fcontext( fc1, fc2, 0)" << std::endl;
        ctx::jump_fcontext( fc1, fc2, 0);
        std::cout << "f1: return" << std::endl;
        ctx::jump_fcontext( fc1, & fcm, 0);
}

void f2( intptr_t)
{
        std::cout << "f2: entered" << std::endl;
        std::cout << "f2: call jump_fcontext( fc2, fc1, 0)" << std::endl;
        ctx::jump_fcontext( fc2, fc1, 0);
        BOOST_ASSERT( false && ! "f2: never returns");
}

int main( int argc, char * argv[])
{
        ctx::guarded_stack_allocator alloc;

        void * base1 = alloc.allocate(ctx::guarded_stack_allocator::default_stacksize());
        BOOST_ASSERT( base1);
        fc1 = ctx::make_fcontext( base1, ctx::guarded_stack_allocator::default_stacksize(), f1);
        BOOST_ASSERT( fc1);
        BOOST_ASSERT( base1 == fc1->fc_stack.sp);
        BOOST_ASSERT( ctx::guarded_stack_allocator::default_stacksize() == fc1->fc_stack.size);

        void * base2 = alloc.allocate(ctx::guarded_stack_allocator::default_stacksize());
        BOOST_ASSERT( base2);
        fc2 = ctx::make_fcontext( base2, ctx::guarded_stack_allocator::default_stacksize(), f2);
        BOOST_ASSERT( fc2);
        BOOST_ASSERT( base2 == fc2->fc_stack.sp);
        BOOST_ASSERT( ctx::guarded_stack_allocator::default_stacksize() == fc2->fc_stack.size);

        std::cout << "main: call start_fcontext( & fcm, fc1, 0)" << std::endl;
        ctx::jump_fcontext( & fcm, fc1, 0);

        std::cout << "main: done" << std::endl;

        return EXIT_SUCCESS;
}

このコードは本来、

main: call start_fcontext( & fcm, fc1, 0)
f1: entered
f1: call jump_fcontext( fc1, fc2, 0)
f2: entered
f2: call jump_fcontext( fc2, fc1, 0)
f1: return
main: done

と出力されて終了するのですが、main: call start ... の行だけ出力されて詰まってしまう状態です。実際にハイライトされている 22 行目で詰まっていたわけですがこれは Cygwin 内部の仕組みのせいでした。

規格に定義されているわけではありませんが、一般的に、ローカル変数(自動変数)、関数の引数などはスタックと呼ばれる領域に格納されています。関数の呼び出しがネストするごとにスタックは伸びていきます。setjmp/longjmp ならば戻るだけ、なので伸びた先のことは忘れてしまえばいいわけですが、コンテキスト切り替えによってまた戻ってくるためにはスタックの状態が保存されていなければなりません。ということで Boost.Context ではスタック領域自体を別に用意した領域に切り替えます。この時、OS 側で管理している情報である NT TIB(Thread Information Block) のスタック情報(top と bottom)も切り替えています。一方、Cygwin ではスタックの底に cygtls というスレッド固有の情報を格納しており、NT TIB を経由して参照しています。このため、Boost.Context によるコンテキスト切り替えの結果、NT TIB が指すスタックの底に cygtls が存在しないことになり(恐らく排他制御に失敗して)詰まってしまっていたわけです。

実際、f1(), f2() 内の出力をコメントアウトすると、正しく切り替え出来 main: done も出力されます(NT TIB が元のスタックの底を指すため)。コンテキスト切り替え先で Cygwin のシステムコールがまともに使えないのはペナルティが大きすぎるので、NT TIB のスタック情報を切り替えをしない版を作ってみたところ正しく動作しているようです(Boost.Context gas on Windows パッチ)。

スタック領域のチェックをするだとかいったデバッグ系ツールと組み合わせられないでしょうが他は大丈夫だと思っているのですがどうでしょうか。

2012年12月8日土曜日

【C++ Advent Calendar 2012】 8日目 「C++ Compiler Farm の紹介」&「キャストの復習」

このエントリは C++ Advent Calendar 2012 8 日目の記事です。

C++ Advent Calendar 2012 というからには普通 C++ のネタを提供するものですが、一応、C++ 関連ではあるのですが言語仕様でもなければライブラリでもないネタです。それだけではなんなので一応小ネタとして「キャストの復習」という内容も書いてみます。

C++ Compiler Farm の紹介

既に一度 Twitter 上で流したネタではあるのですが、「C++ Compiler Farm」 というサイトを作ってみました。

C++ は言語仕様が複雑であり、かつ、標準実装や唯一の実装のようなものが存在しないこともあり処理系によって挙動がまちまちである、というのは C++er は身に染みて良く知っていることだと思います。それでは皆さんの周辺ではコンパイラは何種類くらい利用可能でしょうか?無償利用可能なコンパイラに限っても世の中に結構な数があるわけですがそんなにたくさん常用できる状態にはない、という人も結構いるのではないでしょうか? C++ Compiler Farm はオンラインで複数の C++ コンパイラによるコンパイル結果、実行ファイルの実行結果を確認できるサービスです。

以前 Twitter 上で流した時点ではコンパイル、実行結果の確認はできる、という状態でしたが、結果を後から参照することができませんでした。今回機能強化を果たし、http://ccf.myhome.cx:5000/result/1 のようなリンクで実行結果を後から参照することができるようになりました。これでコンパイラによって挙動が違う、などと言った時に他の人に結果を見せやすくなるんじゃないかな、と期待しています。

コンパイラ無選択状態でも実行可能だったり、全ての結果が保存されたり、編集できなかったりまだまだ低機能ですが利用者がちょっとでも居そうであればちょこちょこやっていこうかと思っています。なお関連ソースコードは https://github.com/yak1ex/ccf で公開しています。

C++ Advent Calendar で紹介しておきながらあるまじきことですが、ほとんど Perl で実装されています。なので概要だけ構成を説明しておくと以下の図のような構成になっています。

AWS EC2 上で Windows / Linux サーバ Micro instance 各1台。それぞれでコンパイルサーバが実行されており、Web サーバは Linux 側。ブラウザ上の Javascript と協調しながら処理する感じです。サーバ側では非同期処理フレームワーク AnyEvent を使っていますので、C++er としては Boost.Asio とかで書けると格好いいところなんですが。サンドボックス部分だけ、Google Chrome のオープンソース実装である Chromium の C/C++ コードを一部修正して使用しています。これで system("rm -rf /"); とか入力されても問題ないようになっています(http://ccf.myhome.cx:5000/result/7)。……そのはず、です。Windows 側はメッセージなしで黙って無視される形ですね。実行時間、使用メモリについても制限をかけています。相変わらず Amazon Web Services 無料範囲内での運用で、使用資源を絞った状態ですが以前よりはちょっと緩めました。この辺は調整だと思っていますので使用実績が増えれば考えるための材料も増えるかと思っています。

「C++ Compiler Farm」を紹介させて頂きました。皆様の C++ life に少しでも役立てば幸いです。

キャストの復習

ということで小ネタ「キャストの復習」です。最初に言っておきますが、規格上どうか、という話であって、実装上は概ね変わらないとかそういう割と役に立たない話になります。また、アラインメントについて省略したり正確な表現でなかったりします。

さて、C++ キャストは以下の 4 種類あります。

  • const_cast
  • dynamic_cast
  • reinterpret_cast
  • static_cast

このうち const_cast は const 外し、dynamic_cast は安全なキャストであるかどうかを判定できる、という点で位置づけは割と明快です(dynamic_cast の使いどころはどこか、というのはそれはそれで議論になりそうですが)。ということでたまに使い分けで議論になる reinterpret_cast と static_cast について、特にポインタの場合について掘り下げてみようと思います。

が、その前に C-style キャストについても確認しておきましょう。C++ においては C-style キャスト (type)value は以下の C++ キャストとして解釈可能なもののうち先にあるものと解釈されます(14882:2011 5.4p4)。

  • const_cast 1回
  • static_cast 1回
  • static_cast 1回 + const_cast 1回
  • reinterpret_cast 1回
  • reinterpret_cast 1回 + const_cast 1回

つまり文脈によってどのように解釈されるかが変わります。

C 言語において割と暗黙のうちに仮定されているんじゃないかという前提として、ポインタのキャストでは指す位置は変わらない(値は変わらず解釈が変わる)、というものがある気がします(注:C 言語においてもchar* への変換以外規格上その保証はありません(9899:1999, 9899:2011 6.3.2.3p7)。ついでに strict aliasing rule 的に char* 系以外の別の型へのポインタ経由でアクセスすると未定義動作です(9899:1999, 9899:2011 6.5p7)。一方で、C++ においてはポインタのキャストでその値(指している場所)が変わる場合が有り得ます。

以下のような継承がされているクラス群がある場合、

struct A { int n; };
struct B { int n; };
struct C : A, B { int n; };

C 型のオブジェクト内に A 型、B 型のオブジェクトが含まれる形となり、かつ先頭位置を C 型と共有可能なのは A 型か B 型かいずれか一つしかないことになります。規格上定義されていませんが典型的にはメモリレイアウトは以下の図のようになります。

図のようにA型、B型の順に並んでいるとして、C* を B* に static_cast すると C の中にある B の先頭を指すことになり、つまり、指す位置が変わります。逆に B* を C* に static_cast しても指す位置が変わります。この場合、もともと C 型のオブジェクト中の B 型オブジェクトを指していない場合などは不正な位置を指すことになります。つまりこのキャストはいつでも安全とは言えないのですが、static_cast はstandard conversion として規定されている型変換の逆方向のキャストもできると規定されている(14882:2003 5.2.9p6, 14882:2011 5.2.9p7)ため一律コンパイル可能です(もちろん不正な位置を指す場合には未定義動作ですが)。つまり「static_cast ならば安全なキャストだ」というわけではありません。

では static_cast の立ち位置とはなんでしょう?上の段落で単なる「キャストする」ではなく「static_cast する」と明示したことに気付かれたでしょうか?C++03 以前では、reinterpret_cast でのポインタのキャストについてはヌルポインタがヌルポインタのままであること(14882:2003 5.2.10p8)、T1* → T2* → T1* で元に戻ること(14882:2003 5.2.10p7)以外は未規定(unspecified)であり可搬性のあるコードを書くならば reinterpret_cast は使うな、が基本でした。つまり、上のような B* → C* あるいは C* → B* についても(続けてやれば元に戻ること以外) reinterpret_cast の結果について言えることはありませんでした。つまり(未定義動作の場合もあるけど)結果が規定されている static_cast と規定されていない reinterpret_cast という位置づけだった訳です。

C++03 以前において例えば char* を unsigned char* にキャストする(規格上)可搬性のある方法は、void* を経由して static_cast する方法でした。

char *pc;
// unsigned char* upc = static_cast<unsigned char*>(pc); // COMPILE ERROR
unsigned char* upc = static_cast<unsigned char*>(static_cast<void*>(pc));

void* への変換は指す位置が変わらないという規定があります(14882:2003 4.10p2)。一方、void* へ変換して元の型に戻すと同じ場所を指すという規定もあるため(14882:2003 5.2.9p10)、void* からのキャストも指す位置は変わらないことになります。一方、以下のコードもコンパイルは通りますが、

char *pc;
unsigned char* upc_bad1 = (unsigned char*)pc;
unsigned char* upc_bad2 = reinterpret_cast<unsigned char*>(pc);

この C-style キャストは(static_cast ではキャストできない変換なので)前述の通り reinterpret_cast として解釈されます。これも前述の通り reinterpret_cast では結果が保証されないためこのコードは可搬性がありません(pc と upc_bad* で同じ位置を指している保証がない)。

というのが、C++03 以前の話。C++11 では reinterpret_cast の規定が変わりました(14882:2011 5.2.10p7)。

An object pointer can be explicitly converted to an object pointer of a different type. When a prvalue v of type “pointer to T1” is converted to the type “pointer to cv T2”, the result is static_cast<cv T2*>(static_cast<cv void*>(v)) if both T1 and T2 are standard-layout types (3.9) and the alignment requirements of T2 are no stricter than those of T1, or if either type is void. Converting a prvalue of type “pointer to T1” to the type “pointer to T2” (where T1 and T2 are object types and where the alignment requirements of T2 are no stricter than those of T1) and back to its original type yields the original pointer value. The result of any other such pointer conversion is unspecified.

太字下線部分が大体追加された規定で、standard-layout type へのポインタ間の場合、void* を経由する static_cast 2回と等価、つまり↑で可搬性がある方法としていたものになります。standard layout type は規格の範囲で(パディング等はあるけど)メモリレイアウトが決まる型のことです。char, unsigned char については standard-layout type なので、C-style キャストの場合も含めて↑で可搬性がないとしていたコードが可搬性があることになりました。世の人々も指す位置が変わらないと思って reinterpret_cast を使ってるし実装も他に選択肢がなくまず間違いなくそうなってるし、という理由で規定が変更されています(DR658)。もともとコードの見た目でキャストの位置づけが分かるように、という意図で C++ キャストが分けられていたはずなのですが、まぁ現実には勝てない、というところなのでしょうか。

さて、これを B*, C* の例に適用すると、クラスについては、メンバ・基本クラスに非 standard-layout class がないこと、メンバに参照なし、仮想関数・仮想基本クラスなし、継承階層中非静的メンバをもつクラスは自分自身を含めて高々一つ、が standard layout class となるため、B は standard-layout type ですが、C は standard-layout type ではありません。結果、reinterpret_cast についてはやっぱり何も言えない、ということになります。実際には C++03, C++11 いずれの実装であっても指す位置が変わらない、というのが普通の実装でしょう(指す位置を変える積極的な理由がない)。ということを示そうとしたのが http://ccf.myhome.cx:5000/result/12 です。いずれの処理系においても static_cast では指す位置が変わる場合があり、reinterpret_cast では指す位置が変わっていません。なおこのコードでは reinterpret_cast によるポインタと整数との変換をしています。これ自体も(整数のサイズが十分であれば)一周回れば元に戻る、以外はどのような変換が実施されるかは処理系依存です(14882:2003, 14882:2011 5.2.10p5)(そしてアドレスをそのまま整数値とする実装が多いでしょうし、このコードは少なくとも変換が単射であることを期待したコードですので厳密には可搬性はありません)。個人的にはこのポインタと整数の相互変換が C++03 以前で reinterpret_cast を使うべき唯一のケースだと思っています。

さて、「キャストの復習」と題して、static_cast、reinterpret_cast によるポインタの変換について、C++11 での変更を含めてお送りしました。今後 C++ コミュニティにおいて reinterpret_cast の位置づけがどのようになるのか(一部の異なるポインタ型の変換について正当な手段とされるのか、あくまでも処理系依存や未規定なキャストについてのみ使うべきとされるのか)、興味深いところではあります。

まとめ

C++ Advent Calendar 2012 8日目として、「C++ Compiler Farm の紹介」と「キャストの復習」をお送りしました。C++ Advent Calendar 2012 明日の担当は @Flast_ROさんです。お楽しみに→【にゃははー】

2012年9月30日日曜日

YAPC::Asia Tokyo 2012 参加メモ

ブログ書くまでが YAPC::Asia らしいので。/.-j にしようか迷ったけど一応こちらで。

1日目、2日目で参加。1日目は新幹線が 80 分くらい遅れたため残念ながら Larry 氏のセッションは聞けず、その後から参加。YAPC 参加者は Web 界隈の人が多いんじゃないかと思っているが、そういう意味ではそもそも日常業務的に Perl どころがプログラミングも(基本)しないという点で特異な参加者な気もする(1日目は年休取得して参加)。ということで Web サービス寄りよりかは Perl 本体寄りのセッション選択、のはず。以降、各セッションの雑多なメモとか感想とか。

一日目

Acmeism, Pegex and CoffeeScript on CPAN

40 分の枠に収まらなかった感じ。もうちょいちゃんと聞きたかった。

リアルタイム通知システムの舞台裏

C++ Compiler Farm みたいなものを作っていて通知ではなくバックエンドのコンパイルサーバについてだがこの辺の管理(どのサーバに投げたか)とかどうしようというのと通ずるものがある気がした。メッセージキュー(RabitMQ)で解決したみたいだが、CCF では S3 がその位置に来そう。

Perl初心者が作ったサーバ運用ツール

英語での発表。運用ツールそのものについて自分に知見がないけど、テストがある、というのが重要なところか。ただこれ、テスト可能なように設定を綺麗に分離してやるというか、role と blueprint との関係が重要な気がする。

GitHubを使った開発とデプロイ

丁度 Perl モジュールの fork とかし始めたところだったのでそういう話なのかなと(勝手に)期待していたけどそうではなく普通にプロジェクトの中心リポジトリとして Github を使う感じだった。

Distributed Job System. Clutch

中央サーバを必要とせずクライアント側で振り分ける分散ジョブシステム、だろうか。多分構成次第? 多対多になるなら間に中央管理サーバを置いた方が楽になる気がするし、振り分け先が動的に変化するケースだと難しくなったりするんじゃないだろうか。

DBD::SQLite: Recipes, Issues, and Plans

正直一番実用的だったかもしれない。解析の仕方から考えなきゃならないデータに対してとりあえず DB に突っ込んで SQL で色々料理してみるというのはかなり強力なスキームだと思っているが、その上で SQLite は強い味方である。で、DB へのデータ投入とか SQL が苦手な処理をする時に一旦外でやろうとする場合などで DBD::SQLite はお世話になりまくりである。bulk insert は自分も prepare/分割commit/各種pragma on でやってたのでお墨付きをもらった感が。複数レコード insert は今度試してみたい。その他、DBD::SQLite ユーザーは一度軽く資料を眺めておくといいんじゃないかと思う。

Profiling memory usage of Perl applications

TreeMap によるメモリ使用量の視覚化デモが格好良かった。使われていたのは JavaScript InfoVis Toolkit だろうか。Interactive な視覚化をやるのに Javascript を使うというのは環境も(あまり)選ばないし便利な気がする。

平均レスポンスタイム50msをPerlで捌く中規模サービスの実装/運用

実際の内容についてはどうこう言えることはないのだが、前振りのアドテック業界について、が非常に分かりやすい導入だったと思う。

Perlアプリケーションのベンチマークとプロファイリング

↑のセッションでもそうだがとにかく計測すること、視覚化することがまず重要か。CCF なんかはとりあえず立ち上げてるだけなので試せるといいなぁ。

LT

相変わらずネタ満載。

二日目

「新しい」を生み出すためのWebアプリ開発とその周辺

今回のベストトーク賞受賞セッション。企画の部分は、Web アプリに限らず何か作ろうと思ってる万人に得るものがあるのではないかと。実装については枯れたものを使えば十分というあたり? 11月末~12月発売予定という「Webサービスのつくり方」は期待大。

Padre - The Perl IDE for Normal People

Emacs ユーザーと Vim ユーザーが聴取者の大半というのが印象的。自分も Emacs も Vim もプログラミングに使わないけど IDE も(ほとんど)使わないという点で Normal People からは外れてるわけだけど。Perl ユーザーが拡張とかしやすいのは Perl で書かれた IDE というのはそうかもしれないけど Perl のコアユーザー層が IDE 使ってないわけで。Eclipse なんかは企業からの後押しが大きかったわけだし、一般ユーザー層への拡大の需要というかそういうのがないと難しいかもしれない。

Perl 今昔物語

日記をさらってみたところ自分は 2008 が初参加の模様。この手のイベント自体が初参加だった。2010 は 2 日目だけ参加、2011 はキャンセル(海外出張)。参加してない部分のタイムテーブルを見ると聞いときゃ良かったというのが結構ある。今後はもう少し参加に貪欲になってもいいかもしれない。内容としてはPerl で書く必然性が薄れてきた、みたいなことを言われていたのが印象的だった。他のセッションの内容にもその辺が出てきているような気がする。

Perl と SQL のいろいろ

DBI/DBD についての分かりやすいチュートリアル。周辺モジュールの簡単な紹介もあって参考になった。質問タイムでプレースホルダの強制ができないんで O/R マッパー使ってるみたいなコメントがあって、プレースホルダの使用くらいプログラマとして最低限度の常識じゃないのかと思ったけど現実はそうもいかないんだろうなぁというのが悲しいところ。

シリコンバレーと世界のPerlエンジニア

川崎さんがアクティブ過ぎて眩しい。「10年後に食える仕事 食えない仕事」から引用されてた「グローカル」「ジャパンプレミアム」「無国籍ジャングル」「重力の世界」っていうのは興味深い。元々の本だとプログラマは「重力の世界」みたいだけど日本の適当な開発要求とのブリッジングという位置では立ち位置はありそう。それはそれでしんどそうだけど。

Perlで始める!初めての機械学習の学習

メモ見たら PRML 同人誌とだけ書いてあったw。とりあえず手に入れたいと思う。perl-kinect は面白そうなので公開されないかなぁ。

Perl入学式をやってみた!

期待していなかったのだが(ごめんなさい)、熱い思いを感じられるいいセッションだったと思う。「ハードルを下げることを重視する」「継続して参加できる環境を作る」というのは他のイベント主催の人も参考にできるかもしれないが、このレベルまで頑張るのは相当大変なことだと思う。あと、Perl 入学式参加者の Perl のわかりにくかったところとして、リファレンス(-> が省略できる場合がある)、コンテキストの概念、ファイルハンドルの , の省略、辺りが挙がってたのはそうだよなぁ、と思わざるを得ない。慣れてくると大丈夫だし逆に楽に書ける要因になったりもするんだけど、場合に応じてうまい具合に挙動が変わる、は、むしろ初めての人にはわかりにくい、と。

Performance Profiling with Devel::NYTProf

他のセッションでも言われていたけど、まず計測しろ、と。あと、目標達成したらそれ以上余計な事しない。局所的な変更から積み上げる。

LT

相変わらずネタ満載。

How Perl Changed My Life & Closing

総括感想も含めて。なんだろう、所詮 Perl はプログラミング言語の一つな訳だけど、TMTOWTDI に代表されるその精神というかそういうのがコミュニティにも有ってそれだけ懐が深いというか、Closing で牧さんが the most welcoming conference みたいなこと書いてたけどそういうのが有るんじゃないかな、と。忘れていたけど自分としては初参加のイベントが YAPC::Asia 2008 だった訳で、そこから色々参加(だけは)するようになったのを考えるとそれなりに大きな転機だったのかもしれないとも思う。そういう意味で Perl ってのは自分の中で結構大きい割合を占めているかもしれない。実際「Perl は生活、C++ は趣味」という感じで技術ネタは C++ が多いけど実際書いてたり日常使ってるものは Perl の方が多かったりするし。今現時点で Perl を選ぶ理由ってのはそんなにないのかもしれないけど、でも自分は Perl が好きなんだなぁ、と思う。で、スピーカーの人にはせっかくの YAPC なので別に Perl べったりの内容にする必要はないと思うんだけどもう少し Perl 寄りの発表をして欲しいなぁという気持ちもあったり。まぁとにかく来年も参加したいと思う。

2012年9月8日土曜日

小ネタ: Perl の警告: Deep recursion on subroutine ...

Perl では再帰呼び出しのネストが深いと警告してくれる機能がある。定石的に use strict; use warnings; していれば有効になっている。

use warnings;
sub dfs {
    if($_[0] < 101) { dfs($_[0]+1); }
}
dfs(0);
Deep recursion on subroutine "main::dfs" at ... line 3.

perldiag には "unless you're writing strange benchmark programs, in which case it indicates something else." などと書いてあったりするのだが、閾値が 100 であるためちょっと大きめのグラフに対して再帰で DFS かけたりしたら余裕で突破したりするのである。警告を抑制したい場合、Perl の警告はカテゴリ分けされているため再帰に関する警告だけオフにしてやればいい。

use warnings;
no warnings 'recursion';
sub dfs {
    if($_[0] < 101) { dfs($_[0]+1); }
}
dfs(0);

が、これだと全体で警告がオフになってしまう。Perl の警告制御は lexical scope である。これは限定した部分で警告をオン・オフできるということだが、

use warnings;
sub dfs {
    if($_[0] < 101) {
        no warnings 'recursion';
        dfs($_[0]+1);
    } # End of the effect of no warnings
}
dfs(0);

もう一つ、dynamic scope ではない、ということも意味している。つまり、以下では警告は抑制されない。

use warnings;
sub dfs {
    if($_[0] < 101) {
        dfs($_[0]+1);
    }
}
no warnings 'recursion';
dfs(0);

実際に deep recursion が発生するのは dfs(0); の実行中じゃないかと思ってしまうのだが、字面上(lexical)は 4 行目の dfs 呼び出しで発生するからだ。

2012年9月4日火曜日

Variadic Template にまつわる Workaround

C++11 は確かに便利、なのだが現状はまだ実装が十分と言えず回避手段(workaround)を取らざるを得ない場合がある。の割には余りそういう説明を見ない、ということでせっかく書いたのでメモってみる。まぁ Boost のソースコードの中にたくさん埋まっているのだろうし、ひょっとしたら C++er は息をするように workaround が書ける人種なのかもしれないが、そこはそれ。

sorry, unimplemented: use of ‘type_pack_expansion’ in template

#include <utility>
struct B { int operator()(int n) const { return n; } };

template<typename C>
struct A {

#if __GNUC__ == 4 && (__GNUC_MINOR__ <= 5 || __GNUC_MINOR__ == 6 && __GNUC_PATCHLEVEL__ == 0)

  template<typename ... Args>
  struct deduce {
      typedef decltype(C()(std::declval<Args&&>()...)) type;
  };
  template<typename ... Args>
  typename deduce<Args...>::type operator()(Args && ... args) const
  { return C()(std::forward<Args>(args)...); }

#else

  template<typename ... Args>
// sorry, unimplemented: use of ‘type_pack_expansion’ in template
// http://gcc.gnu.org/bugzilla/show_bug.cgi?id=48292
  auto operator()(Args && ... args) const -> decltype(C()(std::forward<Args>(args)...))
  { return C()(std::forward<Args>(args)...); }

#endif
};

このエラーメッセージが出るのは恐らくこの場合には限らないと思うのだが、type pack (Args ... みたいな展開)を tailing return type 中で使った場合。GCC bugzilla にも書いてある通り、別のクラスに切り出すことで回避できる。@iorate さんの記事([C++][Boost] C++ で一般化された on を書く)にも workaround の方法を含め書かれている。後は、GCC 4.6.1 から直っている、という情報くらいだろうか。

sorry, unimplemented: cannot expand ‘Args ...’ into a fixed-length argument list

#include <boost/fusion/include/vector.hpp>

#include <boost/fusion/adapted/mpl.hpp>
#include <boost/fusion/include/as_vector.hpp>
#include "variadic_bridge.hpp"

template<typename ... Args>
struct C {

#if __GNUC__ == 4 && __GNUC_MINOR__ <= 6

  typename boost::fusion::result_of::as_vector<
    typename yak::util::variadic_to_vector<Args...>::type
  >::type v;

#else

// sorry, unimplemented: cannot expand ‘Args ...’ into a fixed-length argument list
// http://gcc.gnu.org/bugzilla/show_bug.cgi?id=39653
  boost::fusion::vector<Args...> v;

#endif
};

こっちはエラーメッセージを日本語対象でググっても合っているものは tweet 1件しかヒットしない。……みんな困ってそうなものなのだが。variadic な template parameter を非 variadic な template に渡せない、というものだ。自作ヘルパ variadic_bridge.hpp を使って variadic な template parameter を MPL vector に変換して、さらに boost::fusion::vector に変換している。まぁ MPL vector まで持って来られれば後はどうとでもなるとは思うが、一応、固定長引数として Metafunction Class に渡す variadic_to_fixed も用意はしてある。こっちは GCC 4.7.0 から修正。

まとめ?

そもそも VC は 2012 でも variadic template 対応してねーよ、という時点で PP するしかないという話なのが悲しいところ。

しかし、CSS はブラウザバグとその対処、みたいなものが結構見つかるのだが C++ についてはそういうまとめみたいなものはないのだろうか。

2012年9月2日日曜日

(今更) C++ で拡張メソッド

Transactional Memory について予告をしておきながら別ネタ。何で実装しようと思ったのかそのきっかけを忘れてしまっているが、今更 C++ で拡張メソッドである。func(a, x) のようにメンバ関数外のものが a.func(x) で呼べるというやつである。まぁ、「今更」というくらいであって既にやってる方は色々いるわけだが。@cpp_akira (faith_and_brave) さん (2008年)じくよろさん(でいいのだろうか?)(2009年)@gim_kondo さん(2011年)などが作られているわけである。

もちろん C++ 自体には拡張メソッドは存在しないのでそれっぽいものを実現する仕組みを自前で作ることになるのだが、基本的に C++ で拡張メソッド風なものを実装しようとした場合、拡張するメソッド(関数)以外に大体 3 つの構成要素が必要だと思われる。

  1. 対象のオブジェクト、あるいは、引数を保持するオブジェクト(以下ラッパと表記)
  2. 上記ラッパを作成、値を紐付けする仕組み(以下ラッパ束縛と表記)
  3. 紐付けされなかった方と結びつけて実際の呼び出しを行う仕組み(以下ラッパ呼び出しと表記)

これで先の方の実装について整理するとこんな感じだろうか。じくよろさんの実装は最終的にフリー関数へ転送しているが「func(a, x) を」という形式に拘らなければ関数オブジェクト内でそのまま拡張メソッドの内容を実装してしまえばいいのでそのように実装した場合として記述している。

実装ラッパラッパ束縛ラッパ呼び出し
@cpp_akira さん拡張メソッドの内容を表す関数オブジェクト内に引数も保持コンストラクタoperator| のオーバーロード(任意の1引数関数オブジェクトを受ける)
じくよろさん拡張メソッドの内容を表す関数オブジェクト内に引数も保持コンストラクタoperator, のオーバーロード(関数オブジェクト限定)
@gim_kondo さん拡張メソッドの内容を表す関数オブジェクト内に対象オブジェクトへの this ポインタを保持対象オブジェクトへのメンバテンプレート埋め込みとマクロ置換によるコンストラクタ呼び出し関数オブジェクトの呼び出し

@gim_kondo さんの実装は . (ドット演算子)で呼び出せるようになっているが拡張メソッド呼び出し側でのマクロ置換発生は代償としてちょっと evil だと思われる。この判断をした時点で基本的に演算子オーバーロードで実装ということになるのだが、今回選んだ演算子は ->* である。ほとんどの C++er は使ったことがないだろうと思われる、というかひょっとしたら存在を知ってる人すら少ないかもしれない。通常の使い方は以下のような形である。

struct A { void func(void) {} };

int main(void)
{
 void (A::*mp)(void) = &A::func;
 A a, *pa = &a;
 (pa->*mp)();
 return 0;
}

つまりメンバないしメンバ関数へのポインタを参照するためのものである。とりあえず括弧の付け方に注意。知らないとまず間違えると思う。とりあえずこれがなくて困るとは普通ならない(そもそも普通の使い方ならポインタに対して使う)し、メンバアクセスのための演算子なので拡張メソッドとしては意味は近いはず、ということから選定。

で、拡張メソッド的なものが作れるというのは分かっている上でなぜまた(特に需要もないのに)別に作るのか。↑で書いたとおり、C++ で拡張メソッドを作ろうとすると本来の拡張メソッドだけでなく他の仕組みも必要になるわけでそれが面倒い。できるだけ拡張メソッド本体以外の余計な部分は勝手に作成させたい。で作ってみたのがこんな感じ。内部実装は https://github.com/yak1ex/cpp_stuff/blob/master/extender.hpp にある。

#include <iostream>
#include "extender.hpp"

namespace test { struct A { int n; }; }

namespace ext {

DEFINE_EXTENDER1(test::A, func1, {
 typedef test::A& result_type; // MUST follow result_of protocol

 result_type operator()(test::A& a, int &n) const {
  std::cout << "int&" << std::endl;
  return a;
 }
 result_type operator()(test::A& a, const int &n) const {
  std::cout << "const int&" << std::endl;
  return a;
 }
});

DEFINE_EXTENDER2(test::A, func2, {
 typedef test::A& result_type; // MUST follow result_of protocol

 result_type operator()(test::A& a, int &n) const {
  std::cout << "int&" << std::endl;
  return a;
 }
 result_type operator()(test::A& a, const int &n) const {
  std::cout << "const int&" << std::endl;
  return a;
 }
});

}

int main(void) {
 using ext::func1;
 using ext::func2;

 test::A a = { 0 }; int n = 0;

 ((a->*func1)(1)->*func1)(n); // Cascading but unintuitive

// NOTE: different from ordinary operator semantics/precedence
 a->*func2(1)->*func2(n);

 return 0;
}

実行結果

const int&
int&
const int&
int&

g++ 4.[5678] それぞれで -std=c++0x 有無両方で動作を確認している(いくつか workaround も入っている)。通常の演算子の優先順位の意味論に従ったのが EXTENDER1 の方、使いやすさを優先したのが EXTENDER2。まぁ普通は EXTENDER2 の方だと思う。使う側としてはほぼ書きたい拡張メソッドの内容部分のみだけで実現できている、と言っていいと思う。マクロにしてあるが↓なので直書きでもそんなに変わらない。なお、上記の例では 1 引数同士で const の違いだけでオーバーロードしているが、任意の型で引数の数が違っている場合でもそのまま operator() を書けばオーバーロード可能である。

#define DEFINE_EXTENDER1(target, name, ...) \
struct BOOST_PP_CAT(name, _functor) : public yak::util::extender1<BOOST_PP_CAT(name, _functor), target> \
__VA_ARGS__ name
#define DEFINE_EXTENDER2(target, name, ...) \
struct BOOST_PP_CAT(name, _functor) : public yak::util::extender2<BOOST_PP_CAT(name, _functor), target> \
{ \
 struct _ __VA_ARGS__; \
} name

内部実装について簡単に説明すると、CRTP + Barton Nackman trick を使ってラッパと演算子を定義している形。上表と同じ形で書くと次のようになる。

実装ラッパラッパ束縛ラッパ呼び出し
EXTENDER1拡張メソッドの内容を表す関数オブジェクト内に対象オブジェクトも保持operator->* のオーバーロードで関数オブジェクトを返す関数オブジェクトの呼び出し
EXTENDER2引数を保持するオブジェクトを関数オブジェクトと別に用意operator() のオーバーロードでラッパを返すoperator->* のオーバーロード(ラッパ限定)