melpon日記 - HaskellもC++もまともに扱えないへたれのページ

2011-08-26

[]C++03 と C++11 の互換性 16:25 C++03 と C++11 の互換性を含むブックマーク

C++03 と C++11 ってどれぐらい互換性があるのかなーと気になっていたんだけど、仕様書の §C.2 を見てみたらずばりなものが載っていたので、一通り読んでみた。


C++03 のコードを C++11 として動かそうとしたときにコンパイルエラーやランタイムエラーが発生したら、これを確認してみるといいかも。


新しい文字列リテラル

R, u8, u8R, u, uR, U, UR, LR という新しい文字列リテラルが追加されたため、文字列と一緒にこれらのマクロを使った場合、互換性の無いコードになる可能性がある。

例えば以下のコードは互換性の無いコードである。

#define u8 "abc"
const char* s = u8"def"; // C++03 なら "abcdef"、C++11 なら "def" になる

ユーザはこの手の短いマクロをよく使うため、この問題はよく発生しそうに見えるが、恐らくほとんど遭遇しないだろう。

そもそもなぜ C++11 で u8 が展開されないかというと、u8"..." という形式をプリプロセッサが一つのトークンとして処理するためである。

なので、u8 というマクロ自体はちゃんと展開される。

#define u8 "abc"
const char* s = u8; // C++03 でも C++11 でも "abc" になる

R, u8, u8R, u, uR, U, UR, LR といったマクロを定義した上、それを文字列の手前に引っ付けるなんてことはまずやらないだろう。


ユーザ定義リテラル

上記の文字列リテラルと同様に、以下のコードは互換性の無いコードである。

#define _x "there"
"hello"_x; // C++03 なら "hellothere"、C++11 ならユーザ定義リテラル関数の呼び出しになる

これも上記と同様 "..."xxx をプリプロセッサが一つのトークンとして処理するため起こる現象である。

ただし、上記と違ってこれは任意の名前にできるので、発生する確率は少し高くなるだろう。


キーワードの追加

以下のキーワードが追加されたため、これらを使っていたコードは互換性が無くなる。

alignas
alignof
char16_t
char32_t
constexpr
decltype
noexcept
nullptr
static_assert
thread_local

C++ 標準化委員会は、これらの単語が十分に使われていないことを確認した上で追加している。


整数リテラル

long より大きい数の整数リテラルは、符号なし整数型から signed long long になるように変わった。

この動作は C99 と互換性がある。


整数の除算・剰余の丸め方

C++03 では整数の除算は 0 方向に丸めるか、マイナス方向に丸めるかどちらでも構わなかったのだけど、C++11 では 0 方向に丸めなければならなくなった。また、これに伴って剰余の結果も実装依存では無くなる。

この動作は C99 と互換性がある。


そもそも 0 方向に丸めないコンパイラなんて見たことがないし、あったとしてもごく少数だろうから、これが問題になることはほぼ無いだろう。


auto キーワードの意味の変更

C++03 では auto キーワードストレージクラス定子としての役割を持っていたけれども、これはあまり使われないので、初期化式を推論するためのキーワードとして再利用することとなった。

なので auto キーワードを使っている C++03 のコードは互換性が無くなる。


C++ 標準化委員会は、auto キーワードが十分に使われていないことを確認した上でこのキーワードを再利用している。(どんな方法で調べたのか、どれぐらい使われていなかったのかについて後で追記するかも。というか誰か教えてください)


ナロー変換を伴う aggregate の初期化

aggregate 内での初期化については、ナロー変換の機能を限定するようになったため、互換性の無いコードになる可能性がある。

以下のコードは、C++03 では有効だったが、C++11 ではコンパイルエラーである。

int x[] = { 2.0 }; // aggregate 内で double から int へのナロー変換はできなくなった

この変更は結構大きい。意図している/いないに関わらず、ナロー変換に依存した処理は多い。どのナロー変換が禁止されているのか調べていないけれども、この例の double から int へのナロー変換だけで大分多くのコードがエラーになるだろう。


オブジェクトの暗黙定義

テンプレートの推論を正しく失敗させるために、C++11 の暗黙の特殊関数は、その定義が ill-formed になってしまう場合に deleted 扱いになる。

C++03 では未評価式にのみ出現する場合でも、これらの呼び出し時点で ill-formed になる。

ということらしいです。


デストラクタの例外指定

C++11 では、noexcept で例外指定されたデストラクタで例外が投げられると、(unexpected() ではなく)terminate() を呼び出す。

ただし、継承したクラスの仮想デストラクタで例外を投げるときに、ベースクラスの仮想デストラクタに noexcept が指定されていない場合に限り、terminate() は呼ばれない。*1


export の削除

C++11 では export の機能は削除された。C++03 で export を使っている場合はコンパイルエラーになる。

(export というキーワード自体は将来のために予約してある)


そもそも export なんてほぼ全てのコンパイラが実装していないので、何の問題もない。


テンプレートの右シフト問題

C++03 では >> が右シフトに解釈されてしまうため以下のコードが通らなかったのだが、C++11 では >> の解釈を変更したので、通るようになっている。

vector<vector<int>> x;

しかし、この変更により、一部のマイナーなコードが通らなくなる可能性がある。例えば以下のようなコードである。

template<class T> struct X { };
template<int N> struct Y { };
X< Y< 1 >> 2 > > x;

これは C++03 では有効なコードだが、C++11 ではコンパイルエラーとなる。


新しい識別子の予約

C++11 で多くのライブラリが追加された。

そのため C++03 でその新しい識別子(関数名とか変数名とか)を使用している場合、コンパイルエラーになったり、実行時に C++03 とは異なる挙動をする可能性がある。


多分、std 名前空間に何かを定義していたり(これは仕様に反する)、ADL 周りで何かよく分からないことが起こっていたり(具体的なケースは思いつかない)、プリプロセッサでその識別子を定義していたり(これは結構ありそう)した場合は問題になるかもしれない。


新しいヘッダ

以下の C++ ヘッダが新しく追加された。

<array>
<atomic>
<chrono>
<codecvt>
<condition_variable>
<forward_list>
<future>
<initializer_list>
<mutex>
<random>
<ratio>
<regex>
<scoped_allocator>
<system_error>
<thread>
<tuple>
<typeindex>
<type_traits>
<unordered_map>
<unordered_set>

以下の C 互換ヘッダが新しく追加された。

<ccomplex>
<cfenv>
<cinttypes>
<cstdalign>
<cstdbool>
<cstdint>
<ctgmath>
<cuchar>

このため、もし C++03 でこれらのヘッダを include している場合は何らかの問題が出るかもしれない。


ただ、C++ の慣習として、ヘッダファイルは *.h や *.hpp にすることが多いので、これらのヘッダを include して問題になってしまうことはあまり無いだろう。


swap 関数の移動

C++03 では <algorithm> に定義されていた swap 関数は、C++11 では <utility> に移動した。

このため、swap を使う C++03 のコードで <utility> ヘッダを include していない場合、コンパイルが通らない可能性がある。


これが起こることはそれなりにあるかもしれない。なので、C++11 に対応した多くのコンパイラは、<algorithm> の中で勝手に <utility> を include するようになるんじゃないかと思う。swap 関数の移動に対応した C++ コンパイラの <algorithm> がどうなっているのかを追記するかも)


名前空間の予約

C++11 ではグローバル名前空間posix という名前空間を追加した。そのため、C++03 でグローバルに posix 名前空間を追加している場合は C++11 準拠のコードにならない。


とはいえ、単に予約しただけなので、実際にグローバルに posix 名前空間を追加していたとしても問題にはならないだろう。


属性識別子の追加

C++11 では

override
final
carries_dependency
noreturn

という属性識別子を追加したので、C++03 でこれらの名前のマクロを使っていた場合は C++11 準拠のコードにならない可能性がある。


これらのマクロが定義されていることは、それなりにあるかもしれない。問題になったら修正するしかない。


operator delete の挙動

C++03 のデフォルトの global operator delete[] の挙動は、operator delete を呼び出すということが保証されていなかったが、C++11 ではそうなるようになった。

そのため、C++11 では以下のコードは "custom deallocation" が 2 つ出力されることが保証される。

#include <cstdio>
#include <cstdlib>
#include <new>

void* operator new(std::size_t size) throw(std::bad_alloc) {
    return std::malloc(size);
}

void operator delete(void* ptr) throw() {
    std::puts("custom deallocation");
    std::free(ptr);
}

int main() {
    int* i = new int;
    delete i;            // 単体バージョンの delete
    int* a = new int[3];
    delete[] a;          // 配列バージョンの delete
}

(実際に C++03 でこれを試してみて追記するかも。そもそも C++03 の仕様書に書かれている global operator delete[] のデフォルトの挙動を読めば分かるけれども、明らかにバグっている)


operator new の例外

C++03 では、global operator new は std::bad_alloc だけしか投げなかったが、C++11 では他の例外を投げても構わないようになった。

そのため、C++03 で std::bad_alloc しか投げないという仮定でコードを書いていた場合は C++11 と異なる挙動になるかもしれない。


これは C++03 の例外仕様 throw(std::bad_alloc) が deprecated になり、新しく noexcept(true), noexcept(false) という形式になったことが影響している(どの例外を投げるか指定することができなくなった)。

また、new には std::bad_array_new_length という例外が追加され、これは不正な長さを渡したときに投げるらしい。つまり new に不正な長さを渡していて(そもそもこれがどんな値なのか分からない)、それが C++03 のコードだと std::bad_alloc を投げるようになっていて、catch (std::bad_alloc&) でしかハンドリングしていない場合に問題になるのだろうか。

try {
    int* p = new int[/*何か不正な長さを渡す*/];
    // C++03 では std::bad_alloc が投げられ、C++11 では std::bad_array_new_length が投げられる(とする)
} catch (std::bad_alloc&) { // std::bad_alloc しかハンドリングしていない
}

スレッド間の errno

C++03 で errno がスレッド間で同じ値だと仮定する(これは C++03 でも実装依存である)と、C++11 で異なる結果を引き起こすかもしれない。


GCミニマルサポート

(よく分からないので省略)


std::unary_function と std::binary_function の継承

C++11 では std::unary_function と std::binary_function は deprecated になったため、標準ライブラリはこれを継承しなくなった(unary/binary_function 自体が定義から消えたわけではない)。

そのため、C++03 でこれを継承していたことに依存していたコードがある場合は問題が起きるかもしれない。


しかし、unary_function, binary_function は多くの場合は単に typedef を書く労力を減らすためにあるだけで、unary/binary_function にキャストしたところで出来ることなんて何も無いため、キャストすることなんてまず無い。

なのでこのコードが問題になることはほぼ無いだろう。


basic_string の参照カウント実装の禁止

C++11 では basic_string を参照カウントで実装するのは実質的に不可能になったため、C++03 が参照カウントで実装されていた場合、文字列データが無効になるタイミングが微妙に変わることになる。


例えばメモリの使用量が少し増えてしまって動作しなくなってしまうようなアプリケーションは、あるかもしれない。


basic_string の内部のデータが無効になるタイミングの緩和

上記の参照カウント実装の禁止により、いくつかの const メンバ関数、例えば data() や c_str() 等を呼び出してもイテレータは無効にならなくなっている。


C++03 の仕様上では不正だがたまたま動いていたというケースを除き、無効になるタイミングの緩和が問題になるというケースは思いつかない。


size() の計算量

C++11 では size() の計算量が定数になった。

C++03 では std::list の size() が線形時間になっているものもあったが、そういう場合にはバイナリ互換性や計算時間の互換性が無くなる。


std::list のバイナリに依存したコードなんて書いている人はほぼいないだろうし、size() の計算が速くなって困るコードなんてそうそう無いだろうし、std::list がサイズをメンバに持ったとしても数バイト程度でほとんど影響が無いだろうし、それらが問題になることは無いと思う。

ただ、自作でコンテナを作っていて、size() を定数時間でない実装にしていた場合は、修正しないとコンテナとしての要件を満たせない。C++11 のコンテナの要件を満たせないことが問題になるかというと、そんなことはなくて、C++11 のコンテナの要件を満たしているという前提のコードを新しく追加して、自作のコンテナを渡そうとするまでは問題にならない。


コンテナ間のインターフェース

様々なコンテナを想定して動作するようにしたコードは、C++11 では正しく動かないかもしれない。C++11 でコンテナ間のポータブルに動作させるには、更に以下のことを想定する必要がある。

  • 全てのコンテナが size() を持っている訳ではない。 size() == 0 を使っている個所は empty() にする必要がある。
  • 全てのコンテナの生成直後が空であるとは限らない ( std::array )
  • 全てのコンテナの swap が定数時間であるとは限らない ( std::array )

これも上記のコンテナの要件と同じである。C++11 のコンテナの要件を満たすことを要求するコードを追加しない限りは問題ない。


コンテナのデフォルトコンストラクタに対する要求変更

C++03 でデフォルトコンストラクタを持たないユーザ定義型を使って動作していたコンテナは、C++11 ではコンパイルに失敗する可能性がある。


コンテナのメンバ関数戻り値

  • erase(iter) 【set, multiset, map, multimap】
  • erase(begin, end) 【set, multiset, map, multimap】
  • insert(pos, num, val) 【vector, deque, list, forward_list】
  • insert(pos, begin, end) 【vector, deque, list, forward_list】

C++03 ではこれらのメンバ関数戻り値は void だったが、C++11 では使いやすいパラメータに変更されている。

もし C++03 でこの戻り値が void であることに依存している(例えばこれらのメンバ関数ポインタを使うコード)場合、C++11 ではコンパイルに失敗する可能性がある。


これは例にあるように、メンバ関数へのポインタを取るためにシグネチャを書いている場合はコンパイルエラーになる(そんなコードは一度も見たことないけれども)。

void f(void (std::set<int>::*erase)(std::set<int>::iterator));

f(&std::set<int>::erase); // C++03 ならOK、C++11 ならコンパイルエラー

iterator から const_iterator へ変更

C++11 で、これらの引数iterator から const_iterator になったので、それに依存したコードはコンパイルに失敗する可能性がある。


これも上記の例と同じで、メンバ関数へのポインタを取るためにシグネチャを書いている場合はコンパイルエラーになる。


resize のオーバーロードの追加

C++03 では、resize のシグネチャ

resize(size_type, T = T());

だったが、C++11 では

resize(size_type);
resize(size_type, T);

となったので、この関数を使用する場所でコンパイルエラーになる可能性がある。


上記2つの例と同じでメンバ関数ポインタで問題が起きる可能性があるのだけれども、上記2つより問題は少し深刻である。

シグネチャを書かない場合にエラーになるからだ。

template<class F>
void f(F f);

f(&std::vector<int>::resize); // C++03 ならOK、C++11 ならコンパイルエラー(曖昧である)

むしろシグネチャを書いているなら問題ない。

void f(void (std::vector<int>::*f)(std::vector<int>::size_type, const int&));

f(&std::vector<int>::resize); // C++03 でも C++11 でも問題ない

ただやはり、シグネチャを書かない方が多いだろう。


アルゴリズムを呼び出した後の状態

C++03 では remove や remove_if を呼び出した後の範囲外のオブジェクトの状態については規定していなかった。

C++11 では、remove や remove_if を呼び出した後の範囲外のオブジェクトは、有効なオブジェクトだけれどもそれぞれの操作がどうなるかは規定しないとしている。これを "妥当だが未規定の状態" (valid but unspecified state) と呼ぶ。

C++03 で、この "妥当だが未規定の状態" であるオブジェクトにアクセスするようなコードを書いていた場合、C++11 では異なる挙動になる場合がある。


これはムーブセマンティクスの導入による影響である。remove や remove_if はムーブを使用することで無駄なコピーを発生させることなく動作させることができるようになる。

C++03 で範囲外の状態は規定されていないが、恐らくは元のデータが残ったままになっているだろう。

C++11 では、範囲外のオブジェクトはムーブを使用するため、"妥当だが未規定の状態" になっている可能性がある。

しかし、そもそもこの範囲外のオブジェクトにアクセスするなんてことはほぼ無い(アクセスしていたらそれはほぼ間違いなく未発見のバグである)だろう。


std::complex のバイナリ互換性

C++03 では特に規定されていなかった std::complex 型のバイナリ表現が、C++11 で規定されたため、C++03 のバイナリ表現に依存していた場合は互換性が無くなる可能性がある。

これは C99 の complex 指定された型とのバイナリ互換性がある


入出力クラスの operator bool

C++11 では入出力クラスの operator bool を explicit operator bool にしたため、以下のような処理を書いている場合にコンパイルエラーになる可能性がある。

void f(bool);

f(io);
  • operator== を使って true や false と比較する
if (io == true) ;
  • bool を返す関数から値を返す
bool f() {
    return io;
}
struct X { bool b; }
X x = { io };
const bool& b = io;

これは結構あるかもしれない。しかしそもそも iostream 系のライブラリを使っている人が少ないイメージがある。


std::ios_base::failure のベースクラス

C++03 では std::ios_base::failure のベースクラスは std::exception だったが、C++11 ではベースクラスが std::system_error に変わった。

そのため、このベースクラスに依存した処理は C++11 とは異なる処理になる可能性がある。


std::ios_base のフラグ

std::ios_base に定義されている各フラグは、ビットマスクの定義が変わって constexpr が付けられたため、整数型などの型であることに依存している場合、C++11 ではコンパイルエラーになる可能性がある。

#include <iostream>

int main() {
    int flag = std::ios_base::hex;
    std::cout.setf(flag); // エラー: setf は整数型を引数に取るわけではない
}

*1:これが C++03 の挙動に影響があるのかどうかがよく分かりませんでした。何か読み違えてる?

wraith13wraith13 2011/08/26 16:30 確か、いやらしいことに §C.1 あたりと重複する内容が抜けてるよ。 > §C.2

melponmelpon 2011/08/26 16:46 ではその部分は wraith13 さんに書いて頂くということで。

wraith13wraith13 2011/08/26 16:48 ごめんなさい!><

gintenlabogintenlabo 2011/08/26 21:30 その他にも, SFINAE 周りで微妙に差異がありますね.

典型的なもので言うと,参照の参照に対して今まで SFINAE が適用されていたのに,
C++0x では参照の参照を作ろうとしてもコンパイルエラーにならないので,
参照の参照を作ることで意図的に SFINAE を発動していたようなコードは,動作が変わります.
…まぁ,そんなコードは実質的にないと思いますけど.

melponmelpon 2011/08/27 06:39 ここにある変更にも実質的に無いようなのが結構ある気がしますけど、どういう議論を経て載せるか載せないか決定したのか気になるところですね…単に忘れてた?

anonymouse_useranonymouse_user 2012/03/10 00:44 export無くなるの面倒ですね。
C++ Builder Xで作ったコードを書き直せというのも
なかなか腹立たしいです。

Connection: close