rsp

関数への参照と関数ポインタ、func と &func

機能を引数にとりたいとき複数の方法があり、 関数ポインタ、 std::function 、などは何度もつかってきたものの、「関数への参照」に出会って少しとまどったので記録します

関数への参照

int func(double x){
  return x;
}

と定義されているとき、func の型は int(double) です。これを引き回したいとき

int(double) hoge = func;  // error

のようにコピーすることはできませんが、ポインタまたは参照をとることができます:

int (&ref)(double) = func;   // ok
int (*fptr)(double) = &func; // ok
int (*fptr2)(double) = func; // also ok

このとき、ref の型は int(&)(double)fptrfptr2 の型は int(*)(double) です:

std::cout << std::boolalpha;
std::cout << std::is_same_v<decltype(ref), int(&)(double)> << std::endl;
// -> true
std::cout << std::is_same_v<decltype(fptr), int(*)(double)> << std::endl;
// -> true

関数を引数に渡したときのテンプレート型推論

関数ポインタの代入にあたって

int (*fptr)(double) = &func; // ok
int (*fptr2)(double) = func; // also ok

の 2 種類の書き方が許容されるので、「&関数名」と「関数名」を混同して両方関数ポインタを表すものと思い込んでいましたが、2行めの書き方は関数型*1 int(double) から関数ポインタ型 int(*)(double) への暗黙の変換が行われている*2だけでした。

たまたま現代のコンピュータで「関数」がプログラム上のアドレスに JUMP することなので、 & でアドレス取得をしても、 *デリファレンスしても値が変わらない ということのようです。

テンプレート型推論を絡めると、(func)(&func) の違いをあらわにすることはできます。

template<typename Callable>
void check(Callable&& callable){
  if(std::is_same<decltype(callable), void(&)()>::value){
    std::cout << "void(&)()" << std::endl;
  }
  if(std::is_same<decltype(callable), void(*&&)()>::value){
    std::cout << "void(*&&)()" << std::endl;
  }
}
void func(){
}
  
int main() {
  check(func);    // -> void(&)()
  check(&func);   // -> void(*&&)()
}

func は lvalue なのでユニバーサル参照 Callable&&func への参照型となる void(&)() に推論され、
&func はアドレス取得演算子 & の結果として rvalue であり、 Callable&& はその rvalue への右辺値参照 void(* &&)() になります。

関数への参照に関する GCC のバグ

7.2.0 までの GCC で、関数への参照 T(&)(...) をコピーキャプチャするラムダがコンパイルできません。

8.0.1 では修正されているようです。

template<typename Callable>
auto enlambda(Callable&& callable){
  return [=](){
    callable();
  };
}
void f(){
}

auto lambda = enlambda(f);

Thank you @nus_miz!

参考URL

*1:この言い方が正しいかは分かりません。「関数型」「function types」でGoogle検索すると他のことがたくさんヒットしてしまうので…。

*2:配列 T arr[]; に対する arr と &arr の違いみたいな感じでしょうか。