hoge diary - カテゴリ - C++

[C++] VC2005 とテンプレートと friend

テンプレートクラスの中で関数に対する friend 宣言とその関数の定義を書くと... VC2006 のコンパイラはそれぞれを別物として扱い,結果,呼び出し時に「あいまいである」と C2593 エラーを出してくれます.

今回コンパイルしようとしたのは下のコード.テンプレートクラス Hoge の中で,テンプレート関数を friend 宣言して,クラスの外にその関数の定義を書いたものです.

#include <iostream>

namespace N {

template <size_t A, size_t B>
class Hoge
{
public:
  template <size_t A_, size_t B_>
  friend inline Hoge<A_,B_> operator * ( const Hoge<A_,B_>& a, const Hoge<A_,B_>& h );
};

template <size_t A, size_t B>
inline Hoge<A,B> operator * ( const Hoge<A,B>& a, const Hoge<A,B>& h )
{
  return Hoge<A,B>();
}

}

int main()
{
  N::Hoge<3,4> a;
  N::Hoge<3,4> b;
  N::Hoge<3,4> c = a*b;
  return 0;
}

これを VC2005 Express SP1 のコンパイラ (cl.exe 14.00.50727.762) でコンパイルすると... C2593 エラーが発生します.

$ cl /EHsc /Ox /MD /Oy /GF /Zc:wchar_t /Zc:forScope /nologo /c /Fo"test.obj" "test.cc"
test.cc
test.cc(44) : error C2593: 'operator *' があいまいです。
        test.cc(12): 'N::Hoge<A,B> N::operator *<3,4>(const N::Hoge<A,B> &,const N::Hoge<A,B> &)' [引数依存の照合を使用して検出しました] 、
        with
        [
            A=3,
            B=4
        ]
        test.cc(22): または 'N::Hoge<A,B> N::operator *<3,4>(const N::Hoge<A,B> &,const N::Hoge<A,B> &)' [引数依存の照合を使用して検出し ました] の可能性があります
        with
        [
            A=3,
            B=4
        ]
        引数リスト '(N::Hoge<A,B>, N::Hoge<A,B>)' を一致させようとしているとき
        with
        [
            A=3,
            B=4
        ]
$

ここでコンパイラが曖昧だと言っている 2 つの関数,よく見るとシグネチャが同じです.

  • test.cc(12): 'N::Hoge<3,4> N::operator *<3,4>(const N::Hoge<3,4> &,const N::Hoge<3,4> &)'
  • test.cc(22): 'N::Hoge<3,4> N::operator *<3,4>(const N::Hoge<3,4> &,const N::Hoge<3,4> &)'

VC2005 SP1 のコンパイラはこの 2 つを別物だと扱ってしまっているようです.g++ 3.4.4 では問題なくコンパイルできました.

調べてみると,同じ内容のフィードバックを発見.が,しかし,このフィードバックの状態と書かれた項目を見ると,終了 (修正しない)となっており,VC2005 (SP1 含む) では直さないようです.

これは困った.VC2008 では直ってるのかな.

[C++] メンバコンストラクタの例外

普段からこれっぽっちも疑問に思わなかったんですが,クラスメンバのコンストラクタの例外をキャッチする方法がある(cppll:3219)とのこと.

構文はこんな感じ.

class Foo
{
  Bar bar;  // Bar クラスは Foo のメンバ (定義は省略)
  Hoge() try : bar() {
    // メンバのコンストラクトが成功したときは,
    // ここに書いた Hoge のコンストラクト処理が行われる
    ...
  }
  catch (...) {
    // メンバのコンストラクタが例外を投げたときはここに来る
    ...
  }     // ←ここで自動的に catch した例外が再度投げられる (VC7 は投げてくれないらしい)
};

こんなところに try-catch が書けたんだ... 全然知らなかった...

メンバコンストラクタが投げた例外は catch 節を抜けた後で自動的に re-throw されるようになっているため,そのクラス自身のコンストラクトも失敗します (cppll:3221 によれば,VC7 では標準の動作をさせるために明示的に再スローしないとダメらしいです.手元に VC7 がないので確認できないのですが).

g++-4 や VC8 (Express Edition) ではどうなるかを試してみることに.

#include <iostream>
#include <stdexcept>

using namespace std;

class B
{
public:
  B() {
    throw runtime_error("hoge");
  }
  ~B() {
    cout << "~B()" << endl;
  }
};

class A
{
public:
  // constructor
  A() try : b() {
    cout << "A()" << endl;
  }
  catch (const exception& e) {
    cout << "foo" << endl;
  }

  ~A() {
    cout << "~A()" << endl;
  }

private:
  B b;
};

int main()
{
  try
  {
    A a;
  }
  catch (const exception& e)
  {
    cout << "bar" << endl;
  }
  return 0;
}

g++-4.1.2 (Gentoo 4.1.2) と,VC8 Express Edition (14.00.50727.42) で試したところ,どちらもいずれの実行結果が得られました.

(g++-4.1.2 での結果)
$ g++ test.cc
$ ./a.out
foo
bar
$
(VC8 Express での結果)
$ cl /nologo /EHsc test.cc
test.cc
$ ./test
foo
bar
$

ちゃんと A のコンストラクトは失敗しており,いずれも標準の動作になっています.

[C++] STL を未だに賢く使えていない自分

以下のようなファイルのデータを読み込んでコンテナに格納して,それを標準出力へ出力するプログラムを書くとします.

1 2 3
4 5 6

私がすっと思いつくのが以下のコード.

#include <iostream>
#include <fstream>
#include <list>
#include <functional>
#include <memory>

typedef int Value
typedef std::list<Value> DataContainer

//! ファイルからデータを読み出す
std::auto_ptr<DataContainer> Read(const std::string& filename)
{
    std::auto_ptr<DataContainer> data(new DataContainer);
    std::ifstream in(filename.c_str());

    for (;;)
    {
        int value;
        in >> value;
        if (!in) break;
        data->push_back(value);
    }

    return data;
}

//! データをストリームに書き出す
struct Show : public std::binary_function<int, std::ostream*, void>
{
    void operator () (int value, std::ostream* out) const
    { *out << value << "\n"; }
};

int main(int argc, char* argv[])
{
    std::auto_ptr<DataContainer> data = Read(argv[1]);
    std::for_each(data->begin(), data->end(), std::bind2nd(Show(), &std::cout));
    return 0;
}

とはいえ,Effective STL を読んでいると,std::list のコンストラクタで直接読み込んでいたりするので,上のコードではまだ車輪の再発明をしているわけです.

それを考慮して書き直したのが下のコード.

#include <iostream>
#include <fstream>
#include <iterator>
#include <list>
#include <functional>
#include <memory>

typedef int Value
typedef std::list<Value> DataContainer

//! ファイルからデータを読み出す (STL をより賢く使ったバージョン)
std::auto_ptr<DataContainer> Read(const std::string& filename)
{
    std::ifstream in(filename.c_str());
    std::istream_iterator<Value> in_begin(in);
    std::istream_iterator<Value> in_end;
    return std::auto_ptr<DataContainer>(new DataContainer(in_begin, in_end));
}

int main(int argc, char* argv[])
{
    std::auto_ptr<DataContainer> data = Read(argv[1]);
    std::copy(data->begin(), data->end(), std::ostream_iterator<Value>(std::cout, "\n"));
    return 0;
}

何と,Read() 関数はたった 4 行... 恐るべし STL.ちなみに実行結果はどちらのコードを用いても同じで,以下の通りの結果です.

1
2
3
4
5
6

[C++] 宣言ならば値で受け渡す引数に不完全型を使っても良い

最近 C++ で pimpl イディオム(コンパイラファイアウォール)を使ってヘッダファイル間の依存関係を減らすことを(くだらないクラス型に対しても)積極的に実践しています.

pimpl は不完全型(incomplete type; 定義がなく宣言のみの型)へのポインタもしくはリファレンスが使えることを前提に成り立っているわけですが,私は不完全型はポインタもしくはリファレンスだけにしか適用できないと思い込んでました.

Exceptional C++ の 4 章を読んでいると,値渡し,あるいは値返しをする関数の宣言に不完全型が使われていました.

「え!? こんなことできるの!?」と思ってしまいました.

というのも,ポインタやリファレンスについては,指し示す先の具体的な情報を知らなくていいというのが直感で分かりますのでいいとして,これが値で受け渡す場合にも通用するとは思っていなかったのです.

思い込みとは恐ろしいものです.試しにコードを書いてみると...

// test.cc
class A;
A func();
% g++ -c test.cc
%

通りました.宣言だけなら不完全型で大丈夫のようです.

もちろん使用するには完全型である必要がありますので,以下の 2 つのコードはコンパイルエラーです.

// test1.cc
class A;
A func();    // ただの宣言
int main()
{
  A a1;             // Error. 不完全型のインスタンスを定義することはできない
  A a2 = func();    // Error. 不完全型の初期化はできない
  return 0;
}
// test2.cc
class A;
A* func1();  // ただの宣言
A func2()
{
  return *func1();    // Error. 不完全型を値返しすることはできない
}

int main()
{
  return 0;
}
% g++ -c test1.cc
test1.cc: In function `int main()':
test1.cc:8: error: aggregate `A a1' has incomplete type and cannot be defined
test1.cc:9: error: variable `A a2' has initializer but incomplete type
test1.cc:9: error: invalid use of undefined type `struct A'
test1.cc:2: error: forward declaration of `struct A'
% g++ -c test2.cc
test2.cc: In function `A func2()':
test2.cc:6: error: return type `struct A' is incomplete
test2.cc:7: confused by earlier errors, bailing out
%

ということは,これでさらにヘッダ内の #include を減らすことができる,というわけです.

C++ は全くもって複雑で,調べれば調べるほどに奥深さを感じます.今回もまた一つ勉強になりました.

[C++] std::map::at() の代替

g++-4.1.1 付属の libstdc++ には,std::map::at() というメンバ関数がついています.これは,引数として与えたキーに対応する要素を参照するものですが,operator[] と違うのは,未登録のキーを参照しようとすると,std::out_of_range 例外が投げられるという点です.

g++-4.1.1 で開発しているときにこの関数を使用して何事もなくコンパイルできていましたが,g++-3.4.4 な環境でコンパイルしてエラーが出るので調べてみました.

その結果,g++-4.1.1 の stl_map.h:353 には次のように書いてあり,at() メンバ関数は非標準であることが判明しました.

// DR 464. Suggestion for new member functions in standard containers.

非標準のメンバ関数を使うのもいささか気がひけるので(と言いつつ,Boost は使いまくっていますが),代替の関数 Map_at() を書くことになりました.様々な型のペアに対応できるように,テンプレート関数で書くことにします.そして,最初に書いた定義は以下の通り.左端の数字は行番号です.

 5 template <typename map_type>
 6 map_type::mapped_type& Map_at(map_type& m, const map_type::key_type& key)
 7 {
 8   map_type::iterator it = m.lower_bound(key);
 9   if (it == m.end() || m.key_comp()(key, (*it).first))
10     throw std::out_of_range("Map_at");
11   return (*it).second;
12 }

g++-4.1.1 でコンパイルしてみると... 以下の通りのエラーメッセージが登場.

test.cc:6: error: expected constructor, destructor, or type conversion before '&' token

テンプレート内部において,型の周りで expected 〜 なコンパイルエラーが発生する場合,経験上 typename が抜けているので,勘と試行錯誤を組み合わせて typename を補います.そしてできたのが次のコード.

 5 template <typename map_type>
 6 typename map_type::mapped_type& Map_at(map_type& m, const typename map_type::key_type& key)
 7 {
 8   typename map_type::iterator it = m.lower_bound(key);
 9   if (it == m.end() || m.key_comp()(key, (*it).first))
10     throw std::out_of_range("Map_at");
11   return (*it).second;
12 }

これでめでたくコンパイルが通りました.しかし,まさか 3 箇所も typename を書き加えることになるとは.

今回の例では,C++ Labyrinth にあるような,ポインタ型のつもりで * 記号を書いているわけでもなければ,typedef しているわけでもなかったので,2 個目の typename や 8 行目の typename がなくとも,構文から map_type::key_type や map_type::iterator がそれぞれ引数もしくは変数の型だと判断してくれると期待していたのですが,どうやらこの場合も明示しないとダメみたいです.私にはまだまだ C++ の修行が足りないようです.

それはさておき,コンパイルが通ってしまえば,それを使うのは非常に楽です.以下のような感じで使用します.

std::map<int, int> map_a;
std::map<const char*, int> map_b;

map_a.insert(std::make_pair(10, 20));
map_b.insert(std::make_pair("hoge1", 10));

try
{
  std::cout << Map_at(map_a, 10) << std::endl;
  std::cout << Map_at(map_b, "hoge1") << std::endl;
  std::cout << Map_at(map_b, "hoge2") << std::endl;
}
catch (std::out_of_range& e)
{
  std::cerr << e.what() << std::endl;
}

上記コードの実行結果です.

20
10
Map_at

std::map::at() が標準になるまではこの関数を使うことにします.

[C++] 参照で例外を捕まえる

More Effective C++ 日本語版の 65 ページに参照で例外を捕まえるという項があります.初めてここを読んだときは驚きました.この本に書かれている通り,例外オブジェクトを値渡しすると,スライシングが発生する上に 2 度コピーコンストラクタが呼び出されるので,できれば参照で渡したいと思っていました.しかし,関数ローカルな例外オブジェクトを参照で渡してもいいのだろうか,という疑問も同時に浮上.

夜も眠れぬくらい非常に気になったので,その分の睡眠時間を削って,手近な環境(g++-4.1.1, i686-pc-linux-gnu)でテストしてみることに.

以下の簡単なコードでテストします.

class E {
public:
    int a;
    E() { a = 0x64; }
};

int func1() { throw E(); }

int main() {
    int a = 0;
    try { func1(); }
    catch (E& e) { a = e.a; }
    return 0;
}

これを g++ -O0 でコンパイルした後にリンクして,でき上がった ELF を逆アセンブルした結果が次の通りです.ただし,_fini などの余分な関数は除いています.

080485c0 <_Z5func1v>:
 80485c0:       55                      push   ebp
 80485c1:       89 e5                   mov    ebp,esp
 80485c3:       53                      push   ebx
 80485c4:       83 ec 14                sub    esp,0x14
 80485c7:       c7 04 24 04 00 00 00    mov    DWORD PTR [esp],0x4
 80485ce:       e8 dd fe ff ff          call   80484b0 <__cxa_allocate_exception@plt>
 80485d3:       89 c3                   mov    ebx,eax
 80485d5:       89 d8                   mov    eax,ebx
 80485d7:       89 04 24                mov    DWORD PTR [esp],eax
 80485da:       e8 75 00 00 00          call   8048654 <_ZN1EC1Ev>
 80485df:       c7 44 24 08 00 00 00    mov    DWORD PTR [esp+8],0x0
 80485e6:       00
 80485e7:       c7 44 24 04 1c 87 04    mov    DWORD PTR [esp+4],0x804871c
 80485ee:       08
 80485ef:       89 1c 24                mov    DWORD PTR [esp],ebx
 80485f2:       e8 f9 fe ff ff          call   80484f0 <__cxa_throw@plt>
 80485f7:       90                      nop

080485f8 <main>:
 80485f8:       8d 4c 24 04             lea    ecx,[esp+4]
 80485fc:       83 e4 f0                and    esp,0xfffffff0
 80485ff:       ff 71 fc                push   DWORD PTR [ecx-4]
 8048602:       55                      push   ebp
 8048603:       89 e5                   mov    ebp,esp
 8048605:       51                      push   ecx
 8048606:       83 ec 24                sub    esp,0x24
 8048609:       c7 45 f4 00 00 00 00    mov    DWORD PTR [ebp-12],0x0
 8048610:       e8 ab ff ff ff          call   80485c0 <_Z5func1v>
 8048615:       eb 2e                   jmp    8048645 <main+0x4d>
 8048617:       89 45 e8                mov    DWORD PTR [ebp-24],eax
 804861a:       83 fa 01                cmp    edx,0x1
 804861d:       74 0b                   je     804862a <main+0x32>
 804861f:       8b 45 e8                mov    eax,DWORD PTR [ebp-24]
 8048622:       89 04 24                mov    DWORD PTR [esp],eax
 8048625:       e8 b6 fe ff ff          call   80484e0 <_Unwind_Resume@plt>
 804862a:       8b 45 e8                mov    eax,DWORD PTR [ebp-24]
 804862d:       89 04 24                mov    DWORD PTR [esp],eax
 8048630:       e8 8b fe ff ff          call   80484c0 <__cxa_begin_catch@plt>
 8048635:       89 45 f8                mov    DWORD PTR [ebp-8],eax
 8048638:       8b 45 f8                mov    eax,DWORD PTR [ebp-8]
 804863b:       8b 00                   mov    eax,DWORD PTR [eax]
 804863d:       89 45 f4                mov    DWORD PTR [ebp-12],eax
 8048640:       e8 5b fe ff ff          call   80484a0 <__cxa_end_catch@plt>
 8048645:       b8 00 00 00 00          mov    eax,0x0
 804864a:       83 c4 24                add    esp,0x24
 804864d:       59                      pop    ecx
 804864e:       5d                      pop    ebp
 804864f:       8d 61 fc                lea    esp,[ecx-4]
 8048652:       c3                      ret
 8048653:       90                      nop

MASM ユーザな私には AT&T 記法は読みづらいので,ここでは逆アセンブル結果を得るために objdump -m i386:intel としています.

さて,main 関数から見てみます.まず最初にスタックフレームの作成とローカル変数の初期化をやってますね.その直後に _Z5func1v (func1 関数) をコールしてます.

_Z5func1v に移って,スタックフレームを作り... その直後に (アドレス 0x80485c7で) GOT 経由で __cxa_allocate_exception を呼び出してます.

この関数は何かというと... libstdc++ に含まれている,以下のような関数です.ここでは関数名を一部改変し,エラー処理を全て省略して示しています.

void* __cxa_allocate_exception(std::size_t thrown_size) throw()
{
  void *ret;

  thrown_size += sizeof (__cxa_exception);
  ret = malloc (thrown_size);

  __cxa_eh_globals *globals = __cxa_get_globals ();
  globals->uncaughtExceptions += 1;

  memset (ret, 0, sizeof (__cxa_exception));

  return (void *)((char *)ret + sizeof (__cxa_exception));
}

見ての通り,例外オブジェクト用のメモリ(投げるオブジェクトのサイズ + sizeof(__cxa_exception))をヒープ上に確保しています.

その後で __cxa_throw を call しています.この call 命令の後ろには func1 関数に対応する ret 命令がないことから,__cxa_throw を呼び出した時点で main 関数の catch 節に制御が移るということになります.

その main 関数では他にもいろいろと例外処理に関する関数が呼ばれていますが,今回の目的は例外処理の全貌を解析することではなく,例外オブジェクトを参照で受け取ってもよいかどうかの調査ですので,これ以上調べなくても大丈夫でしょう.念のため,class E のコンストラクタと main 関数の catch 節の両方でそれぞれ例外オブジェクトのポインタの値を出力し,両者が一致していることを確認しました.

結局,ソースコードの上では関数ローカルに見えるオブジェクトが,実はヒープ上に確保されている,ということです.謎は解けました.これで安心して眠り... もとい,参照で catch ができます.めでたしめでたし.

VC の場合はどうなってるのかも気になるところです.また機会があればそのうち...

[C++] delegate in C++

C# ではおなじみの delegate (デリゲート)ですが,C++/CLI でないただの C++ には当然そんなものはありません.今私が作っている C++ プログラムの機能を部分的にプラグイン化するにあたって,C# にあるあの便利な delegate の C++ 版がないかと思って探していると,いくつかの実装例を見つけました.

どちらの手法にも言えることは関数の引数(シグネチャ)をプログラム中にハードコーディングする必要がある,という点です.関数ポインタを使ってデリゲートを実装する以上,型をあらかじめ書く必要があるので仕方ないでしょう.

そんな中,Perl スクリプトを使用して,任意のシグネチャに対するデリゲートクラスを自動生成するアプローチがありました.

このスクリプトは,C# の delegate の定義によく似たファイル(以下)を入力として,

delegate void MyCallback( int a, int b, int c );

このファイルから,次のような C++ のヘッダファイルを生成する,というものです.

class MyCallback
{
public:
        virtual void Call( int a, int b, int c ) = 0;
};

template< class T >
class MyCallback_Delegate : public MyCallback
{
private:
        T* _that;
        void (T::*_func)( int a, int b, int c );

public:
        MyCallback_Delegate( T* that, void (T::*func)( int a, int b, int c ) )
        {
                _that = that;
                _func = func;
        }

        virtual void Call( int a, int b, int c ) {
                (_that->*_func)( a, b, c );
        }
};

template< class T >
inline MyCallback_Delegate<T>* new_MyCallback( T* that, void (T::*func)( int a, int b, int c ) )
{
        return new MyCallback_Delegate<T>( that, func );
}

呼び出す際には,次のようにします.

  1. まず最初に,デリゲート型のインスタンスを生成し,
    MyCallback* cb = new_MyCallback( this, &Receiver::DoStuff );
  2. 生成したインスタンスを使用して,呼び出しを行います.
    static void CallMeBack( MyCallback* cb )
    {
        cb->Call( 1, 2, 3 );
    }

関数ポインタの型が

void (T::*_func)( int a, int b, int c );

となっていますので,クラスの非 static なメンバ関数を呼び出せます.

この方法は,C++ における delegate の実装のうち,自動化できるところは徹底的に自動化したバージョンといえるでしょう.試しに使ってみようと思います.

なお,同じ文書中に,Perl スクリプトを用いない方法についても書いてありました.ただ,こちらの手法は渡す引数の数(型は問わない)に応じたテンプレートクラスを自分で書く必要があります.

[C++] 符号付き整数と符号なし整数の引数オーバーロード (2)

以前書いた符号付き整数と符号なし整数の引数オーバーロードの続編です.

あの時は autoconf を使って size_t がどの型なのかを判別すればいい,と書きましたが,後になって考えてみると,そんなことをする必要はないという考えに至りました.

int, unsigned int, long, unsigned long のそれぞれでオーバーロードしておけばいいだけの話でした.

#include <cstdio>

class A
{
private:
    int b;
public:
    A(int i) { printf("%s\n", __PRETTY_FUNCTION__); }
    A(unsigned int i) { printf("%s\n", __PRETTY_FUNCTION__); }
    A(long i) { printf("%s\n", __PRETTY_FUNCTION__); }
    A(unsigned long i) { printf("%s\n", __PRETTY_FUNCTION__); }
};

こうしておけば,size_t が unsigned int, unsigned long いずれの型であっても問題なくコンパイルできますね.

もちろん,実際に呼ばれる関数は環境によって unsigned int 版になったり unsigned long 版になったりと異なってくるわけですが,通常であればいずれの関数にも同じ処理を書くはずので,問題ないでしょう.

というわけで,autoconf を使わずともあっさり解決できました.

[C++] 符号付き整数と符号なし整数の引数オーバーロード

下のようなソースコード(ここではファイル名を test.cc とします)を書きました.

#include <cstdio>

class A
{
private:
    int b;
public:
    A(int i) { printf("%s\n", __PRETTY_FUNCTION__); }
    A(unsigned int i) { printf("%s\n", __PRETTY_FUNCTION__); }
};

int main()
{
    size_t i = 10;
    A a(i);
    return 0;
}

これを当方が使用している Gentoo Linux 上の gcc-4.1.1 上でコンパイルします.

% g++ --version
g++ (GCC) 4.1.1 (Gentoo 4.1.1)
Copyright (C) 2006 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
% g++ -Wextra test.cc
% ./a.out
A::A(unsigned int)
%

コンパイルが通り,動作します.いつもこの環境でプログラムを書いていたので,「size_t はどの環境でも unsigned int と同じ型」だと思いこんでいました.しかし,Darwin-8.8.1 (Core 2 Duo の iMac 上) 上の gcc-4.0.1 でコンパイルしたところ...

% uname -srvmpio
Darwin 8.8.1 Darwin Kernel Version 8.8.1: Mon Sep 25 19:42:00 PDT 2006; root:xnu-792.13.8.obj~1/RELEASE_I386 i386 i386 iMac5,1 Darwin
% g++ --version
i686-apple-darwin8-g++-4.0.1 (GCC) 4.0.1 (Apple Computer, Inc. build 5363)
Copyright (C) 2005 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
% g++ -Wextra test.cc
test.cc: In function 'int main()':
test.cc:15: error: call of overloaded 'A(size_t&)' is ambiguous
test.cc:9: note: candidates are: A::A(unsigned int)
test.cc:8: note:                 A::A(int)
test.cc:4: note:                 A::A(const A&)
%

と,怒られてしまいました.変数 i の型を size_t から unsigned int にすると,正しくコンパイルされ,最初に述べた Gentoo Linux での結果と一致します.

そういえば size_t って組み込み型じゃなかったなぁ,なんて思い出したので,プリプロセッサの出力を見てみることにします.

g++ -E test.cc | grep size_t
typedef long unsigned int size_t;
  using ::size_t;
typedef long unsigned int __darwin_size_t;
typedef long __darwin_ssize_t;
typedef __int32_t __darwin_blksize_t;
 __darwin_size_t ss_size;
 __darwin_size_t uc_mcsize;
 __darwin_size_t uc_mcsize;
(中略)
 size_t i = 10;
%

Darwin 側には long が付いてます.ちなみに Gentoo 側では以下の通り unsigned int でした.

% g++ -E test.cc | grep size_t | head -n1
typedef unsigned int size_t;
%

念のため確認しておきますと,sizeof(size_t) はどちらの環境でも 4 でした.ただ,お恥ずかしながら,size_t が unsigned int ではない 32bit 処理系も存在する,ということを今知りました.

64bit 処理系では sizeof(size_t) が sizeof(unsigned int) と等しくないので,それを利用してオーバーロードの有無を切り替えていたのですが... 今回の場合は変数のサイズでは比べられないので,何か別な基準が必要になりそうです.例えば typeid でしょうか.

% cat > test2.cc
#include <cstdio>
#include <typeinfo>
int main()
{
    if (typeid(size_t) != typeid(unsigned int))
        puts("NOT SAME");
    else
        puts("SAME");

    if (typeid(size_t) != typeid(long unsigned int))
        puts("NOT SAME");
    else
        puts("SAME");

    return 0;
}
% g++ test2.cc
% ./a.out
NOT SAME
SAME
%

うまくいきました.この判定プログラムの結果を使ってプリプロセッサを制御すれば,size_t が long unsigned int な環境でもコンパイルが通るようにできそうです(この辺の処理には GNU 定番の autoconf を使います).

[C++] std::vector の実装 (2)

hoge diary - February 6, 2006 - std::vector の実装 の続きです.

さて,私は前回memcpy() 等を使ってメモリイメージを丸ごとコピーしているわけではないことがわかりましたが... コピーコンストラクタが 3 回ではなく 4 回呼ばれています.vector の最初の要素は 2 回のコンストラクタ呼び出しによって初期化されているのでしょうか.と書いていましたが,この謎は std::vector の定義を見れば済むことでした(某さんの的確なツッコミに感謝します).

というわけで,定義を見てみましょう.今回使用するのは,GNU g++-3.4.5 に含まれている STL のヘッダです.

ヘッダファイル vector を見ると... bits/stl_vector.h をインクルードしています.stl_vector.h を見ると,std::vector の定義が見つかりました.

template<typename _Tp, typename _Alloc = allocator<_Tp> >
  class vector : protected _Vector_base<_Tp, _Alloc>
  {
    ...(途中省略)...

    void
    resize(size_type __new_size, const value_type& __x)
    {
      if (__new_size < size())
        erase(begin() + __new_size, end());
      else
        insert(end(), __new_size - size(), __x);
    }

    void
    insert(iterator __position, size_type __n, const value_type& __x)
    { _M_fill_insert(__position, __n, __x); }

    ...(途中省略)...

    void
    _M_fill_insert(iterator __pos, size_type __n, const value_type& __x);

    ...(途中省略)...
 };

resize() が insert() を呼び,insert() が _M_fill_insert() なるものを呼んでおりますが,その定義は,bits/vector.tcc に以下の通り書かれています.強調箇所が今回の要点(コンストラクタの呼び出しにかかわる箇所)です.なお,左端に行番号を付けて示しています.

 1  template<typename _Tp, typename _Alloc>
 2    void
 3    vector<_Tp,_Alloc>::
 4    _M_fill_insert(iterator __position, size_type __n, const value_type& __x)
 5    {
 6      if (__n != 0)      // 挿入する要素が無ければ何もする必要はない
 7      {
 8        // 既に n 個の領域を追加するのに十分なメモリを確保してあるかを調べる
 9        if (size_type(this->_M_impl._M_end_of_storage - this->_M_impl._M_finish) >= __n)
10        {
11          // 十分なメモリが確保されているので,要素を追加するだけ.
12  
13          value_type __x_copy = __x;      // ここでコピーコンストラクタがまず最初に 1 回呼ばれる.
14          const size_type __elems_after = end() - __position;
15          iterator __old_finish(this->_M_impl._M_finish);
16          // 挿入地点以後の要素を後方にコピーするが,その前にコピー元とコピー先のメモリ領域が
17          // 重なるかどうかをチェックする.
18          if (__elems_after > __n)
19          {
20            // 重なっているので,末尾の要素から順番にコピーする.
21  
22            std::uninitialized_copy(this->_M_impl._M_finish - __n,
23                                    this->_M_impl._M_finish,
24                                    this->_M_impl._M_finish);
25            this->_M_impl._M_finish += __n;
26            std::copy_backward(__position, __old_finish - __n, __old_finish);
27            std::fill(__position, __position + __n, __x_copy);
28          }
29          else
30          {
31            // 要素は重ならないので,前から順にコピーしてよい.
32  
33            std::uninitialized_fill_n(this->_M_impl._M_finish,
34                                      __n - __elems_after,
35                                      __x_copy);
36            this->_M_impl._M_finish += __n - __elems_after;
37            std::uninitialized_copy(__position, __old_finish, this->_M_impl._M_finish);
38            this->_M_impl._M_finish += __elems_after;
39            std::fill(__position, __old_finish, __x_copy);
40          }
41        }
42        else
43        {
44          // 十分なメモリが確保されていないので,確保する
45  
46          const size_type __old_size = size();
47          const size_type __len = __old_size + std::max(__old_size, __n);
48          iterator __new_start(this->_M_allocate(__len));
49          iterator __new_finish(__new_start);
50          try
51          {
52            // 言語仕様より,std::vector はメモリの連続性を保証する必要がある.
53            // したがって,
54            //   1. 挿入位置より前の要素をコピー
55            //   2. 今回追加する n 個の領域を初期化
56            //   3. 挿入位置より後ろの要素をコピー
57            // の流れをたどる.
58  
59            __new_finish = std::uninitialized_copy(begin(), __position,
60                                                   __new_start);
61            __new_finish = std::uninitialized_fill_n(__new_finish, __n, __x);
62            __new_finish = std::uninitialized_copy(__position, end(),
63                                                   __new_finish);
64          }
65          catch(...)
66          {
67            std::_Destroy(__new_start,__new_finish);
68            _M_deallocate(__new_start.base(),__len);
69            __throw_exception_again;
70          }
71          std::_Destroy(this->_M_impl._M_start, this->_M_impl._M_finish);
72          _M_deallocate(this->_M_impl._M_start,
73                        this->_M_impl._M_end_of_storage - this->_M_impl._M_start);
74          this->_M_impl._M_start = __new_start.base();
75          this->_M_impl._M_finish = __new_finish.base();
76          this->_M_impl._M_end_of_storage = __new_start.base() + __len;
77        }
78      }
79    }

関数 std::uninitialized_copy(),std::uninitialized_fill_n() は,placement new を用いてコンストラクタを呼び出し,確保済みのメモリ領域を初期化します.詳しくはロベールのC++教室 - 第49章 破壊と創造や,s34 - 初期化されていないメモリへの記憶を参照してください.

ここまで来れば,前回の疑問である,コンストラクタが最初に 1 回余分に呼ばれている謎はもう解けました._M_fill_insert() の 13 行目です.ここでコピーが 1 回発生しています.

insert() メンバ関数による要素の挿入時に,挿入位置より後の要素を後方にずらす処理を行っていますが,コピー元とコピー先の範囲が重なるかどうかで処理を分けてるところがあります(18 行目の if 文).どんな場合でも後ろからコピーするわけではなく,可能ならば前から順にコピーするようになっていたことを,今回ソースを眺めることで初めて知ることができました.これを実装した GNU の人はこのようなところにも気を配っているわけです.

[C++] std::vector の実装

以下のソースをコンパイル,実行してみると,コピーコンストラクタが 4 回呼び出されているのを確認できました.

#include <iostream>
#include <vector>

class A
{
public:
    A()                    { std::cerr << "A default ctor" << std::endl; }
    A(const A& s)          { std::cerr << "A copy ctor" << std::endl; value_ = s.value_; }
    int value_;
};

int main()
{
    std::vector<A> a1;
    std::vector<A> a2;

    std::cerr << "a1.resize(4)..." << std::endl;
    a1.resize(4);

    std::cerr << "a2.reserve(4)..." << std::endl;
    a2.reserve(4);

    return 0;
}
% g++ -O0 test.cc
% ./a.out
a1.resize(4)...
A default ctor
A copy ctor
A copy ctor
A copy ctor
A copy ctor
a2.reserve(4)...
%

memcpy() 等を使ってメモリイメージを丸ごとコピーしているわけではないことがわかりましたが... コピーコンストラクタが 3 回ではなく 4 回呼ばれています.vector の最初の要素は 2 回のコンストラクタ呼び出しによって初期化されているのでしょうか.

ちなみにこの挙動は,無償版の icc 6.0.031 を用いても確認することができました.コンストラクタでは初期化以外の処理をしないほうが良さそうです.

[C++] Programming languages -- C++

入手には金が要るはずの ISO/IEC 14882:1998 が転がっています.なお最新は ISO/IEC 14882:2003 です.

More Effective C++ も転がっていました.何なんでしょう.

[C++] 例外

今私が C++ でプログラムを書く際は,使える状況なら間違いなく例外を使います.やっぱり例外はいいもんです.

戻り値が存在しないコンストラクタとデストラクタのエラーもチェックできるので,使い出したらやめられまへん.

ただ,std::exception は,派生クラスの種類が少ない上,コンストラクタに const char * しか渡せません.もうちょっと増やしてほしいですね.const wchar_t * 版もありませんね.

そして STL を使うと,実行ファイルのサイズがでかくなってしまいますね.

STL 無しで 40KB くらいの実行ファイルが,STL を入れると 100KB を超えてしまいます.インライン展開の宿命です.避けられません.

最近のストレージの容量が大きくなってるのでよしとしましょう.

[C++] boost::tokenizer + boost::escaped_list_separator

std::string をトークン列に分解して色々作業する場合,std::istrstream を使うのも手ですが,これでは引用符付きの文字列を分解することができません.

そんなときのお助けライブラリがありました.boost::tokenizer + boost::escaped_list_separator です.

#include <iostream>
#include <boost/tokenizer.hpp>
#include <string>
using namespace std;
using namespace boost;

int main() {
  string s = "Field 1 \'putting quotes around fields, allows commas\' \"Field 3\"";
  tokenizer<escaped_list_separator<char> > tok(s, escaped_list_separator<char>("\\", " ", "\"\'"));
  for(tokenizer<escaped_list_separator<char> >::iterator beg=tok.begin(); beg!=tok.end();++beg){
    cout << *beg << "\n";
  }
}

boost::escaped_list_separator 自体が複数の種類の引用符や区切り文字に対応しています.これでトークン分解が簡単にできます.

[C++] C++ 例外の怪(おまけ)

2004 年 9 月 27 日の laconicDiary で,(re-throwするオブジェクトがないけど) とありますが,まさにそこが「怪」なのです.

引数なしの throw; は例外の再スローを行うことは仕様のようですが,例外ハンドラ以外で再スローすると,なぜ C++ のランタイムライブラリが「ありもしない」例外をハンドルするのか,なぜコンパイルエラーにならないところが不思議です.

ちょっと下の方に書いてあるそれ以前に、10 / 0 (int / int)でどうして「浮動小数点」例外なのかが気になる。という点は私も気になりました.しかしよく考えてみると,浮動小数点で 10.0 / 0.0 を実行すると,値は無限大になるだけで,別に浮動小数点例外が発生するようなことはなかったように記憶しています.試しにやってみることにしました.

#include <iostream>
int main()
{
  std::cout << 10.0 / 0.0 << std::endl;
  return 0;
}

上記のソースコードをコンパイルして実行すると以下の結果が得られます.

% g++ test.cc
test.cc: In function `int main()':
test.cc:4: 警告: division by zero in `1.0e+1 / 0.'
% ./a.out
inf
%

例外は発生していません.一方,整数で 10 / 0 を実行すると... 次の例のように floating point exception が発生します.

#include <iostream>
int main()
{
  std::cout << 10 / 0 << std::endl;
  return 0;
}
% g++ test.cc
test.cc: In function `int main()':
test.cc:4: 警告: division by zero in `10 / 0'
% ./a.out
zsh: 28997 floating point exception  ./a.out
%

当然これは signal(7) の SIGFPE のことです.前者の例から考えると,浮動小数点の 0 除算では「本当の」SIGFPE は発生しないでしょうから,私が推測する理由は次のようなものです.

「UNIX がよく稼働する Sparc では,整数の 0 除算も浮動小数点例外として処理していて,Linux は i386 への実装の際,できるだけ近い実装となるようにした」

(以下 2007 年 2 月 3 日に加筆)

上記の推測が正しいのかどうかは未だに謎なのですが,たまたま見つけた Wikipedia の SIGFPE という項目には以下の記述があります.

SIGFPE is the signal sent to computer programs that perform erroneous arithmetic operations on POSIX compliant platforms.

これを読む限りでは,別に浮動小数点演算に限らず,一般の算術演算に対して SIGFPE が発生するようです.また,SIGFPE の語源についても以下の通りに書いてあります.

Although SIGFPE does not necessarily involve floating-point arithmetic, there is no way to change its name without breaking backward compatibility.

昔に SIGFPE と名付け,今はその名前を変更できないというのは分かりましたが,昔のコンピュータでは SIGFPE が浮動小数点専用だったのかどうたまでは謎のままです.

当時,私がこの文書(加筆前のものです)を書いているときは,不正な浮動小数点演算で SIGFPE を発生させる方法を知りませんでした.が,ちゃんと方法があります.

Tips for the Numerical Computation using GNU/Linux にあるように,feenableexcept() を使用することで,不正な浮動小数点演算時に SIGFPE が発生するようになります.これを踏まえてこの文書の先頭にあるコードを書き直すと次の通りになります.

#include <iostream>
#include <fenv.h>
int main()
{
  feenableexcept(FE_DIVBYZERO);
  std::cout << 10.0 / 0.0 << std::endl;
  return 0;
}

上記のソースコードをコンパイルして実行すると,きちんと浮動小数点演算の 0 除算に対して SIGFPE が発生します.

% g++ test.cc
test.cc: In function `int main()':
test.cc:6: 警告: division by zero in `1.0e+1 / 0.'
% ./a.out
zsh: 7083 floating point exception  ./a.out
%

[C++] C++ 例外の怪

エラー処理を C++ の例外を用いて次のような感じで記述したところ... catch されずにそのまま abort しました.

使用したコンパイラは gcc 3.4.1 です.

#include <iostream>
using std::cout;
using std::endl;
int main(void)
{
  try
  {
    cout << "throw" << endl;
    throw;
  }
  catch (...)
  {
    cout << "caught" << endl;
  }
  return 0;
}

catch (...) とすることで,あらゆる種類の例外を捕捉できると記憶していたのですが...

次のように記述することで,正しく捕捉されました.

#include <iostream>
using std::cout;
using std::endl;
int main(void)
{
  try
  {
    cout << "throw" << endl;
    throw 1;
  }
  catch (int)
  {
    cout << "caught" << endl;
  }
  return 0;
}

throw には式を与えるようにします.

なお,次のコードで発生させた floating point 例外は捕捉されませんでした.

#include <iostream>
using std::cout;
using std::endl;
int main(void)
{
  try
  {
    cout << "throw" << endl;
    cout << 10 / 0 << endl;
  }
  catch (...)
  {
    cout << "caught" << endl;
  }
  return 0;
}
% ./a.out
throw
zsh: 20268 floating point exception  ./a.out
%

何なんでしょうね.


Valid XHTML 1.1! Valid CSS!
© 2004-2009 ぱくちゃん.
Last modified: Sun Jan 18 14:09:47 JST 2009