Fungsi dilewatkan sebagai argumen templat

225

Saya mencari aturan yang melibatkan fungsi templat C ++ sebagai argumen.

Ini didukung oleh C ++ seperti yang ditunjukkan oleh contoh di sini:

#include <iostream>

void add1(int &v)
{
  v+=1;
}

void add2(int &v)
{
  v+=2;
}

template <void (*T)(int &)>
void doOperation()
{
  int temp=0;
  T(temp);
  std::cout << "Result is " << temp << std::endl;
}

int main()
{
  doOperation<add1>();
  doOperation<add2>();
}

Namun, mempelajari teknik ini sulit. Googling untuk "berfungsi sebagai argumen templat" tidak menghasilkan banyak. Dan Template C ++ klasik Panduan Lengkap mengejutkan juga tidak membahasnya (setidaknya bukan dari pencarian saya).

Pertanyaan saya adalah apakah ini valid C ++ (atau hanya beberapa ekstensi yang didukung secara luas).

Juga, apakah ada cara untuk memungkinkan functor dengan tanda tangan yang sama untuk digunakan secara bergantian dengan fungsi eksplisit selama jenis permohonan template ini?

Berikut ini tidak berfungsi dalam program di atas, setidaknya dalam Visual C ++ , karena sintaksnya jelas salah. Akan lebih baik untuk dapat beralih fungsi untuk functor dan sebaliknya, mirip dengan cara Anda dapat melewati pointer fungsi atau functor ke std :: sort algoritma jika Anda ingin mendefinisikan operasi perbandingan kustom.

   struct add3 {
      void operator() (int &v) {v+=3;}
   };
...

    doOperation<add3>();

Pointer ke satu atau dua tautan web, atau satu halaman di buku C ++ Templates akan dihargai!

SPWorley
sumber
Apa manfaat fungsi sebagai argumen templat? Bukankah tipe pengembalian akan menggunakan tipe template?
DaClown
Terkait: lambda tanpa tangkapan dapat meluruh ke pointer fungsi, dan Anda dapat meneruskannya sebagai param template di C ++ 17. Dentang mengkompilasinya ok, tetapi gcc saat ini (8.2) memiliki bug dan menolaknya dengan benar karena tidak memiliki "tautan" bahkan dengan -std=gnu++17. Dapatkah saya menggunakan hasil dari operator konversi constexpr lambda C ++ 17 captureless sebagai argumen penunjuk fungsi templat-tipe? .
Peter Cordes

Jawaban:

124

Ya itu benar.

Untuk membuatnya bekerja dengan functors juga, solusi yang biasa adalah sesuatu seperti ini sebagai gantinya:

template <typename F>
void doOperation(F f)
{
  int temp=0;
  f(temp);
  std::cout << "Result is " << temp << std::endl;
}

yang sekarang dapat disebut sebagai:

doOperation(add2);
doOperation(add3());

Lihat itu langsung

Masalah dengan ini adalah bahwa jika itu membuat rumit bagi kompiler untuk inline panggilan ke add2, karena semua kompiler tahu adalah bahwa jenis fungsi pointer void (*)(int &)sedang diteruskan ke doOperation. (Tetapi add3, sebagai functor, dapat diuraikan dengan mudah. ​​Di sini, kompiler tahu bahwa objek bertipe add3dilewatkan ke fungsi, yang berarti bahwa fungsi yang dipanggil add3::operator(), dan bukan hanya beberapa pointer fungsi yang tidak diketahui.)

jalf
sumber
19
Nah, ini pertanyaan yang menarik. Ketika melewati nama fungsi, BUKAN seperti ada pointer fungsi yang terlibat. Ini adalah fungsi eksplisit, diberikan pada waktu kompilasi. Jadi kompiler tahu persis apa yang didapatnya pada waktu kompilasi.
SPWorley
1
Ada keuntungan menggunakan functors dibandingkan pointer fungsi. Functor dapat di-instanciated di dalam kelas dan dengan demikian memberikan lebih banyak opertunity kepada kompiler untuk optimisasi (seperti inlining). Kompiler akan sulit ditekan untuk mengoptimalkan panggilan melalui penunjuk fungsi.
Martin York
11
Ketika fungsi digunakan dalam parameter templat, ia 'meluruh' menjadi penunjuk ke fungsi yang diteruskan. Ini mirip dengan bagaimana array meluruh menjadi pointer ketika dilewatkan sebagai argumen ke parameter. Tentu saja, nilai penunjuk diketahui pada waktu kompilasi dan harus menunjuk ke suatu fungsi dengan tautan eksternal sehingga kompiler dapat menggunakan informasi ini untuk keperluan optimasi.
CB Bailey
5
Maju cepat ke beberapa tahun kemudian, situasi dengan menggunakan fungsi sebagai argumen templat banyak ditingkatkan di C ++ 11. Anda tidak lagi terikat untuk menggunakan Javaisms seperti kelas functor, dan dapat menggunakan misalnya fungsi inline statis sebagai argumen template secara langsung. Masih jauh dibandingkan dengan Lisp macro tahun 1970-an, tetapi C ++ 11 pasti memiliki kemajuan yang baik selama bertahun-tahun.
pfalcon
5
karena c ++ 11 bukankah akan lebih baik untuk mengambil fungsi sebagai rvalue reference ( template <typename F> void doOperation(F&& f) {/**/}), jadi bind misalnya dapat melewatkan ekspresi bind daripada mengikatnya?
user1810087
70

Parameter templat dapat berupa parameter berdasarkan jenis (jenis nama T) atau menurut nilai (int X).

The "tradisional" C ++ cara templating sepotong kode adalah dengan menggunakan functor - yaitu, kode itu dalam suatu objek, dan objek dengan demikian memberikan jenis kode yang unik.

Ketika bekerja dengan fungsi tradisional, teknik ini tidak berfungsi dengan baik, karena perubahan tipe tidak menunjukkan fungsi tertentu - melainkan hanya menentukan tanda tangan dari banyak fungsi yang mungkin. Begitu:

template<typename OP>
int do_op(int a, int b, OP op)
{
  return op(a,b);
}
int add(int a, int b) { return a + b; }
...

int c = do_op(4,5,add);

Tidak setara dengan kasing functor. Dalam contoh ini, do_op dipakai untuk semua pointer fungsi yang tanda tangannya adalah int X (int, int). Kompiler harus cukup agresif untuk sepenuhnya menyelaraskan kasus ini. (Saya tidak akan mengesampingkannya, karena optimasi kompiler sudah cukup maju.)

Salah satu cara untuk mengatakan bahwa kode ini tidak cukup melakukan apa yang kita inginkan adalah:

int (* func_ptr)(int, int) = add;
int c = do_op(4,5,func_ptr);

masih legal, dan jelas ini tidak mendapatkan inline. Untuk mendapatkan inlining penuh, kita perlu membuat templat berdasarkan nilai, sehingga fungsi tersebut sepenuhnya tersedia dalam templat.

typedef int(*binary_int_op)(int, int); // signature for all valid template params
template<binary_int_op op>
int do_op(int a, int b)
{
 return op(a,b);
}
int add(int a, int b) { return a + b; }
...
int c = do_op<add>(4,5);

Dalam hal ini, setiap versi do_op yang dipakai akan dipakai dengan fungsi tertentu yang sudah tersedia. Jadi kita mengharapkan kode do_op terlihat seperti "mengembalikan + b". (Pemrogram yang lalai, hentikan seringai Anda!)

Kami juga dapat mengkonfirmasi bahwa ini lebih dekat dengan yang kami inginkan karena ini:

int (* func_ptr)(int,int) = add;
int c = do_op<func_ptr>(4,5);

akan gagal dikompilasi. GCC mengatakan: "error: 'func_ptr' tidak dapat muncul dalam ekspresi konstan. Dengan kata lain, saya tidak dapat sepenuhnya memperluas do_op karena Anda belum memberi saya cukup info pada waktu kompiler untuk mengetahui apa op kita.

Jadi, jika contoh kedua benar-benar inlining sepenuhnya op kami, dan yang pertama tidak, apa gunanya template? Apa yang sedang dilakukannya? Jawabannya adalah: ketik paksaan. Riff ini pada contoh pertama akan berfungsi:

template<typename OP>
int do_op(int a, int b, OP op) { return op(a,b); }
float fadd(float a, float b) { return a+b; }
...
int c = do_op(4,5,fadd);

Contoh itu akan berhasil! (Saya tidak menyarankan itu baik C ++ tapi ...) Apa yang terjadi adalah do_op telah templated sekitar tanda tangan dari berbagai fungsi, dan setiap instansiasi terpisah akan menulis kode paksaan jenis yang berbeda. Jadi kode instantiated untuk do_op dengan fadd terlihat seperti:

convert a and b from int to float.
call the function ptr op with float a and float b.
convert the result back to int and return it.

Sebagai perbandingan, case-by-value kami membutuhkan kecocokan yang tepat pada argumen fungsi.

Ben Supnik
sumber
2
Lihat stackoverflow.com/questions/13674935/... untuk pertanyaan tindak lanjut sebagai tanggapan langsung terhadap pengamatan di sini yang int c = do_op(4,5,func_ptr);"jelas tidak mendapatkan inline".
Dan Nissenbaum
Lihat di sini untuk contoh dari yang sedang digarisbawahi ini: stackoverflow.com/questions/4860762/... Tampaknya kompiler semakin pintar akhir-akhir ini.
BigSandwich
15

Pointer fungsi dapat dilewatkan sebagai parameter templat, dan ini adalah bagian dari standar C ++ . Namun dalam template mereka dideklarasikan dan digunakan sebagai fungsi daripada pointer-ke-fungsi. Pada contoh instantiation satu melewati alamat fungsi bukan hanya nama.

Sebagai contoh:

int i;


void add1(int& i) { i += 1; }

template<void op(int&)>
void do_op_fn_ptr_tpl(int& i) { op(i); }

i = 0;
do_op_fn_ptr_tpl<&add1>(i);

Jika Anda ingin meneruskan jenis functor sebagai argumen templat:

struct add2_t {
  void operator()(int& i) { i += 2; }
};

template<typename op>
void do_op_fntr_tpl(int& i) {
  op o;
  o(i);
}

i = 0;
do_op_fntr_tpl<add2_t>(i);

Beberapa jawaban melewati instance functor sebagai argumen:

template<typename op>
void do_op_fntr_arg(int& i, op o) { o(i); }

i = 0;
add2_t add2;

// This has the advantage of looking identical whether 
// you pass a functor or a free function:
do_op_fntr_arg(i, add1);
do_op_fntr_arg(i, add2);

Yang paling dekat dengan penampilan seragam ini dengan argumen templat adalah mendefinisikan do_opdua kali dengan parameter non-tipe dan sekali dengan parameter tipe.

// non-type (function pointer) template parameter
template<void op(int&)>
void do_op(int& i) { op(i); }

// type (functor class) template parameter
template<typename op>
void do_op(int& i) {
  op o; 
  o(i); 
}

i = 0;
do_op<&add1>(i); // still need address-of operator in the function pointer case.
do_op<add2_t>(i);

Jujur, saya benar-benar berharap ini tidak dapat dikompilasi, tetapi itu bekerja untuk saya dengan gcc-4.8 dan Visual Studio 2013.

Kietz
sumber
9

Dalam templat Anda

template <void (*T)(int &)>
void doOperation()

Parameter Tadalah parameter template non-tipe. Ini berarti bahwa perilaku fungsi template berubah dengan nilai parameter (yang harus diperbaiki pada waktu kompilasi, yang merupakan konstanta penunjuk fungsi).

Jika Anda menginginkan sesuatu yang berfungsi dengan objek fungsi dan parameter fungsi, Anda memerlukan templat yang diketik. Ketika Anda melakukan ini, Anda juga perlu memberikan instance objek (baik instance function object atau function pointer) ke fungsi pada saat run time.

template <class T>
void doOperation(T t)
{
  int temp=0;
  t(temp);
  std::cout << "Result is " << temp << std::endl;
}

Ada beberapa pertimbangan kinerja kecil. Versi baru ini mungkin kurang efisien dengan argumen penunjuk fungsi karena penunjuk fungsi tertentu hanya didefinisikan dan dipanggil pada saat run time sedangkan templat penunjuk fungsi Anda dapat dioptimalkan (mungkin pemanggilan fungsi inline) berdasarkan pada penunjuk fungsi tertentu yang digunakan. Objek fungsi sering kali dapat diperluas secara efisien dengan templat yang diketik, meskipun seperti yang operator()ditentukan sepenuhnya oleh jenis objek fungsi.

CB Bailey
sumber
1

Alasan contoh functor Anda tidak berfungsi adalah karena Anda memerlukan sebuah instance untuk memanggil operator().

Nikolai Fetissov
sumber
0

Sunting: Melewati operator sebagai referensi tidak berfungsi. Untuk mempermudah, pahami sebagai fungsi penunjuk. Anda hanya mengirim pointer, bukan referensi. Saya pikir Anda mencoba menulis sesuatu seperti ini.

struct Square
{
    double operator()(double number) { return number * number; }
};

template <class Function>
double integrate(Function f, double a, double b, unsigned int intervals)
{
    double delta = (b - a) / intervals, sum = 0.0;

    while(a < b)
    {
        sum += f(a) * delta;
        a += delta;
    }

    return sum;
}

. .

std::cout << "interval : " << i << tab << tab << "intgeration = "
 << integrate(Square(), 0.0, 1.0, 10) << std::endl;
AraK
sumber