2011年12月25日日曜日

【C++11】 decltype, conditional operator, そして common_type

C++11 に限らず C++03 的にも基本的なところから不勉強で「そういうことだったのかー」と思うことが多いのであまり自信がないのですが、規格の不整合じゃないかと思われる点を見つけたので書いてみます。基本的に語尾に「と思います、多分」がついていると解釈ください。

とりあえず decltype(e) について FDIS(n3290、自分が持っているのは変更履歴付きの n3291 です) で以下の変更が加えられています(7.1.6.2/4)。

The type denoted by decltype(e) is defined as follows:
  • if e is an unparenthesized id-expression or an unparenthesized class member access (5.2.5), decltype(e) is the type of the entity named by e. If there is no such entity, or if e names a set of overloaded functions, the program is ill-formed;
  • otherwise, if e is a function call (5.2.2) or an invocation of an overloaded operator (parentheses around e are ignored), decltype(e) is the return type of the statically chosen function; an xvalue, decltype(e) is T&&, where T is the type of e;
  • otherwise, if e is an lvalue, decltype(e) is T&, where T is the type of e;
  • otherwise, decltype(e) is the type of e.
The operand of the decltype specifier is an unevaluated operand (Clause 5).

decltype((e)) としては関数呼び出し(オーバーロードされた演算子を含む)の場合はその(静的な)返値の型になるという条件付きの記述から、lvalue なら T&、xvalue なら T&&、prvalue なら T、と分かりやすくなったと言えます。さて、この結果、オーバーロードされていない演算子について、xvalue を返すものに対する delctype((e)) は T から T&& へ挙動が変わったことになります。conditional operator (?:) はオーバーロードできないので常にこの場合に入ります。実際のルールはかなりややこしいのですが第2オペランドと第3オペランドが(lvalue, xvalue, prvalue のカテゴリも含めて)同じ型のケースに限ればその同じ型(とカテゴリ)が全体の型(とカテゴリ)になります。従って以下のようになるはずです。

int&  funcL();
int&& funcX();
int   funcPR();

decltype(true ? funcL()  : funcL())  v1 = funcL();  // int&
decltype(true ? funcX()  : funcX())  v2 = funcX();  // int&&
decltype(true ? funcPR() : funcPR()) v3 = funcPR(); // int

さて、規格中で conditional operator と decltype に依存しているのが common_type です。

// n3291 20.2.4
template <class T>
typename add_rvalue_reference<T>::type declval() noexcept;

// n3291 20.9.7.6/3
template <class T, class U>
struct common_type<T, U> {
 typedef decltype(true ? declval<T>() : declval<U>()) type;
};

declval<int>() の返値の型は int&& であり xvalue です。従って common_type<int, int>::type は int && になってしまいます。decltype の挙動変更前は int でした。これがなぜやばいのか。例えば <chrono> にほ以下のような規定があります。

// 20.11.4.3 common_type specializations
template <class Rep1, class Period1, class Rep2, class Period2>
struct common_type<chrono::duration<Rep1, Period1>, chrono::duration<Rep2, Period2>> {
 typedef chrono::duration<typename common_type<Rep1, Rep2>::type, see below> type;
};

// 20.11.5.5, duration arithmetic
template <class Rep1, class Period1, class Rep2, class Period2>
typename common_type<duration<Rep1, Period1>, duration<Rep2, Period2>>::type
constexpr operator+(const duration<Rep1, Period1>& lhs, const duration<Rep2, Period2>& rhs);
// Returns: CD(CD(lhs).count() + CD(rhs).count())

operator+() の返値の型が common_type を使用し、common_type の特殊化でも common_type を使っているため、chrono::duration<int>() + chrono::duration<int>() の返値の型は chrono::duration<int&&> になります。これは規格の規定からも不正です(Rep は arithmetic type ないし arithmetic type をエミュレーションした class でないと駄目です)。また、そもそも自然な利用方法であろう

template<typename T, typename U>
typename std::common_type<T, U>::type operator+(const T& t, const U& u)
{
 return t+u;
}

に対して少なくとも T, U が同一の型の場合に(普通 operator+ は一時オブジェクトを返すため)、返値の型が rvalue reference で一時オブジェクトを束縛してもコンパイルエラーにならず、速攻で dangling reference を生んでしまいます。

自分の解釈だとこうなるのですが、識者の意見が欲しいところです……。

追記

GCC 4.6/4.7 では decltype の挙動が FDIS での修正に沿っていないようで、conditional operator の結果に対する decltype は lvalue の場合 T&(左辺値参照)として、それ以外の場合 T(非参照)として返ってくるようです。その結果 common_type も T(非参照)となり問題は発生しません。

追記2

declval<int>()が抜けていたので訂正しました。

追記3

LWG 2141で同様の指摘が挙げられ修正が決まったようです。

0 件のコメント:

コメントを投稿