İşlev şablon argümanı olarak geçti


225

Argüman olarak C ++ şablonları işlevlerini geçen kuralları arıyorum.

Bu, burada bir örnekte gösterildiği gibi C ++ tarafından desteklenir:

#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>();
}

Ancak bu tekniği öğrenmek zordur. "Şablon argümanı olarak işlev" için googling yapmak pek bir şey yapmaz. Ve klasik C ++ Şablonları Komple Kılavuz şaşırtıcı bir şekilde de tartışmıyor (en azından aramamdan değil).

Sorularım, bu geçerli C ++ (ya da sadece geniş çapta desteklenen bir uzantı) olup olmadığıdır.

Ayrıca, aynı imzalı functor'ın bu tür şablon çağırma sırasında açık işlevlerle dönüşümlü olarak kullanılmasına izin vermenin bir yolu var mı?

Sözdizimi açıkça yanlış olduğu için aşağıdaki programda, en azından Visual C ++ ' da çalışmaz . Özel bir karşılaştırma işlemi tanımlamak isterseniz, bir işlev işaretçisi veya işlevlisini std :: sort algoritmasına geçirebilmenize benzer şekilde, bir işlev için bir işlevi ya da tam tersi bir işlevi değiştirebilmeniz güzel olurdu.

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

    doOperation<add3>();

Bir veya iki web bağlantısına veya C ++ Şablonları kitabındaki bir sayfaya işaret etmek takdir edilecektir!


Bir fonksiyonun şablon argümanı olarak faydası nedir? Dönüş türü şablon türü olarak kullanılmaz mı?
DaClown

İlgili: yakalama olmayan bir lambda, bir işlev işaretçisine bozulabilir ve bunu C ++ 17'de şablon parametresi olarak iletebilirsiniz. Clang iyi derler, ancak mevcut gcc (8.2) bir hataya sahiptir ve yanlış bir şekilde "bağlantı yok" olarak reddeder -std=gnu++17. Bir C ++ 17 captureless lambda constexpr dönüşüm operatörünün sonucunu işlev işaretçisi şablonu tür olmayan bir bağımsız değişken olarak kullanabilir miyim? .
Peter Cordes

Yanıtlar:


124

Evet, geçerli.

Functors ile de çalışmasına gelince, normal çözüm bunun gibi bir şeydir:

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

şimdi ikisinden biri olarak adlandırılabilir:

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

Canlı izle

Buradaki sorun, derleyicinin çağrıyı satır içi yapmasını zorlaştırıyorsa add2, tüm derleyicinin bildiği için, bir işlev işaretçisi türünün void (*)(int &)iletilmesidir doOperation. (Ancak add3, bir functor olmak kolayca satır içine alınabilir. Burada, derleyici add3işleve türden bir nesnenin aktarıldığını bilir, yani çağrılacak işlevin add3::operator()sadece bilinmeyen bir işlev işaretçisi olmadığı anlamına gelir .)


19
Şimdi ilginç bir soru. Bir işlev adı iletildiğinde, ilgili bir işlev işaretçisi gibi DEĞİLDİR. Derleme zamanında verilen açık bir işlevdir. Derleyici derleme zamanında ne olduğunu tam olarak bilir.
SPWorley

1
Fonksiyon işaretçileri üzerinde functor kullanmanın bir avantajı vardır. Functor sınıf içinde örneklenebilir ve böylece derleyici için optimizasyonlar (satır içi gibi) için daha fazla seçenek sağlar. Derleyiciye bir çağrıyı işlev işaretçisi üzerinden optimize etmek için sert bir şekilde basılır.
Martin York

11
İşlev bir şablon parametresinde kullanıldığında, iletilen işlevin işaretçisine "bozulur". Parametrelere argüman olarak iletildiğinde, dizilerin işaretçilere nasıl bozulduğuna benzer. Tabii ki, işaretçi değeri derleme zamanında bilinir ve derleyicinin bu bilgileri optimizasyon amacıyla kullanabilmesi için harici bağlantıya sahip bir işleve işaret etmesi gerekir.
CB Bailey

5
Birkaç yıl sonra, C ++ 11'de şablon argümanları olarak işlevlerin kullanıldığı durum çok gelişti. Artık functor sınıfları gibi Javaizmleri kullanmak zorunda değilsiniz ve örneğin statik satır içi işlevlerini doğrudan şablon bağımsız değişkenleri olarak kullanabilirsiniz. 1970'lerin Lisp makrolarına kıyasla hala çok ağlıyor, ancak C ++ 11 kesinlikle yıllar boyunca iyi bir ilerleme kaydetti.
pfalcon

5
c ++ 11, işlevi rvalue reference ( template <typename F> void doOperation(F&& f) {/**/}) olarak almak daha iyi olmaz , bu nedenle örneğin bağlama, bağlama yerine bir bağlama ifadesi iletebilir mi?
user1810087

70

Şablon parametreleri, türe (T türü) veya değere (int X) göre parametrelendirilebilir.

Bir kod parçasını değiştirmenin "geleneksel" C ++ yolu, bir functor kullanmaktır - yani, kod bir nesnede bulunur ve nesne, kodun benzersiz bir türünü verir.

Geleneksel işlevlerle çalışırken, bu teknik iyi çalışmaz, çünkü türdeki bir değişiklik belirli bir işlevi göstermez - bunun yerine yalnızca birçok olası işlevin imzasını belirtir. Yani:

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);

Fonksiyoner durumu ile eşdeğer değildir. Bu örnekte, do_op imzası int X (int, int) olan tüm işlev işaretçileri için başlatılır. Derleyicinin bu durumu tam olarak sıralamak için oldukça agresif olması gerekir. (Derleyici optimizasyonu oldukça gelişmiş olduğu için bunu ekarte etmem.)

Bu kodun istediğimizi tam olarak yapmadığını söylemenin bir yolu:

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

hala yasal ve açıkçası bu satır içi değil. Tam satırsonu elde etmek için değere göre şablon yapmamız gerekir, böylece işlev şablonda tamamen kullanılabilir.

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);

Bu durumda, do_op'un her somutlaştırılmış versiyonu zaten mevcut olan belirli bir fonksiyonla somutlaştırılır. Bu nedenle do_op kodunun "return a + b" gibi görünmesini bekliyoruz. (Lisp programcıları, sırıtmanı durdur!)

Bunun istediğimiz şeye daha yakın olduğunu da doğrulayabiliriz çünkü:

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

derlemek başarısız olur. GCC diyor ki: "error: 'func_ptr' sabit bir ifadede görünemiyor.

Peki, ikinci örnek gerçekten bizim operasyonumuzu tamamen içeriyorsa ve birincisi değilse, şablon ne kadar iyi? Ne yapıyor? Cevap: zorlama türü. İlk örnekte bu riff işe yarayacak:

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);

Bu örnek işe yarayacak! (Ben iyi C ++ olduğunu önermiyorum ama ...) Ne oldu do_op çeşitli fonksiyonların imzaları etrafında templated olduğunu ve her ayrı örnekleme farklı tip zorlama kodu yazacaktır. Yani fadd ile do_op için başlatılmış kod şöyle görünür:

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.

Karşılaştırma olarak, by-value durumumuz işlev bağımsız değişkenleriyle tam olarak eşleşmeyi gerektirir.


2
Bkz stackoverflow.com/questions/13674935/... burada gözlem doğrudan yanıt takip sorusuna int c = do_op(4,5,func_ptr);"açıkça inlined almıyorum" dir.
Dan Nissenbaum

Çizginin bunun bir örneği için buraya bakın: stackoverflow.com/questions/4860762/… Derleyicilerin bu günlerde oldukça akıllı hale geldiği anlaşılıyor.
BigSandwich

15

İşlev işaretçileri şablon parametreleri olarak iletilebilir ve bu standart C ++ 'ın bir parçasıdır . Ancak, şablonda işaretçi-işlev yerine işlev olarak bildirilir ve kullanılır. Şablon örneklemede işlevin adresi yalnızca addan ziyade geçer.

Örneğin:

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);

Şablon bağımsız değişkeni olarak bir işlev türü iletmek istiyorsanız:

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);

Birkaç cevap bir functor örneğini argüman olarak iletir:

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);

Bir şablon argümanı ile bu tekdüze görünüme en yakın olan, do_opiki kez bir kez tür olmayan bir parametre ile ve bir kez de tür parametresi ile tanımlamaktır .

// 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);

Doğrusu, gerçekten derlemeye değil bu beklenen, ancak gcc-4.8 ve Visual Studio 2013 ile benim için çalıştı.


9

Şablonunuzda

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

Parametre T, tür olmayan bir şablon parametresidir. Bu, şablon işlevinin davranışının parametrenin değeriyle değiştiği anlamına gelir (derleme zamanında düzeltilmesi gereken, işaretçi sabitleri olan).

Hem işlev nesneleri hem de işlev parametreleriyle çalışan bir şey istiyorsanız, yazılı bir şablona ihtiyacınız vardır. Ancak bunu yaptığınızda, çalışma zamanında işleve bir nesne örneği (işlev nesnesi örneği veya işlev işaretçisi) sağlamanız gerekir.

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

Bazı küçük performans hususları vardır. Bu yeni sürüm, işlev işaretçisi bağımsız değişkenleriyle daha az verimli olabilir, çünkü belirli işlev işaretçisi yalnızca çalışma zamanında serbest bırakılır ve çağrılırken, işlev işaretçisi şablonunuz kullanılan işlev işaretçisi temel alınarak optimize edilebilir (muhtemelen işlev çağrısı çizgili). Fonksiyon nesneleri operator()tamamen fonksiyon nesnesinin tipine göre belirlendiği için, genellikle yazılan şablonla çok verimli bir şekilde genişletilebilir .


1

Functor örneğinizin çalışmamasının nedeni, çağırmak için bir örneğe ihtiyacınız olmasıdır operator().


0

Düzenleme: İşlecin başvuru olarak iletilmesi çalışmaz. Basitlik için, bir işlev işaretçisi olarak anlayın. Sadece işaretçiyi gönderirsiniz, referans değil. Bence böyle bir şey yazmaya çalışıyorsun.

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;
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.