템플릿 인수로 전달 된 함수


224

C ++ 템플릿 함수를 인수로 전달하는 규칙을 찾고 있습니다.

이것은 C ++에서 지원하는 예제입니다.

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

그러나이 기술에 대해 배우기는 어렵습니다. "템플릿 인수로서의 기능"에 대한 인터넷 검색은 그다지 중요하지 않습니다. 그리고 고전적인 C ++ Templates The Complete Guide도 놀랍게도 토론하지 않습니다 (적어도 내 검색은 아닙니다).

내가 가진 질문은 이것이 유효한 C ++ (또는 널리 지원되는 확장)인지 여부입니다.

또한 이러한 종류의 템플릿 호출 중에 동일한 서명을 가진 functor를 명시 적 함수와 호환 가능하게 사용할 수있는 방법이 있습니까?

구문이 틀리기 때문에 다음 프로그램은 적어도 Visual C ++ 에서 작동 하지 않습니다 . 사용자 정의 비교 연산을 정의하려는 경우 함수 포인터 또는 functor를 std :: sort 알고리즘에 전달할 수있는 방법과 유사하게 functor의 함수를 전환 할 수 있고 그 반대도 가능합니다.

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

    doOperation<add3>();

웹 링크 또는 두 개 또는 C ++ 템플릿 책의 페이지에 대한 포인터는 높이 평가됩니다!


템플릿 인수로 함수의 이점은 무엇입니까? 반환 유형을 템플릿 유형으로 사용하지 않습니까?
DaClown

관련 : 캡처가없는 람다는 함수 포인터로 붕괴 될 수 있으며 C ++ 17에서 템플릿 매개 변수로 전달할 수 있습니다. Clang은 정상적으로 컴파일하지만 현재 gcc (8.2)에는 버그가 있으며로도 "링크가 없음"으로 잘못 거부합니다 -std=gnu++17. C ++ 17 캡처리스 람다 constexpr 변환 연산자의 결과를 함수 포인터 템플릿 비 유형 인수로 사용할 수 있습니까? .
Peter Cordes

답변:


123

예, 유효합니다.

functors에서도 작동하게 만드는 일반적인 해결책은 다음과 같습니다.

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

이제 다음 중 하나로 호출 할 수 있습니다.

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

라이브보기

이것의 문제점은 컴파일러가 호출을 인라인하는 것이 까다로워지면 add2함수 포인터 유형 void (*)(int &)이 전달되고 있다는 것 doOperation입니다. (그러나 add3functor 인 인라인은 쉽게 인라인 될 수 있습니다. 여기서 컴파일러는 유형의 객체가 add3함수에 전달 된다는 것을 알고 있습니다. 이는 호출 할 함수가 add3::operator()알 수없는 함수 포인터가 아니라 임을 의미합니다 .)


19
이제 흥미로운 질문이 있습니다. 함수 이름을 전달할 때 함수 포인터가있는 것과 다릅니다. 컴파일 타임에 명시적인 함수입니다. 따라서 컴파일러는 컴파일 타임에 무엇이 있는지 정확히 알고 있습니다.
SPWorley

1
함수 포인터보다 functors를 사용하면 이점이 있습니다. functor는 클래스 내부에 포함될 수 있으므로 최적화를 위해 컴파일러에 더 많은 기능을 제공합니다 (예 : 인라이닝). 함수 포인터를 통한 호출을 최적화하기 위해 컴파일러를 세게 누르십시오.
Martin York

11
함수가 템플릿 매개 변수에 사용되면 전달 된 함수에 대한 포인터로 '부패'합니다. 매개 변수에 인수로 전달 될 때 배열이 포인터로 붕괴되는 방식과 유사합니다. 물론 포인터 값은 컴파일 타임에 알려져 있으며 컴파일러가 최적화 목적으로이 정보를 사용할 수 있도록 외부 연결이있는 함수를 가리켜 야합니다.
CB Bailey

5
몇 년 후, C ++ 11에서 함수를 템플릿 인수로 사용하는 상황이 훨씬 개선되었습니다. 더 이상 functor 클래스와 같은 Javaism을 사용할 수 없으며 정적 인라인 함수를 템플릿 인수로 직접 사용할 수 있습니다. 1970 년대의 Lisp 매크로와 비교해 보면 여전히 울부 짖지 만 C ++ 11은 수년에 걸쳐 확실히 발전했습니다.
pfalcon

5
c ++ 11은 rvalue 참조 ( template <typename F> void doOperation(F&& f) {/**/}) 로 함수를 사용하는 것이 더 좋지 않으므로 bind 예를 들어 bind 대신 bind-expression을 전달할 수 있습니까?
user1810087

70

템플릿 매개 변수는 유형 (typename T) 또는 값 (int X)으로 매개 변수화 할 수 있습니다.

코드 조각을 템플릿 화하는 "전통적인"C ++ 방식은 펑터 (functor)를 사용하는 것입니다. 즉, 코드는 객체에 있으므로 객체는 코드에 고유 한 유형을 부여합니다.

전통적인 함수로 작업 할 때이 기술은 잘 작동하지 않습니다. 유형 변경은 특정 함수를 나타내지 않기 때문에 가능한 많은 함수의 서명 만 지정하기 때문입니다. 그래서:

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

functor 케이스와 동일하지 않습니다. 이 예제에서 서명이 int X (int, int) 인 모든 함수 포인터에 대해 do_op가 인스턴스화됩니다. 이 경우를 완전히 인라인하려면 컴파일러가 매우 공격적이어야합니다. (컴파일러 최적화가 상당히 발전함에 따라 배제하지는 않습니다.)

이 코드가 우리가 원하는 것을 수행하지 못한다는 것을 알리는 한 가지 방법은 다음과 같습니다.

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

여전히 합법적이며 분명히 인라인되지 않습니다. 전체 인라인을 얻으려면 값으로 템플릿을 작성해야하므로 템플릿에서 기능을 완전히 사용할 수 있습니다.

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

이 경우 인스턴스화 된 각 버전의 do_op은 이미 사용 가능한 특정 기능으로 인스턴스화됩니다. 따라서 우리는 do_op의 코드가 "return a + b"처럼 보일 것으로 기대합니다. (Lips programmers, 당신의 능글 맞은 웃음을 멈 추세 요!)

또한 이것이 다음과 같은 이유로 우리가 원하는 것에 더 가깝다는 것을 확인할 수 있습니다.

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

컴파일에 실패합니다. GCC는 "오류 : 'func_ptr'은 상수 표현으로 나타날 수 없습니다. 즉, 컴파일러 시간에 우리의 op가 무엇인지 알 수있는 정보를 충분히 얻지 못했기 때문에 do_op을 완전히 확장 할 수 없습니다.

두 번째 예가 실제로 우리의 op를 완전히 요약하고 있고 첫 번째 예가 그렇지 않다면 템플릿은 무엇입니까? 뭐하는거야? 답은 유형 강제입니다. 첫 번째 예 에서이 리프는 작동합니다.

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

그 예는 효과가 있습니다! (나는 그것이 좋은 C ++을 제안하지는 않지만 ...) 무슨 일이 있었는지 do_op은 다양한 함수 의 시그니처 주위에 템플릿 화되어 있으며 각 개별 인스턴스화는 다른 유형의 강제 코드를 작성합니다. 따라서 fadd가있는 do_op의 인스턴스화 된 코드는 다음과 같습니다.

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.

이와 비교해 볼 때, 값 단위의 경우 함수 인수와 정확히 일치해야합니다.


2
"명확하게 인라인되지 않음" 에 대한 관찰에 직접 응답하는 후속 질문에 대해서는 stackoverflow.com/questions/13674935/… 를 참조하십시오 int c = do_op(4,5,func_ptr);.
Dan Nissenbaum

인라인되는 예는 여기를 참조하십시오. stackoverflow.com/questions/4860762/… 요즘 컴파일러가 꽤 똑똑해 보입니다.
BigSandwich

15

함수 포인터는 템플릿 매개 변수로 전달 될 수 있으며 이는 표준 C ++의 일부입니다 . 그러나 템플릿에서 이들은 포인터 대 함수가 아닌 함수로 선언되고 사용됩니다. 템플릿 인스턴스화 에서 이름이 아닌 함수의 주소를 전달합니다.

예를 들면 다음과 같습니다.

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

functor 유형을 템플릿 인수로 전달하려면 다음을 수행하십시오.

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

몇 가지 답변이 functor 인스턴스를 인수로 전달합니다.

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

템플릿 인수를 사용하여이 균일 한 모양을 얻을 수있는 가장 가까운 do_op것은 비 유형 매개 변수로 두 번 정의 하고 유형 매개 변수로 한 번 정의 하는 것입니다.

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

솔직히, 나는 이것이 컴파일되지 않기를 정말로 기대했지만 gcc-4.8 및 Visual Studio 2013에서 나에게 효과적이었습니다.


9

템플릿에서

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

매개 변수 T는 유형이 아닌 템플리트 매개 변수입니다. 이는 템플릿 함수의 동작이 매개 변수 값 (함수 포인터 상수가 컴파일 타임에 수정되어야 함)에 따라 변경됨을 의미합니다.

함수 객체와 함수 매개 변수 모두에서 작동하는 무언가를 원한다면 형식화 된 템플릿이 필요합니다. 그러나이를 수행 할 때 런타임에 함수에 오브젝트 인스턴스 (함수 오브젝트 인스턴스 또는 함수 포인터)를 제공해야합니다.

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

약간의 성능 고려 사항이 있습니다. 이 새로운 버전은 특정 함수 포인터가 런타임에만 참조되고 호출되는 반면 함수 포인터 템플릿은 사용되는 특정 함수 포인터에 따라 함수 호출 인라인으로 최적화 될 수 있으므로 함수 포인터 인수를 사용하면 효율성이 떨어질 수 있습니다. 함수 개체의 유형에 따라 특정 operator()개체가 완전히 결정되기 때문에 함수 개체는 형식이 지정된 템플릿을 사용하여 매우 효율적으로 확장 할 수 있습니다 .



0

편집 : 연산자를 참조로 전달해도 작동하지 않습니다. 간단하게하기 위해 함수 포인터로 이해하십시오. 당신은 포인터가 아닌 참조를 보냅니다. 나는 당신이 이와 같은 것을 쓰려고 노력하고 있다고 생각합니다.

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;
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.