読者です 読者をやめる 読者になる 読者になる

空飛ぶ気まぐれ雑記帳

主に趣味とかプログラミングについて扱います。

SFINAEやテンプレートの部分特殊化で気をつけたいこと

C++

はじめに

久々にSFINAE(Substitution Failure Is Not An Error)やテンプレートの部分特殊化(Partial Specialization)を久々に使ったら見事にハマったので、まとめときます。
ただし、用語の間違えについては自分の中でもあやふやなんで気づいた人は指摘お願いします。

SIFNAEとは

説明が面倒なので以下を参照。

C++ SFINAE - プログラミングの教科書を置いておくところ

環境について

多分、どの環境でも同じだけれども、ボクはVisual Studio 2015 Update2を使ってるんで他の環境ついては知りません。

本題

主に気をつけたいことは以下の3つです。いずれもコンパイルエラーの原因です。

  1. 解決可能なオーバーロードが複数ある。
  2. 実は lvalue もしくは rvalue な参照がテンプレート引数になっている(のに気が付いていない)
  3. 配列を受け取りたいのに何故かエラーになる

どれも解決までに結構悩んだものです。

解決可能なオーバーロードが複数ある

これは最もやらかしやすいと思う。
まあ、コンパイルエラーを読めば、そのまんま書いてあるからその通りに直せば良いのだけれども、量が増えるとそうは行かなくなる。
以下は実例。

#include <iostream>
#include <type_traits>

template <class Tp, class = void>
struct type_impl
{
  static void print() { std::cout << "default" << std::endl; }
};

// 本来はポインタ型だけを受け取りたい特殊化
template <class Pointer>
struct type_impl<Pointer, std::enable_if_t<std::is_pointer<Pointer>::value>>
{
  static void print() { std::cout << "Pointer" << std::endl; }
};

// 本来はPOD型を受け取りたい特殊化
template <class POD>
struct type_impl<POD, 
  std::enable_if_t<std::is_pod<POD>::value>
>
{
  static void print() { std::cout << "POD" << std::endl; }
};

template <class Tp>
struct type : public type_impl<Tp> {};

int main()
{
  type<int*>::print();
  type<int>::print();
  return 0;
}

これ、エラーになります。
見ればわかるんですけど、is_podはポインタを受け取ってもtrueになってしまうからですね。
当然、解決策の検討が必要になるんだけれども、最も安直な方法はこれ。
16行目にポインタを受け取らないようにチェックを足してやる

template <class POD>
struct type_impl<POD, 
  std::enable_if_t<
    std::is_pod<POD>::value && 
    !std::is_pointer<POD>::value  // ポインタは受け取りませんと明示する
  >
>
{
  static void print() { std::cout << "Literal" << std::endl; }
};

実は lvalue もしくは rvalue な参照がテンプレート引数になっている(のに気が付いていない)

以下がその例。っていうかさっきと同じコードのmain関数に1行だけ足した

#include <iostream>
#include <type_traits>

template <class Tp, class = void>
struct type_impl
{
  static void print() { std::cout << "default" << std::endl; }
};

template <class Pointer>
struct type_impl<Pointer, std::enable_if_t<std::is_pointer<Pointer>::value>>
{
  static void print() { std::cout << "Pointer" << std::endl; }
};

template <class POD>
struct type_impl<POD, std::enable_if_t<
  std::is_pod<POD>::value &&
  !std::is_pointer<POD>::value>
>
{
  static void print() { std::cout << "POD" << std::endl; }
};

template <class Tp>
struct type : public type_impl<Tp> {};

int main()
{
  type<int*>::print();
  type<int>::print();
  type<int&>::print(); // 足した部分
  
  return 0;
}

さてこれの出力はどうなるでしょうか?
まあ、当たり前なんだけど

Pointer
POD
default

になります。これがどういう時に困るかというと、以下の様な関数を足した時。

template <class Tp, class Traits = type<Tp>>
void print(Tp&& value)
{
  Traits::print(/* std::forward<Tp>(value) */); // 実際にはvalueを渡すことが多かろう
}

// in int main()
  int hoge = 0;
  print(10);              // 多分 lvalue reference
  print(std::move(hoge)); // 間違いなく rvalue reference
  print(&hoge);           // 間違いなくポインタ
  print(hoge);            // ?

で、これの出力が以下のようになって欲しいのに、

POD
POD
Pointer
POD

以下のようになってしまいます。

POD
default
Pointer
default

一瞬うーんと成るかも知れませんが、参照はPOD型ではありません。
それ故、これを解決するためには remove_reference を使い以下のようにPOD型の判定を行う際に参照を外す必要があります。

template <class POD>
struct type_impl<POD, std::enable_if_t<
  std::is_pod<std::remove_reference_t<POD>>::value &&
  !std::is_pointer<POD>::value>
>
{
  static void print() { std::cout << "POD" << std::endl; }
};

配列を受け取りたいのに何故かエラーになる

これが一番、厄介、というか一番気づくまでに時間がかかった。
さて、ここで言う配列というのは一次元、それも固定長の配列だ。
この配列だが一般的に以下の方法で配列のサイズを取得することが可能とされている

template <class Array, std::size_t Size>
constexpr std::size_t size(Array(&)[Size])
{
  return Size;
}

int hoge[5] = {};
std::cout << size(hoge) << std::endl; // 5

この方法を応用して以下の様なコードを考える。

#include <iostream>
#include <type_traits>

template <class Tp, class = void>
struct type_impl
{
  static void print(Tp&&) { std::cout << "default" << std::endl; }
};

template <class Array, std::size_t Size>
struct type_impl<Array[Size], std::enable_if_t<std::is_array<Array>::value>>
{
  static void print(Array (&& value)[Size]) { std::cout << "Array:(" << value << "," << Size << ")" << std::endl; }
};

template <class POD>
struct type_impl<POD, std::enable_if_t<
  std::is_pod<std::remove_reference_t<POD>>::value &&
  !std::is_array<POD>::value>
>
{
  static void print(POD&& p) { std::cout << "POD:" << p << std::endl; }
};

template <class Tp>
struct type : public type_impl<Tp> {};

template <class Tp, class Traits = type<Tp>>
void print(Tp&& value)
{
  Traits::print(std::forward<Tp>(value));
}

int main()
{
  int hoge = 20;
  int array[10] = {};
  print(10);
  print(hoge);
  print(array);

  return 0;
}

これを実行すると期待的には以下のとおりにしたいのだが、

POD:10
POD:20
Array:(<arrayのアドレス>,10)

実際は、

POD:10
POD:20
POD:<arrayのアドレス>

となる。
実は、ここで犯しているのはテンプレートの置き換え云々の問題だけではない。
以下がその問題と修正したコードだ。

  • 配列は rvalue referenceで受け取れない。
  • 特殊化するのはArrayだけではなく、Array[Size]。
#include <iostream>
#include <type_traits>

template <class Tp, class = void>
struct type_impl
{
  static void print(Tp&&) { std::cout << "default" << std::endl; }
};

template <class Array, std::size_t Size>
struct type_impl<Array[Size], std::enable_if_t<std::is_array<Array[Size]>::value>>  // 型の渡し方を変える
{
  // Array (&&)[Size] ではなく Array(&)[Size]
  static void print(Array (&value)[Size]) { std::cout << "Array:(" << value << "," << Size << ")" << std::endl; }
};

template <class POD>
struct type_impl<POD, std::enable_if_t<
  std::is_pod<std::remove_reference_t<POD>>::value &&
  !std::is_array<POD>::value>
>
{
  static void print(POD&& p) { std::cout << "POD:" << p << std::endl; }
};

template <class Tp>
struct type : public type_impl<Tp> {};

template <class Tp, class Traits = type<Tp>>
void print(Tp&& value)
{
  Traits::print(std::forward<Tp>(value));
}

// オーバーロードする
template <class Tp, std::size_t Size, class Traits = type<Tp[Size]>>
void print(Tp (&value)[Size])
{
  Traits::print(value);
}

int main()
{
  int hoge = 20;
  int array[10] = {};
  print(10);
  print(hoge);
  print(array);

  return 0;
}

こうすることで期待通りの結果得られる筈だ。

おわりに

未だにSFINAE機構を使うとやらかすことが多いので、メモを兼ねて記した。
もし、SFINAEやテンプレートの特殊化で困ったことがあれば、この話を思い出して欲しい。