noexcept operator が gcc-4.6.0 で正しく SFINAE されない件

gcc-4.6.0 では、以下のコードが正しくコンパイルされてしまう。

template<class T>
T&& declval() noexcept;

template< class T >
inline void f1( T& x ) noexcept( noexcept( declval<T&>().foo() ) )
{
  x.foo();
}

template< class T,
  bool Noexcept = noexcept( declval<T&>().foo() )
>
inline void f2( T& x ) noexcept( Noexcept )
{
  x.foo();
}

// a common and trivial mistake
template< class T >
inline void f3( T& x ) noexcept( declval<T&>().foo() )
{
  x.foo();
}


struct X
{
  void foo();
};

struct Y
{
  void foo() noexcept;
};

struct Z {};


int main()
{
  X x; Y y; Z z;

  static_assert( !noexcept( f1(x) ), "OK." );
  static_assert( !noexcept( f2(x) ), "OK." );
  // static_assert( !noexcept( f3(x) ), "shall be ill-formed(OK)." );

  static_assert(  noexcept( f1(y) ), "OK." );
  static_assert(  noexcept( f2(y) ), "OK." );
  // static_assert(  noexcept( f3(y) ), "shall be ill-formed(OK)." );

  static_assert(  noexcept( f1(z) ), "shall be ill-formed." );
  static_assert(  noexcept( f2(z) ), "shall be ill-formed." );
  static_assert( !noexcept( f3(z) ), "shall be ill-formed." );

}

本来ならば、単純ミスである noexcept( f3(z) ) は勿論、
noexcept( f1(z) )noexcept( f2(z) ) も ill-formed でなければならない。
何故ならば、未評価式 declval().foo() が ill-formed である以上、それを引数に取る noexcept 演算子も ill-formed でなければならない為だ。*1


このバグの為、 gcc-4.6.0 では

// if x.swap(y) is exist, calls it.
template<class T>
void my_swap_( T& x, T& y, int )
  noexcept( noexcept( std::declval<T&>().swap( std::declval<T&>() ) ) )
{
  x.swap( y );
}

// otherwise, calls [std::]swap
using std::swap;
template<class T>
void my_swap_( T& x, T& y, ... )
  noexcept( noexcept( swap( std::declval<T&>(), std::declval<T&>() ) ) )
{
  swap( x, y );
}


template<class T>
void my_swap( T& x, T& y )
  noexcept( noexcept( my_swap_( std::declval<T&>(), std::declval<T&>(), 0 ) ) )
{
  my_swap_( x, y, 0 );
}

のような、 noexcept 演算子を使った SFINAE による実装切り分けが 不可能になっている。*2
勿論、 noexcept 演算子を使わない SFINAE の方法もあるが、上記の例のように 戻り値型が void に固定されている場合には、 noexcept 演算子を使った SFINAE のみで済む方が、ずっとシンプルになることは明らかだろう。


* * *


というわけで、その旨を gcc にバグ報告として投げてみました。
http://gcc.gnu.org/bugzilla/show_bug.cgi?id=48468


もっとも、これは もしかしたら gcc 側で意図した動作なのかもしれません。
というのも、

template<class T>
struct X
{
  T value;
  
  friend auto operator==( X const& lhs, X const& rhs )
    -> decltype( lhs.value == rhs.value )
  {
    return lhs.value == rhs.value;
  }
  
};

上記のようなコードは、 T== で比較できない場合、 gcc だと、実際に == による比較を行わない場合でも、 X<T>インスタンス化の時点で、問答無用でコンパイルエラーになるからです。*3
コンパイルエラーを避けるためには、関数テンプレートのデフォルトテンプレート引数を用いて、

template<class T>
struct X
{
  T value;
  
  template< class T_ = T >
  friend auto operator==( X const& lhs, X const& rhs )
    -> decltype( std::declval<T_ const&>() == std::declval<T_ const&>() )
  {
    return lhs.value == rhs.value;
  }
  
};

のように書けばいいわけですが、これは初心者がパッと思いつくものではありません。
もし noexcept でも同様の動作になる場合、そういう厄介な問題が起きる頻度が更に跳ね上がる事になるため、それを考えると、関数テンプレート中で noexcept 演算子がエラーにならない現行の gcc の実装を一概に dis ることは出来ないのです。*4


…という旨を bug report にも書きたかったのですが、英語力のない僕には無理でした。
まぁでも、そういう意図があった場合には、その旨を返信してくれるでしょう、きっと。


というか、実際の所、前者のような operator== って、規格的にはどうなっているんでしょう。
勿論、 operator== の場合には、外部で

template<class T>
struct X
{
  T value;
};

template<class T>
inline auto operator==( X<T> const& lhs, X<T> const& rhs )
  -> decltype( lhs.value == rhs.value )
{
  return lhs.value == rhs.value;
}

と定義すれば済む問題なわけですが、メンバ関数 swap を SFINAE 有りで用意したい場合には、こういう小細工はできない訳で。
有識者の意見、求む。

*1:現行ドラフトである N3242 には、 noexcept 演算子中の未評価式を特別扱いするような規則は定められていない。

*2: SFINAE の適用には例外も幾つか定められているが、現行ドラフト N3242 には noexcept-expression や noexcept-specification を SFINAE 適用の例外とする規則は存在しない為、上記のような SFINAE は問題なく行えるべきである。

*3:規格でどのように規定されているかは、よく調べていないので 不明です。

*4:前半部分で何回も規格を引き合いに出したのは、「 ill-formed になる」という僕の解釈が正しい自信がなかったからです。